├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md └── src ├── CMakeLists.txt ├── cmake.deps └── FetchPandocMan.cmake ├── cmake_uninstall.cmake.in ├── config.cpp ├── config.hpp ├── man └── ncpamixer.1.md ├── pa.cpp ├── pa.hpp ├── pa_card.cpp ├── pa_card.hpp ├── pa_input.cpp ├── pa_input.hpp ├── pa_object.cpp ├── pa_object.hpp ├── pa_object_attribute.hpp ├── pa_port.hpp ├── pa_sink.cpp ├── pa_sink.hpp ├── pa_source.cpp ├── pa_source.hpp ├── pa_source_output.cpp ├── pa_source_output.hpp ├── pulsemixer.cpp ├── ui ├── tab.cpp ├── tab.hpp ├── tabs │ ├── configuration.cpp │ ├── configuration.hpp │ ├── fallback.cpp │ ├── fallback.hpp │ ├── input.cpp │ ├── input.hpp │ ├── output.cpp │ ├── output.hpp │ ├── playback.cpp │ ├── playback.hpp │ ├── recording.cpp │ └── recording.hpp ├── ui.cpp └── ui.hpp ├── version.cpp.in └── version.hpp /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [c0r73x] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /test.cpp 2 | /build 3 | /pulsemixer2.cpp 4 | /stderr.txt 5 | *.plist 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BASE_DIR=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 2 | 3 | BUILD_TYPE=debug 4 | BUILD_INFO=Debug 5 | 6 | ifdef RELEASE_DBG_INFO 7 | BUILD_TYPE=release_dbg_info 8 | BUILD_INFO=RelWithDebInfo 9 | export RELEASE_DBG_INFO=1 10 | endif 11 | ifdef RELEASE 12 | BUILD_TYPE=release 13 | BUILD_INFO=Release 14 | export RELEASE=1 15 | endif 16 | 17 | ifdef PREFIX 18 | CMAKE_PREFIX="-DCMAKE_INSTALL_PREFIX=$(shell readlink -f $(PREFIX))" 19 | endif 20 | 21 | ifdef USE_WIDE 22 | CMAKE_USE_WIDE="-DUSE_WIDE=TRUE" 23 | endif 24 | 25 | .PHONY: all build distclean clean release release_dbg_info debug install verifybuildtype 26 | .SILENT: 27 | 28 | all: release 29 | 30 | build: build/Makefile 31 | $(MAKE) -C build/ 32 | 33 | build/Makefile: src/CMakeLists.txt 34 | @mkdir -p "build" 35 | @cd "build" && \ 36 | cmake \ 37 | -DCMAKE_BUILD_TYPE=$(BUILD_INFO) \ 38 | -DCMAKE_EXPORT_COMPILE_COMMANDS=1 \ 39 | "$(BASE_DIR)/src" \ 40 | $(CMAKE_USE_WIDE) \ 41 | $(CMAKE_PREFIX) 42 | 43 | verifybuildtype: build/Makefile 44 | if ! grep -q "^CMAKE_BUILD_TYPE:STRING=$(BUILD_TYPE)$$" build/CMakeCache.txt > /dev/null; \ 45 | then cd build;cmake -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) $(BASE_DIR)/src; \ 46 | fi 47 | 48 | debug: 49 | $(MAKE) verifybuildtype DEBUG=1 50 | $(MAKE) build DEBUG=1 51 | 52 | release: 53 | $(MAKE) verifybuildtype RELEASE=1 54 | $(MAKE) build RELEASE=1 55 | 56 | release_dbg_info: 57 | $(MAKE) verifybuildtype RELEASE_DBG_INFO=1 58 | $(MAKE) build RELEASE_DBG_INFO=1 59 | 60 | install: build 61 | $(MAKE) -C build install 62 | 63 | uninstall: build/install_manifest.txt 64 | $(MAKE) -C build uninstall 65 | 66 | clean: 67 | $(MAKE) -C "build" clean 68 | 69 | distclean: 70 | @rm -rf build 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ncurses PulseAudio Mixer 2 | 3 | An ncurses mixer for PulseAudio inspired by pavucontrol. 4 | 5 | ![demo](https://cloud.githubusercontent.com/assets/1078548/17714097/90dff48c-63fe-11e6-8d37-1d20c44981ef.gif) 6 | 7 | ### Config 8 | Your configuration gets created on first run. If `$XDG_CONFIG_HOME` is defined then it will be created at `$XDG_CONFIG_HOME/ncpamixer.conf` otherwise `$HOME/.ncpamixer.conf` 9 | 10 | 11 | 12 | ### Custom colors? Why not! 13 | ncpamixer supports 256 colors. You can change them in ncpamixer.conf 14 | 15 | ### Custom bindings? Sure! 16 | You find them in ncpamixer.conf 17 | 18 | #### Default bindings 19 | | Event | Default | Description | 20 | | --- | --- |---| 21 | | switch | tab | Cycle trought sinks, outputs, profile and ports | 22 | | select | enter | Select option in dropdowns | 23 | | quit | escape | Quit | 24 | | quit | q | Quit | 25 | | dropdown | c | Open dropdown for selecting sinks, outputs, profile and ports | 26 | | mute | m | Mute selected item | 27 | | set_default | d | Set default sink/source | 28 | | volume_up | l | Increase volume on selected item | 29 | | volume_down | h | Decrease volume on selected item | 30 | | volume_up | arrow right | Increase volume on selected item | 31 | | volume_down | arrow left | Decrease volume on selected item | 32 | | move_up | k | Move up | 33 | | move_down | j | Move down | 34 | | move_up | arrow up | Move up | 35 | | move_down | arrow down | Move down | 36 | | page_up | page up | Previous page in dropdown | 37 | | page_down | page down | Next page in dropdown | 38 | | tab_next | L | Next tab | 39 | | tab_prev | H | Previous tab | 40 | | tab_playback | F1 | Jump to playback tab| 41 | | tab_recording | F2 | Jump to recording tab | 42 | | tab_output | F3 | Jump to output tab | 43 | | tab_input | F4 | Jump to input tab | 44 | | tab_config | F5 | Jump to configuration tab | 45 | | move_last | G | Move to last item | 46 | | move_first | g | Move to first item | 47 | | set_volume_0 | 0 | Set volume to 0% | 48 | | set_volume_10 | 1 | Set volume to 10% | 49 | | set_volume_20 | 2 | Set volume to 20% | 50 | | set_volume_30 | 3 | Set volume to 30% | 51 | | set_volume_40 | 4 | Set volume to 40% | 52 | | set_volume_50 | 5 | Set volume to 50% | 53 | | set_volume_60 | 6 | Set volume to 60% | 54 | | set_volume_70 | 7 | Set volume to 70% | 55 | | set_volume_80 | 8 | Set volume to 80% | 56 | | set_volume_90 | 9 | Set volume to 90% | 57 | | help | ? | Pop up with keycode information | 58 | | set_volume_100 | Unbound | Set volume to 100% | 59 | | toggle_static | Unbound | Toggle barmode static/none static | 60 | 61 | ### Dependencies 62 | * PulseAudio :alien: 63 | * ncurses 64 | 65 | ### Build dependencies 66 | * CMake 67 | * C++14 compatible compiler 68 | * Pandoc for `manpages` 69 | 70 | On Debian(-based) systems, you'd need `libncurses-dev` and `libpulse-dev`. 71 | 72 | ### Building 73 | * In the main directory, run `make` 74 | * On some systems (tested on Debian-stable), you may need to instead run `make USE_WIDE=True` 75 | * The above command is needed to get UTF-8 support; it will cause CMake to look for the ncursesw library, and it'll link to it with -lncursesw 76 | * (This also means you might have to first `rm build/CMakeCache.txt` if you ran `make` without using the USE_WIDE setting, so it'll re-generate the Makefile) 77 | 78 | ### Install 79 | 80 | ##### Arch Linux 81 | `packer -S ncpamixer-git` 82 | https://aur.archlinux.org/packages/ncpamixer-git/ 83 | 84 | #### Gentoo ebuild 85 | https://github.com/fulhax/fulhax-overlay/tree/master/media-sound/ncpamixer 86 | 87 | ### Tested on 88 | * Gentoo + cachyos-kernel 6.13.7, PipeWire 1.4.1 and ncurses 6.5_p20250308 89 | * Gentoo kernel 4.12.10, PulseAudio 11.0 and ncurses 6.0-r1 90 | * Gentoo kernel 4.6.2, PulseAudio 9.0 and ncurses 6.0-r1 91 | * Arch Linux 4.6.4-1 PulseAudio 9.0 and ncurses 6.0-4 92 | * Fedora kernel 4.18.10-200, PulseAudio 12.2 and ncurses 6.1-5 93 | 94 | ### License 95 | 96 | MIT 97 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.11) 2 | 3 | project(ncpamixer) 4 | set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) 5 | set(CMAKE_CXX_STANDARD 17) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 8 | 9 | option(BUILD_MANPAGES "Build Man pages using pandoc" ON) 10 | 11 | set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) 12 | 13 | if(USE_WIDE) 14 | set(CURSES_NEED_WIDE TRUE) 15 | endif() 16 | 17 | include(FetchContent) 18 | include(GNUInstallDirs) 19 | 20 | if (BUILD_MANPAGES) 21 | include("${CMAKE_CURRENT_SOURCE_DIR}/cmake.deps/FetchPandocMan.cmake") 22 | include(PandocMan) 23 | add_pandoc_man("${CMAKE_CURRENT_SOURCE_DIR}/man/ncpamixer.1.md") 24 | endif() 25 | 26 | set(CURSES_NEED_NCURSES TRUE) 27 | find_package(Curses REQUIRED) 28 | # See https://gitlab.kitware.com/cmake/cmake/issues/18517 29 | find_library(MENU_LIBRARY menu) 30 | 31 | find_package(PulseAudio REQUIRED) 32 | find_package(Threads REQUIRED) 33 | 34 | string(TIMESTAMP BUILD_DATE "%Y-%m-%d %H:%M") 35 | if(NOT CMAKE_BUILD_TYPE) 36 | set(CMAKE_BUILD_TYPE "empty") 37 | endif() 38 | string(TOLOWER ${CMAKE_BUILD_TYPE} BUILD_TYPE) 39 | 40 | execute_process(COMMAND 41 | "git" "describe" "--tags" "--dirty" 42 | WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" 43 | OUTPUT_VARIABLE GIT_VERSION 44 | ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE 45 | ) 46 | 47 | configure_file( 48 | "${CMAKE_CURRENT_SOURCE_DIR}/version.cpp.in" 49 | "${CMAKE_CURRENT_BINARY_DIR}/version.cpp" 50 | ) 51 | 52 | message("-- Got version ${GIT_VERSION}") 53 | 54 | set(COMMON_SRC 55 | "pulsemixer.cpp" 56 | "pa_object.cpp" 57 | "pa_card.cpp" 58 | "pa_sink.cpp" 59 | "pa_input.cpp" 60 | "pa_source.cpp" 61 | "pa_source_output.cpp" 62 | "pa.cpp" 63 | "config.cpp" 64 | "ui/ui.cpp" 65 | "ui/tab.cpp" 66 | "ui/tabs/playback.cpp" 67 | "ui/tabs/recording.cpp" 68 | "ui/tabs/output.cpp" 69 | "ui/tabs/input.cpp" 70 | "ui/tabs/configuration.cpp" 71 | "ui/tabs/fallback.cpp" 72 | "${CMAKE_CURRENT_BINARY_DIR}/version.cpp" 73 | ) 74 | 75 | include_directories( 76 | "src/" 77 | ${CURSES_INCLUDE_DIR} 78 | ${PULSEAUDIO_INCLUDE_DIR} 79 | ) 80 | 81 | add_executable(${CMAKE_PROJECT_NAME} ${COMMON_SRC} ${UI_RESOURCES}) 82 | 83 | target_link_libraries(${CMAKE_PROJECT_NAME} 84 | ${CURSES_LIBRARIES} 85 | ${MENU_LIBRARY} 86 | ${PULSEAUDIO_LIBRARY} 87 | ${CMAKE_THREAD_LIBS_INIT} 88 | ) 89 | 90 | target_compile_options(${CMAKE_PROJECT_NAME} PUBLIC "-Werror") 91 | target_compile_options(${CMAKE_PROJECT_NAME} PUBLIC "-Wall") 92 | target_compile_options(${CMAKE_PROJECT_NAME} PUBLIC "-Wpedantic") 93 | 94 | install(TARGETS ${CMAKE_PROJECT_NAME} RUNTIME DESTINATION bin) 95 | 96 | # uninstall target 97 | if(NOT TARGET uninstall) 98 | configure_file( 99 | "${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in" 100 | "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" 101 | IMMEDIATE @ONLY) 102 | 103 | add_custom_target(uninstall 104 | COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) 105 | endif() 106 | -------------------------------------------------------------------------------- /src/cmake.deps/FetchPandocMan.cmake: -------------------------------------------------------------------------------- 1 | FetchContent_Declare( 2 | cmake_modules 3 | GIT_REPOSITORY https://github.com/rnpgp/cmake-modules.git 4 | GIT_TAG main 5 | ) 6 | 7 | FetchContent_MakeAvailable(cmake_modules) 8 | FetchContent_GetProperties(cmake_modules) 9 | list(APPEND CMAKE_MODULE_PATH "${cmake_modules_SOURCE_DIR}") 10 | 11 | -------------------------------------------------------------------------------- /src/cmake_uninstall.cmake.in: -------------------------------------------------------------------------------- 1 | if(NOT EXISTS "@CMAKE_BINARY_DIR@/install_manifest.txt") 2 | message(FATAL_ERROR "Cannot find install manifest: @CMAKE_BINARY_DIR@/install_manifest.txt") 3 | endif(NOT EXISTS "@CMAKE_BINARY_DIR@/install_manifest.txt") 4 | 5 | file(READ "@CMAKE_BINARY_DIR@/install_manifest.txt" files) 6 | string(REGEX REPLACE "\n" ";" files "${files}") 7 | foreach(file ${files}) 8 | message(STATUS "Uninstalling $ENV{DESTDIR}${file}") 9 | if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") 10 | exec_program( 11 | "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" 12 | OUTPUT_VARIABLE rm_out 13 | RETURN_VALUE rm_retval 14 | ) 15 | if(NOT "${rm_retval}" STREQUAL 0) 16 | message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}") 17 | endif(NOT "${rm_retval}" STREQUAL 0) 18 | else(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") 19 | message(STATUS "File $ENV{DESTDIR}${file} does not exist.") 20 | endif(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") 21 | endforeach(file) 22 | -------------------------------------------------------------------------------- /src/config.cpp: -------------------------------------------------------------------------------- 1 | #include "config.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #define MAX_LINE 300 16 | 17 | Config config; 18 | 19 | std::string Config::getKeycodeName(const char *keycode) 20 | { 21 | if (keycode == nullptr) return {}; 22 | if (keycode[0] == 0) return {}; 23 | if (memcmp(keycode, KEY, KEY_SIZE) != 0) return {}; 24 | if (strlen(keycode) < KEY_SIZE + 2) return {}; 25 | const auto key_val{keycode + KEY_SIZE + 1}; 26 | const auto is_vt100{key_val[0] == 'f' && key_val[1] == '.'}; 27 | int k{0}; 28 | try { k = std::stoi((is_vt100) ? key_val + 2 : key_val); } 29 | catch (const std::exception&) { return{}; } 30 | if (is_vt100) return { "VT100_F" + std::to_string(k - 79) }; 31 | return {keyname(k)}; 32 | } 33 | 34 | const config_map Config::getConfig() const 35 | { 36 | return config; 37 | } 38 | 39 | const config_map Config::getKeycodeNameEvents() const 40 | { 41 | config_map keycodes{}; 42 | 43 | for (const auto& it : config) { 44 | const auto kn{getKeycodeName(it.first.c_str())}; 45 | if (kn.empty()) continue; 46 | keycodes[kn] = it.second; 47 | } 48 | return keycodes; 49 | } 50 | 51 | const char *Config::getHomeDir() 52 | { 53 | const char *homedir = getenv("HOME"); 54 | if (homedir != nullptr) return homedir; 55 | passwd pwd = {nullptr}, *result{nullptr}; 56 | size_t bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); 57 | if (bufsize == static_cast(-1)) bufsize = 16384; 58 | 59 | std::unique_ptr buf = std::make_unique(bufsize); 60 | getpwuid_r(getuid(), &pwd, buf.get(), bufsize, &result); 61 | if (result == nullptr) { 62 | fprintf(stderr, "Unable to find home-directory\n"); 63 | exit(EXIT_FAILURE); 64 | } 65 | 66 | homedir = result->pw_dir; 67 | return homedir; 68 | } 69 | 70 | bool Config::getDefaultConfigFile() 71 | { 72 | std::error_code ec{}; 73 | std::string f{FILENAME}; 74 | const auto xdg{getenv(XDG_CONFIG)}; 75 | fs::path config_dir{xdg == nullptr ? "" : xdg}; 76 | if (!fs::is_directory(config_dir, ec)) 77 | { 78 | const auto home{getHomeDir()}; 79 | if (home) config_dir = fs::path(home); 80 | f.insert(0, 1, '.'); 81 | } 82 | 83 | if (fs::is_directory(config_dir, ec)) 84 | { 85 | full_path = fs::absolute(config_dir / f, ec); 86 | if (!fs::exists(full_path, ec)) 87 | { 88 | fprintf( 89 | stderr, 90 | "Default file '%s' does not exist, creating one for you ;)\n", 91 | full_path.c_str() 92 | ); 93 | return createDefault(); 94 | } 95 | return true; 96 | } 97 | 98 | fprintf( 99 | stderr, 100 | "Unable to find 'HOME' and/or '%s' folders.\n", 101 | XDG_CONFIG 102 | ); 103 | return false; 104 | } 105 | 106 | bool Config::getConfigFile(std::optional& conf) 107 | { 108 | if (!conf.has_value()) 109 | { 110 | if (getDefaultConfigFile()) return true; 111 | return false; 112 | } 113 | 114 | std::error_code ec{}; 115 | const auto config_file{conf.value()}; 116 | if (!fs::exists(config_file, ec)) 117 | { 118 | fprintf(stderr, "Unable to find config file '%s'.\n", config_file.c_str()); 119 | return false; 120 | } 121 | 122 | full_path = fs::absolute(config_file, ec); 123 | return true; 124 | } 125 | 126 | void Config::init(std::optional& conf) 127 | { 128 | if (!getConfigFile(conf)) exit(EXIT_FAILURE); 129 | if (!readConfig()) exit(EXIT_FAILURE); 130 | } 131 | 132 | bool Config::readConfig() 133 | { 134 | FILE *f = fopen(full_path.c_str(), "rb"); 135 | if (f == nullptr) { 136 | fprintf(stderr, "Unable to open config file '%s'.\n", full_path.c_str()); 137 | return false; 138 | } 139 | 140 | while (feof(f) == 0) { 141 | char line[MAX_LINE] = {0}; 142 | bool instring = false; 143 | std::string key; 144 | std::string val; 145 | std::string *current = &key; 146 | 147 | if (fgets(&line[0], MAX_LINE, f) == nullptr) { 148 | break; 149 | } 150 | 151 | char *tmp = &line[0]; 152 | 153 | while (*tmp != '\0' && *tmp != '\n' && *tmp != '\r') { 154 | if (*tmp == '=') { 155 | current = &val; 156 | } else if (*tmp == '#') { 157 | break; 158 | } else if (*tmp == '"') { 159 | instring = !instring; 160 | } else if (*tmp != ' ' || instring) { 161 | current->append(tmp, 1); 162 | } 163 | 164 | tmp++; 165 | } 166 | 167 | if (key.length() > 0) { 168 | config[key] = val; 169 | } 170 | } 171 | 172 | fclose(f); 173 | return true; 174 | } 175 | 176 | std::string Config::getString(const char *key, const std::string &def) 177 | { 178 | auto conf = config.find(key); 179 | 180 | if (conf == config.end()) { 181 | config[key] = def; 182 | } 183 | 184 | return config[key]; 185 | } 186 | 187 | int Config::getInt(const char *key, int def) 188 | { 189 | int ret = atoi(getString(key, std::to_string(def)).c_str()); 190 | return ret; 191 | } 192 | 193 | bool Config::getBool(const char *key, bool def) 194 | { 195 | std::string ret = getString(key, (def) ? "true" : "false"); 196 | return (ret == "1" || ret == "yes" || ret == "true"); 197 | } 198 | 199 | bool Config::keyExists(const char *key) const 200 | { 201 | auto conf = config.find(key); 202 | return !(conf == config.end()); 203 | } 204 | 205 | bool Config::keyEmpty(const char *key) 206 | { 207 | if (keyExists(key)) { 208 | return (config[key].empty()); 209 | } 210 | 211 | return true; 212 | } 213 | 214 | bool Config::createDefault() const 215 | { 216 | // light ░ \u2593 217 | // medium ▒ \u2592 218 | // dark shade ▓ \u2593 219 | // block █ \u2588 220 | // lower ▁ \u2581 221 | // higher ▔ \u2594 222 | // triangle ▲ \u25b2 223 | // https://en.wikipedia.org/wiki/Block_Elements 224 | 225 | FILE *f = fopen(full_path.c_str(), "w"); 226 | if (f == nullptr) { 227 | fprintf(stderr, "Unable to create default config!\n"); 228 | return false; 229 | } 230 | 231 | fprintf( 232 | f, 233 | "\"theme\" = \"default\"\n" 234 | "\n" 235 | "# Default theme {\n" 236 | " \"theme.default.static_bar\" = false\n" 237 | " \"theme.default.default_indicator\" = \"♦ \"\n" 238 | " \"theme.default.bar_style.bg\" = \"░\"\n" 239 | " \"theme.default.bar_style.fg\" = \"█\"\n" 240 | " \"theme.default.bar_style.indicator\" = \"█\"\n" 241 | " \"theme.default.bar_style.top\" = \"▁\"\n" 242 | " \"theme.default.bar_style.bottom\" = \"▔\"\n" 243 | " \"theme.default.bar_low.front\" = 2\n" 244 | " \"theme.default.bar_low.back\" = 0\n" 245 | " \"theme.default.bar_mid.front\" = 3\n" 246 | " \"theme.default.bar_mid.back\" = 0\n" 247 | " \"theme.default.bar_high.front\" = 1\n" 248 | " \"theme.default.bar_high.back\" = 0\n" 249 | " \"theme.default.volume_low\" = 2\n" 250 | " \"theme.default.volume_mid\" = 3\n" 251 | " \"theme.default.volume_high\" = 1\n" 252 | " \"theme.default.volume_peak\" = 1\n" 253 | " \"theme.default.volume_indicator\" = -1\n" 254 | " \"theme.default.selected\" = 2\n" 255 | " \"theme.default.default\" = -1\n" 256 | " \"theme.default.border\" = -1\n" 257 | " \"theme.default.dropdown.selected_text\" = 0\n" 258 | " \"theme.default.dropdown.selected\" = 2\n" 259 | " \"theme.default.dropdown.unselected\" = -1\n" 260 | "# }\n" 261 | "# c0r73x theme {\n" 262 | " \"theme.c0r73x.static_bar\" = false\n" 263 | " \"theme.c0r73x.default_indicator\" = \"■ \"\n" 264 | " \"theme.c0r73x.bar_style.bg\" = \"■\"\n" 265 | " \"theme.c0r73x.bar_style.fg\" = \"■\"\n" 266 | " \"theme.c0r73x.bar_style.indicator\" = \"■\"\n" 267 | " \"theme.c0r73x.bar_style.top\" = \"\" \n" 268 | " \"theme.c0r73x.bar_style.bottom\" = \"\" \n" 269 | " \"theme.c0r73x.bar_low.front\" = 0\n" 270 | " \"theme.c0r73x.bar_low.back\" = -1\n" 271 | " \"theme.c0r73x.bar_mid.front\" = 0\n" 272 | " \"theme.c0r73x.bar_mid.back\" = -1\n" 273 | " \"theme.c0r73x.bar_high.front\" = 0\n" 274 | " \"theme.c0r73x.bar_high.back\" = -1\n" 275 | " \"theme.c0r73x.volume_low\" = 6\n" 276 | " \"theme.c0r73x.volume_mid\" = 6\n" 277 | " \"theme.c0r73x.volume_high\" = 6\n" 278 | " \"theme.c0r73x.volume_peak\" = 1\n" 279 | " \"theme.c0r73x.volume_indicator\" = 15\n" 280 | " \"theme.c0r73x.selected\" = 6\n" 281 | " \"theme.c0r73x.default\" = -1\n" 282 | " \"theme.c0r73x.border\" = -1\n" 283 | " \"theme.c0r73x.dropdown.selected_text\" = 0\n" 284 | " \"theme.c0r73x.dropdown.selected\" = 6\n" 285 | " \"theme.c0r73x.dropdown.unselected\" = -1\n" 286 | "# }\n" 287 | "# Keybinds {\n" 288 | " \"keycode.9\" = \"switch\" # tab\n" 289 | " \"keycode.13\" = \"select\" # enter\n" 290 | " \"keycode.27\" = \"quit\" # escape\n" 291 | " \"keycode.99\" = \"dropdown\" # c\n" 292 | " \"keycode.113\" = \"quit\" # q\n" 293 | " \"keycode.109\" = \"mute\" # m\n" 294 | " \"keycode.100\" = \"set_default\" # d\n" 295 | " \"keycode.108\" = \"volume_up\" # l\n" 296 | " \"keycode.104\" = \"volume_down\" # h\n" 297 | " \"keycode.261\" = \"volume_up\" # arrow right\n" 298 | " \"keycode.260\" = \"volume_down\" # arrow left\n" 299 | " \"keycode.107\" = \"move_up\" # k\n" 300 | " \"keycode.106\" = \"move_down\" # j\n" 301 | " \"keycode.259\" = \"move_up\" # arrow up\n" 302 | " \"keycode.258\" = \"move_down\" # arrow down\n" 303 | " \"keycode.338\" = \"page_up\" # page up\n" 304 | " \"keycode.339\" = \"page_down\" # page down\n" 305 | " \"keycode.76\" = \"tab_next\" # L\n" 306 | " \"keycode.72\" = \"tab_prev\" # H\n" 307 | " \"keycode.265\" = \"tab_playback\" # f1\n" 308 | " \"keycode.266\" = \"tab_recording\" # f2\n" 309 | " \"keycode.267\" = \"tab_output\" # f3\n" 310 | " \"keycode.268\" = \"tab_input\" # f4\n" 311 | " \"keycode.269\" = \"tab_config\" # f5\n" 312 | " \"keycode.f.80\" = \"tab_playback\" # f1 VT100\n" 313 | " \"keycode.f.81\" = \"tab_recording\" # f2 VT100\n" 314 | " \"keycode.f.82\" = \"tab_output\" # f3 VT100\n" 315 | " \"keycode.f.83\" = \"tab_input\" # f4 VT100\n" 316 | " \"keycode.f.84\" = \"tab_config\" # f5 VT100\n" 317 | " \"keycode.71\" = \"move_last\" # G\n" 318 | " \"keycode.103\" = \"move_first\" # g\n" 319 | "# \"keycode.48\" = \"set_volume_100\" # 0\n" 320 | " \"keycode.48\" = \"set_volume_0\" # 0\n" 321 | " \"keycode.49\" = \"set_volume_10\" # 1\n" 322 | " \"keycode.50\" = \"set_volume_20\" # 2\n" 323 | " \"keycode.51\" = \"set_volume_30\" # 3\n" 324 | " \"keycode.52\" = \"set_volume_40\" # 4\n" 325 | " \"keycode.53\" = \"set_volume_50\" # 5\n" 326 | " \"keycode.54\" = \"set_volume_60\" # 6\n" 327 | " \"keycode.55\" = \"set_volume_70\" # 7\n" 328 | " \"keycode.56\" = \"set_volume_80\" # 8\n" 329 | " \"keycode.57\" = \"set_volume_90\" # 9\n" 330 | " \"keycode.63\" = \"help\" # ?\n" 331 | "# }" 332 | ); 333 | 334 | fclose(f); 335 | return true; 336 | } 337 | -------------------------------------------------------------------------------- /src/config.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_HPP_ 2 | #define CONFIG_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace fs = std::filesystem; 10 | 11 | using config_map = std::map; 12 | 13 | class Config 14 | { 15 | public: 16 | Config() = default; 17 | ~Config() = default; 18 | 19 | void init(std::optional& conf); 20 | 21 | std::string getString(const char *key, const std::string &def); 22 | int getInt(const char *key, int def); 23 | bool getBool(const char *key, bool def); 24 | bool keyExists(const char *key) const; 25 | bool keyEmpty(const char *key); 26 | const config_map getConfig() const; 27 | const config_map getKeycodeNameEvents() const; 28 | static std::string getKeycodeName(const char *keycode); 29 | private: 30 | static constexpr char KEY[] = {"keycode"}; 31 | static constexpr auto KEY_SIZE{sizeof (KEY) - 1}; 32 | static constexpr char XDG_CONFIG[] = {"XDG_CONFIG_HOME"}; 33 | static constexpr char FILENAME[] = {"ncpamixer.conf"}; 34 | 35 | config_map config; 36 | fs::path full_path{}; 37 | 38 | static const char *getHomeDir(); 39 | bool createDefault() const; 40 | bool readConfig(); 41 | bool getDefaultConfigFile(); 42 | bool getConfigFile(std::optional& conf); 43 | }; 44 | 45 | extern Config config; 46 | 47 | #endif // CONFIG_HPP_ 48 | -------------------------------------------------------------------------------- /src/man/ncpamixer.1.md: -------------------------------------------------------------------------------- 1 | % NCPAMIXER(1) Version 1.3.5 | ncurses PulseAudio Mixer 2 | 3 | NAME 4 | ==== 5 | 6 | **ncpamixer** — An ncurses mixer for PulseAudio inspired by pavucontrol. 7 | 8 | SYNOPSIS 9 | ======== 10 | 11 | `ncpamixer` [-v] [-h] [-c CONFIG_FILE] [-t TAB] 12 | 13 | 14 | DESCRIPTION 15 | =========== 16 | 17 | An ncurses mixer for PulseAudio inspired by pavucontrol. 18 | 19 | Options 20 | ------- 21 | 22 | `-v, --version` 23 | : Print version info. 24 | 25 | `-h, --help` 26 | : Print this help screen. 27 | 28 | `-c, --config=CONFIG_FILE` 29 | : Set custom location for config. 30 | 31 | `-t, --tab=TAB` 32 | : Open on given tab. Choices: (p)layback (default), (r)ecording, (o)utput, (i)nput, (c)onfiguration. 33 | 34 | CONFIG 35 | ===== 36 | 37 | Your configuration gets created on first run. If `$XDG_CONFIG_HOME` is defined then it will be created at `$XDG_CONFIG_HOME/ncpamixer.conf` otherwise `$HOME/.ncpamixer.conf` 38 | 39 | Default bindings 40 | ---------------- 41 | 42 | | Event | Default | Description | 43 | | --- | --- |---| 44 | | switch | tab | Cycle trought sinks, outputs, profile and ports | 45 | | select | enter | Select option in dropdowns | 46 | | quit | escape | Quit | 47 | | quit | q | Quit | 48 | | dropdown | c | Open dropdown for selecting sinks, outputs, profile and ports | 49 | | mute | m | Mute selected item | 50 | | set_default | d | Set default sink/source | 51 | | volume_up | l | Increase volume on selected item | 52 | | volume_down | h | Decrease volume on selected item | 53 | | volume_up | arrow right | Increase volume on selected item | 54 | | volume_down | arrow left | Decrease volume on selected item | 55 | | move_up | k | Move up | 56 | | move_down | j | Move down | 57 | | move_up | arrow up | Move up | 58 | | move_down | arrow down | Move down | 59 | | page_up | page up | Previous page in dropdown | 60 | | page_down | page down | Next page in dropdown | 61 | | tab_next | L | Next tab | 62 | | tab_prev | H | Previous tab | 63 | | tab_playback | F1 | Jump to playback tab| 64 | | tab_recording | F2 | Jump to recording tab | 65 | | tab_output | F3 | Jump to output tab | 66 | | tab_input | F4 | Jump to input tab | 67 | | tab_config | F5 | Jump to configuration tab | 68 | | move_last | G | Move to last item | 69 | | move_first | g | Move to first item | 70 | | set_volume_0 | 0 | Set volume to 0% | 71 | | set_volume_10 | 1 | Set volume to 10% | 72 | | set_volume_20 | 2 | Set volume to 20% | 73 | | set_volume_30 | 3 | Set volume to 30% | 74 | | set_volume_40 | 4 | Set volume to 40% | 75 | | set_volume_50 | 5 | Set volume to 50% | 76 | | set_volume_60 | 6 | Set volume to 60% | 77 | | set_volume_70 | 7 | Set volume to 70% | 78 | | set_volume_80 | 8 | Set volume to 80% | 79 | | set_volume_90 | 9 | Set volume to 90% | 80 | | help | ? | Pop up help screen with keycodes | 81 | | set_volume_100 | Unbound | Set volume to 100% | 82 | | toggle_static | Unbound | Toggle barmode static/none static | 83 | 84 | BUGS 85 | ==== 86 | 87 | See GitHub Issues: 88 | 89 | AUTHOR 90 | ====== 91 | 92 | and contributors 93 | 94 | -------------------------------------------------------------------------------- /src/pa.cpp: -------------------------------------------------------------------------------- 1 | #include "pa.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // https://freedesktop.org/software/pulseaudio/doxygen/introspect_8h.html 12 | 13 | Pa pulse; 14 | 15 | Pa::Pa() 16 | { 17 | notify_update_cb = nullptr; 18 | pa_ml = nullptr; 19 | pa_api = nullptr; 20 | pa_init = false; 21 | pa_ctx = nullptr; 22 | reconnect_running = false; 23 | connected = false; 24 | } 25 | 26 | Pa::~Pa() 27 | { 28 | 29 | clearAllPaObjects(); 30 | 31 | if (pa_init) { 32 | exitPa(); 33 | } 34 | } 35 | 36 | 37 | void Pa::clearAllPaObjects() 38 | { 39 | deletePaobjects(&PA_SOURCE_OUTPUTS); 40 | deletePaobjects(&PA_INPUTS); 41 | deletePaobjects(&PA_SOURCES); 42 | deletePaobjects(&PA_SINKS); 43 | deletePaobjects(&PA_CARDS); 44 | } 45 | 46 | 47 | void Pa::do_reconnect(Pa *pa) 48 | { 49 | if (pa->reconnect_running || pa->connected) { 50 | return; 51 | } 52 | 53 | pa->reconnect_running = true; 54 | pa->clearAllPaObjects(); 55 | 56 | while (!pa->connected) { 57 | pa->pa_connect(); 58 | usleep(500000); 59 | } 60 | 61 | pa->reconnect_running = false; 62 | } 63 | 64 | void Pa::reconnect() 65 | { 66 | 67 | if (!reconnect_running && !connected) { 68 | std::thread rThread(do_reconnect, this); 69 | rThread.detach(); 70 | } 71 | } 72 | 73 | 74 | bool Pa::pa_connect() 75 | { 76 | if (pa_ctx) { 77 | return false; 78 | } 79 | 80 | pa_threaded_mainloop_lock(pa_ml); 81 | 82 | pa_proplist *proplist = pa_proplist_new(); 83 | pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, "ncpamixer"); 84 | pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, "ncpamixer"); 85 | pa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, "audio-card"); 86 | pa_ctx = pa_context_new_with_proplist(pa_api, NULL, proplist); 87 | pa_proplist_free(proplist); 88 | 89 | if (!pa_ctx) { 90 | fprintf(stderr, "Unable to create PA context.\n"); 91 | pa_threaded_mainloop_unlock(pa_ml); 92 | pa_ctx = nullptr; 93 | exit(EXIT_FAILURE); 94 | return false; 95 | } 96 | 97 | pa_context_set_state_callback(pa_ctx, &Pa::ctx_state_cb, this); 98 | pa_context_set_subscribe_callback(pa_ctx, &Pa::subscribe_cb, this); 99 | 100 | if (pa_context_connect(pa_ctx, NULL, PA_CONTEXT_NOFAIL, NULL) < 0) { 101 | pa_context_disconnect(pa_ctx); 102 | pa_context_unref(pa_ctx); // Tror ej den behövs? 103 | pa_threaded_mainloop_unlock(pa_ml); 104 | pa_ctx = nullptr; 105 | 106 | connectionMtx.unlock(); 107 | reconnect(); 108 | return false; 109 | } 110 | 111 | pa_threaded_mainloop_unlock(pa_ml); 112 | 113 | connectionMtx.unlock(); 114 | return true; 115 | 116 | } 117 | 118 | bool Pa::init() 119 | { 120 | pa_init = true; 121 | 122 | pa_ml = pa_threaded_mainloop_new(); 123 | 124 | if (!pa_ml) { 125 | fprintf(stderr, "Unable to create PA main loop.\n"); 126 | exit(EXIT_FAILURE); 127 | return false; 128 | } 129 | 130 | pa_api = pa_threaded_mainloop_get_api(pa_ml); 131 | 132 | if (pa_threaded_mainloop_start(pa_ml) < 0) { 133 | fprintf(stderr, "Unable to start PA mainloop.\n"); 134 | pa_context_disconnect(pa_ctx); 135 | pa_context_unref(pa_ctx); 136 | pa_threaded_mainloop_unlock(pa_ml); 137 | pa_ctx = nullptr; 138 | exit(EXIT_FAILURE); 139 | return false; 140 | } 141 | 142 | return pa_connect(); 143 | } 144 | 145 | void Pa::ctx_success_cb(pa_context *ctx, int success, void *instance) 146 | { 147 | Pa *p = reinterpret_cast(instance); 148 | pa_threaded_mainloop_signal(p->pa_ml, 0); 149 | } 150 | 151 | void Pa::fetchPaobjects() 152 | { 153 | 154 | //pa_threaded_mainloop_lock(pa_ml); 155 | pa_operation *o; 156 | 157 | // get cards 158 | o = pa_context_get_card_info_list(pa_ctx, &Pa::ctx_cardlist_cb, this); 159 | wait_on_pa_operation(o); 160 | pa_operation_unref(o); 161 | 162 | // source list cb 163 | o = pa_context_get_source_info_list(pa_ctx, &Pa::ctx_sourcelist_cb, this); 164 | wait_on_pa_operation(o); 165 | pa_operation_unref(o); 166 | 167 | // Sink devices list cb 168 | o = pa_context_get_sink_info_list(pa_ctx, &Pa::ctx_sinklist_cb, this); 169 | wait_on_pa_operation(o); 170 | pa_operation_unref(o); 171 | 172 | // Sink input list (application) cb 173 | o = pa_context_get_sink_input_info_list(pa_ctx, &Pa::ctx_inputlist_cb, this); 174 | wait_on_pa_operation(o); 175 | pa_operation_unref(o); 176 | 177 | // source outputs list cb 178 | o = pa_context_get_source_output_info_list( 179 | pa_ctx, 180 | &Pa::ctx_sourceoutputlist_cb, 181 | this 182 | ); 183 | wait_on_pa_operation(o); 184 | pa_operation_unref(o); 185 | 186 | // Get server info 187 | o = pa_context_get_server_info(pa_ctx, &Pa::ctx_serverinfo_cb, this); 188 | wait_on_pa_operation(o); 189 | pa_operation_unref(o); 190 | 191 | //pa_threaded_mainloop_unlock(pa_ml); 192 | } 193 | 194 | void Pa::deletePaobjects(std::map *objects) 195 | { 196 | std::lock_guard lk(inputMtx); 197 | 198 | for (auto i = objects->begin(); i != objects->end(); ) { 199 | delete i->second; 200 | i = objects->erase(i); 201 | } 202 | } 203 | 204 | void Pa::remove_paobject(std::map *objects, 205 | uint32_t index) 206 | { 207 | std::lock_guard lk(inputMtx); 208 | 209 | for (auto i = objects->begin(); i != objects->end(); i++) { 210 | if (i->first == index) { 211 | delete i->second; 212 | i = objects->erase(i); 213 | return; 214 | } 215 | } 216 | } 217 | 218 | void Pa::exitPa() 219 | { 220 | if (pa_ctx) { 221 | pa_context_disconnect(pa_ctx); 222 | pa_threaded_mainloop_stop(pa_ml); 223 | pa_threaded_mainloop_free(pa_ml); 224 | } 225 | } 226 | 227 | uint32_t Pa::exists(std::map objects, uint32_t index) 228 | { 229 | if (objects.empty()) { 230 | return -1; 231 | } 232 | 233 | if ((objects.find(index) == objects.end())) { 234 | index = objects.begin()->first; 235 | } 236 | 237 | return index; 238 | } 239 | 240 | void Pa::set_defaults(const pa_server_info *info) 241 | { 242 | std::lock_guard lk(inputMtx); 243 | 244 | for (auto &s : PA_SINKS) { 245 | if (!strcmp(s.second->pa_name, info->default_sink_name)) { 246 | s.second->is_default = true; 247 | } else { 248 | s.second->is_default = false; 249 | } 250 | } 251 | 252 | for (auto &s : PA_SOURCES) { 253 | if (!strcmp(s.second->pa_name, info->default_source_name)) { 254 | s.second->is_default = true; 255 | } else { 256 | s.second->is_default = false; 257 | } 258 | } 259 | } 260 | 261 | void Pa::update_source_output(const pa_source_output_info *info) 262 | { 263 | std::lock_guard lk(inputMtx); 264 | 265 | // https://github.com/pulseaudio/pavucontrol/blob/master/src/mainwindow.cc#L802 266 | const char *app; 267 | 268 | if ((app = pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_ID))) { 269 | if (strcmp(app, "org.PulseAudio.pavucontrol") == 0 270 | || strcmp(app, "org.gnome.VolumeControl") == 0 271 | || strcmp(app, "org.kde.kmixd") == 0 272 | || strcmp(app, "ncpamixer") == 0) { 273 | return; 274 | } 275 | } 276 | 277 | PaSourceOutput *p; 278 | 279 | if (PA_SOURCE_OUTPUTS.count(info->index) == 0) { 280 | p = new PaSourceOutput; 281 | PA_SOURCE_OUTPUTS[info->index] = p; 282 | p->monitor_stream = nullptr; 283 | } else { 284 | p = reinterpret_cast(PA_SOURCE_OUTPUTS[info->index]); 285 | } 286 | 287 | p->index = info->index; 288 | p->source = info->source; 289 | p->channels = info->channel_map.channels; 290 | p->monitor_index = info->source; 291 | p->volume = (const pa_volume_t) pa_cvolume_avg(&info->volume); 292 | p->mute = info->mute; 293 | 294 | strncpy(p->name, info->name, sizeof(p->name) - 1); 295 | 296 | const auto app_name = pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_NAME); 297 | p->app_name = (app_name != NULL) ? app_name : "Module"; 298 | 299 | notify_update(); 300 | } 301 | 302 | void Pa::update_source(const pa_source_info *info) 303 | { 304 | std::lock_guard lk(inputMtx); 305 | 306 | PaSource *p; 307 | 308 | bool newObj = false; 309 | 310 | if (PA_SOURCES.count(info->index) == 0) { 311 | p = new PaSource; 312 | PA_SOURCES[info->index] = p; 313 | p->monitor_index = info->index; 314 | p->monitor_stream = nullptr; 315 | newObj = true; 316 | } else { 317 | p = reinterpret_cast(PA_SOURCES[info->index]); 318 | } 319 | 320 | p->index = info->index; 321 | p->channels = info->channel_map.channels; 322 | p->volume = (const pa_volume_t) pa_cvolume_avg(&info->volume); 323 | p->mute = info->mute; 324 | 325 | strncpy(p->name, info->description, sizeof(p->name) - 1); 326 | strncpy(p->pa_name, info->name, sizeof(p->pa_name) - 1); 327 | 328 | if (newObj) { 329 | create_monitor_stream_for_paobject(p); 330 | } 331 | 332 | p->updatePorts(info->ports, info->n_ports); 333 | 334 | if (info->active_port != nullptr) { 335 | for (uint32_t i = 0; i < p->attributes.size(); i++) { 336 | if (strcmp(p->attributes[i]->name, info->active_port->name) == 0) { 337 | p->active_attribute = p->attributes[i]; 338 | break; 339 | } 340 | } 341 | } 342 | 343 | notify_update(); 344 | } 345 | 346 | void Pa::update_card(const pa_card_info *info) 347 | { 348 | std::lock_guard lk(inputMtx); 349 | PaCard *p; 350 | 351 | if (PA_CARDS.count(info->index) == 0) { 352 | p = new PaCard; 353 | PA_CARDS[info->index] = p; 354 | p->monitor_index = info->index; 355 | p->monitor_stream = nullptr; 356 | } else { 357 | p = reinterpret_cast(PA_CARDS[info->index]); 358 | } 359 | 360 | p->index = info->index; 361 | p->channels = 0; 362 | p->volume = 0; 363 | p->mute = false; 364 | p->updateProfiles(info->profiles, info->n_profiles); 365 | p->active_attribute = nullptr; 366 | 367 | if (info->active_profile != nullptr) { 368 | for (uint32_t i = 0; i < p->attributes.size(); i++) { 369 | if (strcmp(p->attributes[i]->name, info->active_profile->name) == 0) { 370 | p->active_attribute = p->attributes[i]; 371 | break; 372 | } 373 | } 374 | } 375 | 376 | const char *description; 377 | description = pa_proplist_gets(info->proplist, PA_PROP_DEVICE_DESCRIPTION); 378 | 379 | if (description) { 380 | snprintf(p->name, sizeof(p->name), "%s", description); 381 | } else { 382 | snprintf(p->name, sizeof(p->name), "%s", info->name); 383 | } 384 | 385 | notify_update(); 386 | } 387 | 388 | void Pa::update_sink(const pa_sink_info *info) 389 | { 390 | std::lock_guard lk(inputMtx); 391 | 392 | PaSink *p; 393 | 394 | if (PA_SINKS.count(info->index) == 0) { 395 | p = new PaSink; 396 | PA_SINKS[info->index] = p; 397 | p->monitor_stream = nullptr; 398 | } else { 399 | p = reinterpret_cast(PA_SINKS[info->index]); 400 | } 401 | 402 | p->index = info->index; 403 | p->channels = info->channel_map.channels; 404 | p->monitor_index = info->monitor_source; 405 | p->volume = (const pa_volume_t) pa_cvolume_avg(&info->volume); 406 | p->mute = info->mute; 407 | 408 | strncpy(p->name, info->description, sizeof(p->name) - 1); 409 | strncpy(p->pa_name, info->name, sizeof(p->pa_name) - 1); 410 | 411 | p->updatePorts(info->ports, info->n_ports); 412 | 413 | if (info->active_port != nullptr) { 414 | for (uint32_t i = 0; i < p->attributes.size(); i++) { 415 | if (strcmp(p->attributes[i]->name, info->active_port->name) == 0) { 416 | p->active_attribute = p->attributes[i]; 417 | break; 418 | } 419 | } 420 | } 421 | 422 | notify_update(); 423 | } 424 | 425 | void Pa::update_input(const pa_sink_input_info *info) 426 | { 427 | 428 | std::lock_guard lk(inputMtx); 429 | 430 | PaInput *p; 431 | 432 | bool sink_changed = true; 433 | 434 | if (PA_INPUTS.count(info->index) == 0) { 435 | p = new PaInput; 436 | PA_INPUTS[info->index] = p; 437 | p->monitor_stream = nullptr; 438 | } else { 439 | p = reinterpret_cast(PA_INPUTS[info->index]); 440 | 441 | if (PA_INPUTS.count(info->index)) { 442 | sink_changed = info->sink != p->sink; 443 | } 444 | } 445 | 446 | 447 | p->index = info->index; 448 | p->channels = info->channel_map.channels; 449 | p->volume = (const pa_volume_t) pa_cvolume_avg(&info->volume); 450 | p->mute = info->mute; 451 | p->sink = info->sink; 452 | snprintf(p->name, sizeof(p->name), "%s", info->name); 453 | 454 | const auto app_name = pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_NAME); 455 | p->app_name = (app_name != NULL) ? app_name : "Module"; 456 | 457 | if (sink_changed) { 458 | create_monitor_stream_for_paobject(p); 459 | } 460 | 461 | notify_update(); 462 | } 463 | 464 | // https://github.com/pulseaudio/pavucontrol/blob/master/src/mainwindow.cc#L541 465 | void Pa::read_callback(pa_stream *s, size_t length, void *instance) 466 | { 467 | Pa *pa = reinterpret_cast(instance); 468 | std::lock_guard lk(pa->inputMtx); 469 | const void *data; 470 | float v; 471 | 472 | if (pa_stream_peek(s, &data, &length) < 0) { 473 | return; 474 | } 475 | 476 | if (!data) { 477 | /* NULL data means either a hole or empty buffer. 478 | Only drop the stream when there is a hole (length > 0) */ 479 | if (length) { 480 | pa_stream_drop(s); 481 | } 482 | 483 | return; 484 | } 485 | 486 | assert(length > 0); 487 | assert(length % sizeof(float) == 0); 488 | 489 | v = ((const float *) data)[length / sizeof(float) - 1]; 490 | 491 | pa_stream_drop(s); 492 | 493 | if (v < 0) { 494 | v = 0; 495 | } 496 | 497 | if (v > 1) { 498 | v = 1; 499 | } 500 | 501 | uint32_t input_idx = pa_stream_get_monitor_stream(s); 502 | 503 | if (input_idx != PA_INVALID_INDEX && pa->PA_INPUTS.find(input_idx) != pa->PA_INPUTS.end()) { 504 | pa->PA_INPUTS[input_idx]->peak = v; 505 | } else { 506 | pa->updatePeakByDeviceId(pa_stream_get_device_index(s), v); 507 | } 508 | 509 | pa->notify_update(); 510 | } 511 | 512 | void Pa::updatePeakByDeviceId(uint32_t index, float peak) 513 | { 514 | for (auto &s : PA_SINKS) { 515 | if (s.second->monitor_index == index) { 516 | s.second->peak = peak; 517 | } 518 | } 519 | 520 | for (auto &s : PA_SOURCES) { 521 | if (s.second->monitor_index == index) { 522 | s.second->peak = peak; 523 | } 524 | } 525 | 526 | for (auto &s : PA_SOURCE_OUTPUTS) { 527 | if (s.second->monitor_index == index) { 528 | s.second->peak = peak; 529 | } 530 | } 531 | } 532 | 533 | // https://github.com/pulseaudio/pavucontrol/blob/master/src/mainwindow.cc#L574 534 | pa_stream *Pa::create_monitor_stream_for_source(uint32_t source_index, 535 | uint32_t stream_index = -1) 536 | { 537 | pa_stream *s; 538 | char t[16]; 539 | pa_buffer_attr attr; 540 | pa_sample_spec ss; 541 | pa_stream_flags_t flags; 542 | 543 | ss.channels = 1; 544 | ss.format = PA_SAMPLE_FLOAT32; 545 | ss.rate = 25; 546 | 547 | memset(&attr, 0, sizeof(attr)); 548 | attr.fragsize = sizeof(float); 549 | attr.maxlength = (uint32_t) - 1; 550 | 551 | snprintf(t, sizeof(t), "%u", source_index); 552 | 553 | if (!(s = pa_stream_new(pa_ctx, "Peak detect", &ss, NULL))) { 554 | return NULL; 555 | } 556 | 557 | if (stream_index != (uint32_t) - 1) { 558 | pa_stream_set_monitor_stream(s, stream_index); 559 | } 560 | 561 | pa_stream_set_read_callback(s, &Pa::read_callback, this); 562 | 563 | flags = (pa_stream_flags_t)(PA_STREAM_DONT_MOVE | PA_STREAM_PEAK_DETECT | 564 | PA_STREAM_ADJUST_LATENCY); 565 | 566 | if (pa_stream_connect_record(s, t, &attr, flags) < 0) { 567 | pa_stream_unref(s); 568 | return NULL; 569 | } 570 | 571 | return s; 572 | } 573 | 574 | void Pa::create_monitor_stream_for_paobject(PaObject *po) 575 | { 576 | if (po->monitor_stream != nullptr) { 577 | pa_stream_disconnect(po->monitor_stream); 578 | po->monitor_stream = nullptr; 579 | } 580 | 581 | po->monitor_stream = create_monitor_stream_for_source 582 | (po->monitor_index, po->type == pa_object_t::INPUT ? po->index : -1); 583 | } 584 | 585 | void Pa::set_notify_update_cb(const notify_update_callback &cb) 586 | { 587 | notify_update_cb = cb; 588 | } 589 | 590 | void Pa::notify_update() 591 | { 592 | if (notify_update_cb != nullptr) { 593 | notify_update_cb(); 594 | } 595 | } 596 | 597 | void Pa::ctx_cardlist_cb(pa_context *ctx, const pa_card_info *info, 598 | int eol, 599 | void *instance) 600 | { 601 | Pa *pa = reinterpret_cast(instance); 602 | 603 | if (!eol) { 604 | pa->update_card(info); 605 | } 606 | 607 | return; 608 | } 609 | 610 | 611 | 612 | 613 | void Pa::ctx_sourcelist_cb(pa_context *ctx, const pa_source_info *info, 614 | int eol, 615 | void *instance) 616 | { 617 | Pa *pa = reinterpret_cast(instance); 618 | 619 | if (!eol) { 620 | pa->update_source(info); 621 | } 622 | } 623 | 624 | void Pa::ctx_sourceoutputlist_cb(pa_context *ctx, 625 | const pa_source_output_info *info, 626 | int eol, 627 | void *instance) 628 | { 629 | Pa *pa = reinterpret_cast(instance); 630 | 631 | if (!eol) { 632 | pa->update_source_output(info); 633 | } 634 | } 635 | 636 | void Pa::ctx_inputlist_cb(pa_context *ctx, const pa_sink_input_info *info, 637 | int eol, void *instance) 638 | { 639 | Pa *pa = reinterpret_cast(instance); 640 | 641 | if (!eol) { 642 | pa->update_input(info); 643 | } 644 | } 645 | 646 | void Pa::ctx_sinklist_cb(pa_context *ctx, const pa_sink_info *info, int eol, 647 | void *instance) 648 | { 649 | Pa *pa = reinterpret_cast(instance); 650 | 651 | if (!eol) { 652 | pa->update_sink(info); 653 | } 654 | } 655 | 656 | void Pa::ctx_serverinfo_cb(pa_context *ctx, const pa_server_info *info, 657 | void *instance) 658 | { 659 | Pa *pa = reinterpret_cast(instance); 660 | 661 | pa->set_defaults(info); 662 | } 663 | 664 | void Pa::subscribe_cb(pa_context *ctx, pa_subscription_event_type_t t, 665 | uint32_t index, void *instance) 666 | { 667 | 668 | int type = (t & PA_SUBSCRIPTION_EVENT_TYPE_MASK); 669 | Pa *pa = reinterpret_cast(instance); 670 | // https://freedesktop.org/software/pulseaudio/doxygen/def_8h.html#a6bedfa147a9565383f1f44642cfef6a3 671 | 672 | switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { 673 | case PA_SUBSCRIPTION_EVENT_SINK: { 674 | if (type == PA_SUBSCRIPTION_EVENT_REMOVE) { 675 | pa->remove_paobject(&pa->PA_SINKS, index); 676 | } else if (type == PA_SUBSCRIPTION_EVENT_NEW || 677 | type == PA_SUBSCRIPTION_EVENT_CHANGE) { 678 | 679 | pa_operation *o = pa_context_get_sink_info_by_index( 680 | ctx, 681 | index, 682 | &Pa::ctx_sinklist_cb, 683 | instance 684 | ); 685 | pa->wait_on_pa_operation(o); 686 | pa_operation_unref(o); 687 | } 688 | 689 | break; 690 | } 691 | 692 | case PA_SUBSCRIPTION_EVENT_SINK_INPUT: { 693 | 694 | if (type == PA_SUBSCRIPTION_EVENT_REMOVE) { 695 | pa->remove_paobject(&pa->PA_INPUTS, index); 696 | } else if (type == PA_SUBSCRIPTION_EVENT_NEW || 697 | type == PA_SUBSCRIPTION_EVENT_CHANGE) { 698 | 699 | pa_operation *o = pa_context_get_sink_input_info( 700 | ctx, 701 | index, 702 | &Pa::ctx_inputlist_cb, 703 | instance 704 | ); 705 | pa->wait_on_pa_operation(o); 706 | pa_operation_unref(o); 707 | } 708 | 709 | break; 710 | } 711 | 712 | case PA_SUBSCRIPTION_EVENT_SOURCE: { 713 | if (type == PA_SUBSCRIPTION_EVENT_REMOVE) { 714 | pa->remove_paobject(&pa->PA_SOURCES, index); 715 | } else if (type == PA_SUBSCRIPTION_EVENT_NEW || 716 | type == PA_SUBSCRIPTION_EVENT_CHANGE) { 717 | 718 | pa_operation *o; 719 | 720 | if (!(o = pa_context_get_source_info_by_index(ctx, index, 721 | &Pa::ctx_sourcelist_cb, instance))) { 722 | return; 723 | } 724 | 725 | pa->wait_on_pa_operation(o); 726 | pa_operation_unref(o); 727 | } 728 | 729 | break; 730 | } 731 | 732 | case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: 733 | if (type == PA_SUBSCRIPTION_EVENT_REMOVE) { 734 | pa->remove_paobject(&pa->PA_SOURCE_OUTPUTS, index); 735 | } else if (type == PA_SUBSCRIPTION_EVENT_NEW || 736 | type == PA_SUBSCRIPTION_EVENT_CHANGE) { 737 | pa_operation *o = pa_context_get_source_output_info(ctx, index, 738 | &Pa::ctx_sourceoutputlist_cb, instance); 739 | 740 | pa->wait_on_pa_operation(o); 741 | pa_operation_unref(o); 742 | } 743 | 744 | break; 745 | 746 | case PA_SUBSCRIPTION_EVENT_CARD: 747 | if (type == PA_SUBSCRIPTION_EVENT_REMOVE) { 748 | pa->remove_paobject(&pa->PA_CARDS, index); 749 | } else if (type == PA_SUBSCRIPTION_EVENT_NEW || 750 | type == PA_SUBSCRIPTION_EVENT_CHANGE) { 751 | pa_operation *o = pa_context_get_card_info_by_index(ctx, index, 752 | &Pa::ctx_cardlist_cb, instance); 753 | 754 | pa->wait_on_pa_operation(o); 755 | pa_operation_unref(o); 756 | } 757 | 758 | break; 759 | 760 | case PA_SUBSCRIPTION_EVENT_SERVER: { 761 | pa_operation *o = pa_context_get_server_info(ctx, 762 | &Pa::ctx_serverinfo_cb, instance); 763 | 764 | pa->wait_on_pa_operation(o); 765 | pa_operation_unref(o); 766 | } 767 | 768 | break; 769 | 770 | case PA_SUBSCRIPTION_EVENT_MODULE: 771 | case PA_SUBSCRIPTION_EVENT_CLIENT: 772 | case PA_SUBSCRIPTION_EVENT_SAMPLE_CACHE: 773 | default: 774 | break; 775 | 776 | } 777 | 778 | } 779 | 780 | void Pa::wait_on_pa_operation(pa_operation *o) 781 | { 782 | while (pa_operation_get_state(o) == PA_OPERATION_DONE) { 783 | pa_threaded_mainloop_wait(pa_ml); 784 | } 785 | } 786 | 787 | void Pa::ctx_state_cb(pa_context *ctx, void *instance) 788 | { 789 | int state = pa_context_get_state(ctx); 790 | Pa *pa = reinterpret_cast(instance); 791 | 792 | switch (state) { 793 | case PA_CONTEXT_READY: { 794 | pa_operation *o = pa_context_subscribe( 795 | pa->pa_ctx, 796 | (pa_subscription_mask_t) 797 | (PA_SUBSCRIPTION_MASK_SINK | 798 | PA_SUBSCRIPTION_MASK_SOURCE | 799 | PA_SUBSCRIPTION_MASK_SINK_INPUT | 800 | PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT | 801 | PA_SUBSCRIPTION_MASK_CARD | 802 | PA_SUBSCRIPTION_MASK_SERVER 803 | ), &Pa::ctx_success_cb, instance); 804 | pa->wait_on_pa_operation(o); 805 | pa_operation_unref(o); 806 | pa->fetchPaobjects(); 807 | 808 | pa->connectionMtx.lock(); 809 | pa->connected = true; 810 | pa->connectionMtx.unlock(); 811 | break; 812 | } 813 | 814 | case PA_CONTEXT_FAILED: 815 | pa->connectionMtx.lock(); 816 | pa->connected = false; 817 | pa_context_unref(pa->pa_ctx); 818 | pa->pa_ctx = nullptr; 819 | pa->connectionMtx.unlock(); 820 | pa->reconnect(); 821 | break; 822 | 823 | 824 | case PA_CONTEXT_UNCONNECTED: 825 | case PA_CONTEXT_CONNECTING: 826 | case PA_CONTEXT_AUTHORIZING: 827 | case PA_CONTEXT_SETTING_NAME: 828 | case PA_CONTEXT_TERMINATED: 829 | break; 830 | } 831 | 832 | return; 833 | } 834 | -------------------------------------------------------------------------------- /src/pa.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PA_HPP 2 | #define PA_HPP 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "pa_card.hpp" 10 | #include "pa_input.hpp" 11 | #include "pa_object.hpp" 12 | #include "pa_sink.hpp" 13 | #include "pa_source.hpp" 14 | #include "pa_source_output.hpp" 15 | 16 | struct PA_SINK { 17 | uint32_t index; 18 | char name[255]; 19 | char app_name[255]; 20 | pa_volume_t volume; 21 | bool mute; 22 | unsigned int channels; 23 | 24 | uint32_t monitor_index; 25 | pa_stream *monitor_stream; 26 | float peak; 27 | }; 28 | 29 | struct PA_INPUT : PA_SINK { 30 | uint32_t sink; 31 | }; 32 | 33 | using notify_update_callback = void(*)(); 34 | 35 | class Pa 36 | { 37 | public: 38 | Pa(); 39 | ~Pa(); 40 | 41 | bool init(); 42 | void exitPa(); 43 | void updatePeakByDeviceId(uint32_t index, float peak); 44 | void update_input(const pa_sink_input_info *info); 45 | void update_sink(const pa_sink_info *info); 46 | void update_source(const pa_source_info *info); 47 | void update_card(const pa_card_info *info); 48 | void update_source_output(const pa_source_output_info *info); 49 | void remove_paobject(std::map *objects, uint32_t index); 50 | void toggle_input_mute(uint32_t index); 51 | void toggle_sink_mute(uint32_t index); 52 | void move_input_sink(uint32_t input_index, uint32_t sink_index); 53 | void set_defaults(const pa_server_info *info); 54 | void fetchPaobjects(); 55 | void reconnect(); 56 | void static do_reconnect(Pa *pa); 57 | bool pa_connect(); 58 | void clearAllPaObjects(); 59 | 60 | bool reconnect_running; 61 | bool connected; 62 | 63 | static uint32_t exists( 64 | std::map objects, 66 | uint32_t index 67 | ); 68 | 69 | static void subscribe_cb( 70 | pa_context *ctx, 71 | pa_subscription_event_type_t t, 72 | uint32_t index, 73 | void *instance 74 | ); 75 | static void ctx_state_cb( 76 | pa_context *ctx, 77 | void *instance 78 | ); 79 | static void ctx_success_cb( 80 | pa_context *ctx, 81 | int success, 82 | void *instance 83 | ); 84 | static void ctx_sinklist_cb( 85 | pa_context *ctx, 86 | const pa_sink_info *info, 87 | int eol, 88 | void *instance 89 | ); 90 | static void ctx_inputlist_cb( 91 | pa_context *ctx, 92 | const pa_sink_input_info *info, 93 | int eol, 94 | void *instance 95 | ); 96 | static void ctx_sourcelist_cb( 97 | pa_context *ctx, 98 | const pa_source_info *info, 99 | int eol, 100 | void *instance 101 | ); 102 | static void ctx_sourceoutputlist_cb( 103 | pa_context *ctx, 104 | const pa_source_output_info *info, 105 | int eol, 106 | void *instance 107 | ); 108 | static void ctx_cardlist_cb( 109 | pa_context *ctx, 110 | const pa_card_info *info, 111 | int eol, 112 | void *instance 113 | ); 114 | static void ctx_serverinfo_cb( 115 | pa_context *ctx, 116 | const pa_server_info *info, 117 | void *instance 118 | ); 119 | 120 | static void read_callback(pa_stream *s, size_t length, void *instance); 121 | static void stream_suspended_cb(pa_stream *stream, void *instance); 122 | static void stream_state_cb(pa_stream *stream, void *info); 123 | 124 | void create_monitor_stream_for_paobject(PaObject *po); 125 | pa_stream *create_monitor_stream_for_source( 126 | uint32_t source_index, 127 | uint32_t stream_index 128 | ); 129 | 130 | void set_notify_update_cb(const notify_update_callback &cb); 131 | void notify_update(); 132 | 133 | std::map PA_INPUTS; 134 | std::map PA_SINKS; 135 | std::map PA_SOURCES; 136 | std::map PA_SOURCE_OUTPUTS; 137 | std::map PA_CARDS; 138 | 139 | void (*notify_update_cb)(); 140 | std::mutex inputMtx; 141 | std::mutex connectionMtx; 142 | 143 | pa_context *pa_ctx; 144 | pa_threaded_mainloop *pa_ml; 145 | pa_mainloop_api *pa_api; 146 | private: 147 | bool pa_init; 148 | void wait_on_pa_operation(pa_operation *o); 149 | void deletePaobjects(std::map *objects); 150 | std::mutex sinkMtx; 151 | }; 152 | 153 | extern Pa pulse; 154 | 155 | #endif 156 | -------------------------------------------------------------------------------- /src/pa_card.cpp: -------------------------------------------------------------------------------- 1 | #include "pa_card.hpp" 2 | #include 3 | 4 | PaCard::PaCard() 5 | { 6 | type = pa_object_t::CARD; 7 | monitor_stream = nullptr; 8 | pa_set_active_attribute = pa_context_set_card_profile_by_index; 9 | pa_set_volume = nullptr; 10 | pa_set_mute = nullptr; 11 | pa_move = nullptr; 12 | active_attribute = nullptr; 13 | pa_set_default = nullptr; 14 | } 15 | 16 | void PaCard::updateProfiles(pa_card_profile_info *pa_profiles, uint32_t n_profile) 17 | { 18 | clearAttributes(); 19 | 20 | for (uint32_t i = 0; i < n_profile; i++) { 21 | PaObjectAttribute *p = new PaObjectAttribute; 22 | snprintf(p->name, sizeof(p->name), "%s", pa_profiles[i].name); 23 | snprintf( 24 | p->description, 25 | sizeof(p->description), 26 | "%s", 27 | pa_profiles[i].description 28 | ); 29 | attributes.push_back(p); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/pa_card.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PA_CARD_ 2 | #define PA_CARD_ 3 | #include 4 | #include "pa_object.hpp" 5 | #include "pa_object_attribute.hpp" 6 | 7 | class PaCard : public PaObject 8 | { 9 | public: 10 | PaCard(); 11 | void updateProfiles(pa_card_profile_info *pa_profiles, uint32_t n_profile); 12 | }; 13 | 14 | #endif // PA_CARD_ 15 | -------------------------------------------------------------------------------- /src/pa_input.cpp: -------------------------------------------------------------------------------- 1 | #include "pa_input.hpp" 2 | 3 | #include 4 | 5 | PaInput::PaInput() 6 | { 7 | type = pa_object_t::INPUT; 8 | monitor_stream = nullptr; 9 | pa_set_volume = &pa_context_set_sink_input_volume; 10 | pa_set_mute = &pa_context_set_sink_input_mute; 11 | pa_move = &pa_context_move_sink_input_by_index; 12 | pa_set_active_attribute = nullptr; 13 | pa_set_default = nullptr; 14 | 15 | sink = 0; 16 | } 17 | -------------------------------------------------------------------------------- /src/pa_input.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PA_INPUT_ 2 | #define PA_INPUT_ 3 | #include "pa_object.hpp" 4 | #include 5 | 6 | class PaInput : public PaObject 7 | { 8 | public: 9 | PaInput(); 10 | uint32_t sink; 11 | std::string app_name; 12 | 13 | uint32_t getRelation() override { 14 | return sink; 15 | }; 16 | 17 | const char* getAppName() override { 18 | return app_name.c_str(); 19 | }; 20 | }; 21 | 22 | #endif // PA_INPUT_ 23 | -------------------------------------------------------------------------------- /src/pa_object.cpp: -------------------------------------------------------------------------------- 1 | #include "pa_object.hpp" 2 | 3 | #include 4 | 5 | #include "pa.hpp" 6 | 7 | PaObject::PaObject() : type(pa_object_t::SINK) 8 | { 9 | index = 0; 10 | memset(name, 0, sizeof(name)); 11 | memset(pa_name, 0, sizeof(pa_name)); 12 | channels = 0; 13 | mute = false; 14 | monitor_index = 0; 15 | monitor_stream = nullptr; 16 | peak = 0; 17 | pa_set_volume = nullptr; 18 | pa_set_mute = nullptr; 19 | pa_move = nullptr; 20 | pa_set_active_attribute = nullptr; 21 | pa_set_default = nullptr; 22 | active_attribute = nullptr; 23 | is_default = false; 24 | } 25 | 26 | PaObject::~PaObject() 27 | { 28 | clearAttributes(); 29 | } 30 | 31 | void PaObject::set_volume(float perc) 32 | { 33 | 34 | if (pa_set_volume != nullptr) { 35 | pa_threaded_mainloop_lock(pulse.pa_ml); 36 | 37 | int vol = (PA_VOLUME_NORM * perc); 38 | pa_cvolume cvol; 39 | 40 | pa_cvolume_init(&cvol); 41 | pa_cvolume_set(&cvol, channels, vol); 42 | 43 | 44 | pa_operation *o = pa_set_volume( 45 | pulse.pa_ctx, 46 | index, 47 | &cvol, 48 | NULL, 49 | NULL 50 | ); 51 | pa_operation_unref(o); 52 | pa_threaded_mainloop_unlock(pulse.pa_ml); 53 | } 54 | } 55 | 56 | void PaObject::step_volume(int dir) 57 | { 58 | if (volume < 0x400 && dir == -1) { 59 | set_volume(0); 60 | return; 61 | } 62 | 63 | set_volume(static_cast(volume + (0x400 * dir)) / PA_VOLUME_NORM); 64 | } 65 | 66 | 67 | void PaObject::toggle_mute() 68 | { 69 | if (pa_set_mute != nullptr) { 70 | pa_operation *o = pa_set_mute( 71 | pulse.pa_ctx, 72 | index, 73 | !mute, 74 | NULL, 75 | NULL 76 | ); 77 | pa_operation_unref(o); 78 | } 79 | } 80 | 81 | 82 | void PaObject::move(uint32_t dest) 83 | { 84 | if (pa_move != nullptr) { 85 | pa_operation *o = pa_move( 86 | pulse.pa_ctx, 87 | index, 88 | dest, 89 | NULL, 90 | NULL 91 | ); 92 | pa_operation_unref(o); 93 | } 94 | } 95 | 96 | 97 | void PaObject::set_active_attribute(const char *name) 98 | { 99 | if (pa_set_active_attribute != nullptr) { 100 | pa_operation *o = pa_set_active_attribute( 101 | pulse.pa_ctx, 102 | index, 103 | name, 104 | NULL, 105 | NULL 106 | ); 107 | pa_operation_unref(o); 108 | } 109 | } 110 | 111 | 112 | void PaObject::set_default(const char *name) 113 | { 114 | if (pa_set_default != nullptr) { 115 | pa_operation *o = pa_set_default( 116 | pulse.pa_ctx, 117 | name, 118 | NULL, 119 | NULL 120 | ); 121 | pa_operation_unref(o); 122 | } 123 | } 124 | 125 | void PaObject::clearAttributes() 126 | { 127 | for (uint32_t i = 0; i < attributes.size(); i++) { 128 | delete attributes[i]; 129 | } 130 | 131 | attributes.clear(); 132 | } 133 | 134 | uint32_t PaObject::getRelation() 135 | { 136 | if (active_attribute != nullptr) { 137 | for (uint32_t j = 0; j < attributes.size(); j++) { 138 | int current = strcmp(attributes[j]->name, active_attribute->name); 139 | 140 | if (current == 0) { 141 | return j; 142 | } 143 | } 144 | } 145 | 146 | return -1; 147 | } 148 | -------------------------------------------------------------------------------- /src/pa_object.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PA_OBJECT_ 2 | #define PA_OBJECT_ 3 | 4 | #include 5 | #include 6 | 7 | #include "pa_object_attribute.hpp" 8 | #include "pa_port.hpp" 9 | 10 | enum pa_object_t {SINK, INPUT, SOURCE, SOURCE_OUTPUT, CARD}; 11 | 12 | class PaObject 13 | { 14 | public: 15 | PaObject(); 16 | virtual ~PaObject(); 17 | pa_object_t type; 18 | 19 | uint32_t index; 20 | char name[255]; 21 | char pa_name[255]; 22 | bool is_default; 23 | 24 | unsigned int channels; 25 | pa_volume_t volume; 26 | bool mute; 27 | 28 | uint32_t monitor_index; 29 | pa_stream *monitor_stream; 30 | float peak; 31 | 32 | PaObjectAttribute *active_attribute; 33 | std::vector attributes; 34 | 35 | pa_operation *(*pa_set_volume)( 36 | pa_context *, 37 | uint32_t, 38 | const pa_cvolume *, 39 | pa_context_success_cb_t, 40 | void * 41 | ); 42 | pa_operation *(*pa_set_mute)( 43 | pa_context *, 44 | uint32_t, 45 | int, 46 | pa_context_success_cb_t, 47 | void * 48 | ); 49 | pa_operation *(*pa_move)( 50 | pa_context *, 51 | uint32_t, 52 | uint32_t, 53 | pa_context_success_cb_t, 54 | void * 55 | ); 56 | pa_operation *(*pa_set_active_attribute)( 57 | pa_context *, 58 | uint32_t, 59 | const char *, 60 | pa_context_success_cb_t, 61 | void * 62 | ); 63 | pa_operation *(*pa_set_default)( 64 | pa_context *, 65 | const char*, 66 | pa_context_success_cb_t, 67 | void * 68 | ); 69 | 70 | void set_volume(float perc); 71 | void step_volume(int dir); 72 | void move(uint32_t dest); 73 | void toggle_mute(); 74 | void set_active_attribute(const char* name); 75 | void set_default(const char* name); 76 | void clearAttributes(); 77 | 78 | virtual const char *getAppName() 79 | { 80 | return nullptr; 81 | }; 82 | 83 | virtual uint32_t getRelation(); 84 | }; 85 | 86 | #endif // PA_OBJECT_ 87 | -------------------------------------------------------------------------------- /src/pa_object_attribute.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PA_OBJECT_ATTRIBUTE_ 2 | #define PA_OBJECT_ATTRIBUTE_ 3 | 4 | class PaObjectAttribute 5 | { 6 | public: 7 | char name[255]; 8 | char description[255]; 9 | virtual ~PaObjectAttribute(){}; 10 | }; 11 | 12 | #endif // PA_OBJECT_ATTRIBUTE_ 13 | -------------------------------------------------------------------------------- /src/pa_port.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PA_PORT_ 2 | #define PA_PORT_ 3 | #include "pa_object_attribute.hpp" 4 | 5 | class PaPort : public PaObjectAttribute 6 | { 7 | public: 8 | int available; 9 | }; 10 | #endif // PA_PORT_ 11 | -------------------------------------------------------------------------------- /src/pa_sink.cpp: -------------------------------------------------------------------------------- 1 | #include "pa_sink.hpp" 2 | #include 3 | 4 | PaSink::PaSink() 5 | { 6 | type = pa_object_t::SINK; 7 | monitor_stream = nullptr; 8 | 9 | pa_set_volume = &pa_context_set_sink_volume_by_index; 10 | pa_set_mute = &pa_context_set_sink_mute_by_index; 11 | pa_move = nullptr; 12 | pa_set_active_attribute = &pa_context_set_sink_port_by_index; 13 | pa_set_default = &pa_context_set_default_sink; 14 | } 15 | 16 | 17 | 18 | void PaSink::updatePorts(pa_sink_port_info **info, uint32_t n_ports) 19 | { 20 | clearAttributes(); 21 | 22 | for (uint32_t i = 0; i < n_ports; i++) { 23 | auto *p = new PaPort; 24 | 25 | snprintf(p->name, sizeof(p->name), "%s", info[i]->name); 26 | snprintf( 27 | p->description, 28 | sizeof(p->description), 29 | "%s", 30 | info[i]->description 31 | ); 32 | attributes.push_back(p); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/pa_sink.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PA_SINK_ 2 | #define PA_SINK_ 3 | #include 4 | #include "pa_object.hpp" 5 | #include "pa_port.hpp" 6 | 7 | class PaSink : public PaObject 8 | { 9 | public: 10 | PaSink(); 11 | void updatePorts(pa_sink_port_info **info, uint32_t n_ports); 12 | }; 13 | 14 | #endif // PA_SINK_ 15 | -------------------------------------------------------------------------------- /src/pa_source.cpp: -------------------------------------------------------------------------------- 1 | #include "pa_source.hpp" 2 | #include 3 | 4 | PaSource::PaSource() 5 | { 6 | type = pa_object_t::SOURCE; 7 | monitor_stream = nullptr; 8 | 9 | pa_set_volume = &pa_context_set_source_volume_by_index; 10 | pa_set_mute = &pa_context_set_source_mute_by_index ; 11 | pa_move = nullptr; 12 | pa_set_active_attribute = &pa_context_set_source_port_by_index; 13 | pa_set_default = &pa_context_set_default_source; 14 | } 15 | 16 | void PaSource::updatePorts(pa_source_port_info **info, uint32_t n_ports) 17 | { 18 | clearAttributes(); 19 | 20 | for (uint32_t i = 0; i < n_ports; i++) { 21 | PaPort *p = new PaPort; 22 | snprintf(p->name, sizeof(p->name), "%s", info[i]->name); 23 | snprintf( 24 | p->description, 25 | sizeof(p->description), 26 | "%s", 27 | info[i]->description 28 | ); 29 | attributes.push_back(p); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/pa_source.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PA_SOURCE_ 2 | #define PA_SOURCE_ 3 | #include "pa_object.hpp" 4 | 5 | class PaSource : public PaObject 6 | { 7 | public: 8 | PaSource(); 9 | void updatePorts(pa_source_port_info **info, uint32_t n_ports); 10 | }; 11 | 12 | #endif // PA_SOURCE_ 13 | -------------------------------------------------------------------------------- /src/pa_source_output.cpp: -------------------------------------------------------------------------------- 1 | #include "pa_source_output.hpp" 2 | 3 | PaSourceOutput::PaSourceOutput() 4 | { 5 | type = pa_object_t::SOURCE_OUTPUT; 6 | monitor_stream = nullptr; 7 | 8 | pa_set_volume = &pa_context_set_source_output_volume; 9 | pa_set_mute = &pa_context_set_source_output_mute; 10 | pa_move = &pa_context_move_source_output_by_index; 11 | pa_set_active_attribute = nullptr; 12 | pa_set_default = nullptr; 13 | 14 | source = 0; 15 | } 16 | -------------------------------------------------------------------------------- /src/pa_source_output.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PA_SOURCE_OUTPUT_ 2 | #define PA_SOURCE_OUTPUT_ 3 | #include "pa_input.hpp" 4 | 5 | class PaSourceOutput : public PaInput 6 | { 7 | public: 8 | PaSourceOutput(); 9 | uint32_t source; 10 | 11 | uint32_t getRelation() override { 12 | return source; 13 | }; 14 | }; 15 | 16 | #endif // PA_SOURCE_ 17 | -------------------------------------------------------------------------------- /src/pulsemixer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include "ui/ui.hpp" 14 | #include "pa.hpp" 15 | #include "config.hpp" 16 | #include "version.hpp" 17 | 18 | namespace fs = std::filesystem; 19 | 20 | void version() 21 | { 22 | if (strlen(GIT_VERSION) > 0) { 23 | printf("ncpamixer git v%s\n", GIT_VERSION); 24 | } else { 25 | printf("ncpamixer v%s\n", FALLBACK_VERSION); 26 | } 27 | 28 | printf("Build type: %s\n", BUILD_TYPE); 29 | printf("Build date: %s\n", BUILD_DATE); 30 | printf("\n"); 31 | 32 | } 33 | 34 | void help() 35 | { 36 | version(); 37 | 38 | printf("-c --config=CONFIG_FILE Set custom location for config\n"); 39 | printf("-t --tab=TAB Open on given tab. Choices: " 40 | "(p)layback (default), (r)ecording, (o)utput, (i)nput, (c)onfiguration\n"); 41 | printf("-h --help Print this help screen\n"); 42 | printf("-v --version Print version info\n"); 43 | } 44 | 45 | int main(int argc, char *argv[]) 46 | { 47 | setlocale(LC_ALL, ""); 48 | 49 | static const struct option longOpts[] = { 50 | { "version", no_argument, 0, 'v' }, 51 | { "help", no_argument, 0, 'h' }, 52 | { "config", required_argument, 0, 'c' }, 53 | { "tab", required_argument, 0, 't' }, 54 | { 0, 0, 0, 0 } 55 | }; 56 | 57 | int c; 58 | int longIndex = 0; 59 | int startingTab = TAB_PLAYBACK; 60 | 61 | std::optional config_path{std::nullopt}; 62 | 63 | while ((c = getopt_long(argc, argv, "vhc:t:", longOpts, &longIndex)) != -1) { 64 | 65 | switch (c) { 66 | case 'v': 67 | version(); 68 | return 0; 69 | 70 | case 'h': 71 | help(); 72 | return 0; 73 | 74 | case 'c': 75 | config_path = optarg; 76 | break; 77 | 78 | case 't': 79 | switch (optarg[0]) { 80 | case 'p': 81 | break; 82 | case 'r': 83 | startingTab = TAB_RECORDING; 84 | break; 85 | case 'o': 86 | startingTab = TAB_OUTPUT; 87 | break; 88 | case 'i': 89 | startingTab = TAB_INPUT; 90 | break; 91 | case 'c': 92 | startingTab = TAB_CONFIGURATION; 93 | break; 94 | default: 95 | fprintf(stderr, "invalid tab: %s\n", optarg); 96 | return 1; 97 | 98 | } 99 | break; 100 | case '?': 101 | return 0; 102 | 103 | default: 104 | break; 105 | } 106 | } 107 | 108 | config.init(config_path); 109 | 110 | pulse.init(); 111 | 112 | if (ui.init(startingTab) > 0) { 113 | ui.run(); 114 | } 115 | 116 | return 0; 117 | } 118 | -------------------------------------------------------------------------------- /src/ui/tab.cpp: -------------------------------------------------------------------------------- 1 | #include "tab.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "config.hpp" 14 | #include "ui.hpp" 15 | 16 | int Tab::getBlockSize() 17 | { 18 | int BLOCK_SIZE = 5; 19 | 20 | if (has_volume) { 21 | if (ui.hide_top) { 22 | BLOCK_SIZE -= 1; 23 | } 24 | 25 | if (ui.hide_bottom) { 26 | BLOCK_SIZE -= 1; 27 | } 28 | } 29 | 30 | return BLOCK_SIZE; 31 | } 32 | 33 | void Tab::draw() 34 | { 35 | if (object == nullptr) { 36 | return; 37 | } 38 | 39 | int baseY = 0; 40 | int current_block = 0; 41 | int BLOCK_SIZE = getBlockSize(); 42 | 43 | total_blocks = (ui.height - 2) / BLOCK_SIZE; 44 | int blocks_drawn = 0; 45 | 46 | bool more_up = false; 47 | bool more_down = false; 48 | 49 | for (auto &i : *object) { 50 | if (current_block <= selected_block - total_blocks) { 51 | current_block++; 52 | more_up = true; 53 | continue; 54 | } 55 | 56 | if (blocks_drawn >= total_blocks) { 57 | more_down = true; 58 | break; 59 | } 60 | 61 | blocks_drawn++; 62 | 63 | float perc = static_cast(i.second->volume) / 64 | (PA_VOLUME_NORM * 1.5f); 65 | 66 | if (has_volume) { 67 | if (ui.static_bar) { 68 | volumeBar(ui.width, ui.height, 0, baseY + 3, perc, perc); 69 | } else { 70 | volumeBar(ui.width, ui.height, 0, baseY + 3, perc, i.second->peak); 71 | } 72 | } else { // Configuration 73 | if (i.first == selected_index) { 74 | wattron(ui.window, COLOR_PAIR(COLOR_SELECTED)); 75 | } else { 76 | wattron(ui.window, COLOR_PAIR(COLOR_DEFAULT)); 77 | } 78 | 79 | if (i.second->active_attribute != nullptr) { 80 | mvwaddstr( 81 | ui.window, 82 | baseY + 3, 83 | 3, 84 | i.second->active_attribute->description 85 | ); 86 | borderBox(ui.width - 2, 2, 1, baseY + 2); 87 | } 88 | 89 | if (i.first == selected_index) { 90 | wattroff(ui.window, COLOR_PAIR(COLOR_SELECTED)); 91 | } else { 92 | wattroff(ui.window, COLOR_PAIR(COLOR_DEFAULT)); 93 | } 94 | } 95 | 96 | if (current_block == selected_block) { 97 | wattron(ui.window, COLOR_PAIR(COLOR_SELECTED)); 98 | } else { 99 | wattron(ui.window, COLOR_PAIR(COLOR_DEFAULT)); 100 | } 101 | 102 | char label[255] = {0}; 103 | std::string app = {0}; 104 | 105 | int toggle_len = 0; 106 | 107 | if (toggle != nullptr) { 108 | auto rel = toggle->find(i.second->getRelation()); 109 | 110 | if (rel != toggle->end()) { 111 | char *name = rel->second->name; 112 | 113 | if (name != nullptr) { 114 | unsigned int len = strlen(name); 115 | unsigned int sink_pos = ui.width - 1 - len; 116 | 117 | mvwaddstr( 118 | ui.window, 119 | baseY + 1, 120 | sink_pos, 121 | name 122 | ); 123 | 124 | toggle_len += strlen(name); 125 | } 126 | } 127 | } else { 128 | if (i.second->active_attribute != nullptr && has_volume) { 129 | unsigned int len = strlen( 130 | i.second->active_attribute->description 131 | ); 132 | 133 | unsigned int sink_pos = ui.width - 1 - len; 134 | 135 | mvwaddstr( 136 | ui.window, 137 | baseY + 1, 138 | sink_pos, 139 | i.second->active_attribute->description 140 | ); 141 | 142 | toggle_len += strlen(i.second->active_attribute->description); 143 | } 144 | } 145 | 146 | const char *app_name = i.second->getAppName(); 147 | 148 | if (app_name != nullptr && strlen(i.second->getAppName()) > 0) { 149 | app = std::string(i.second->getAppName()) + ": " + 150 | std::string(i.second->name); 151 | } else { 152 | app = i.second->name; 153 | } 154 | 155 | if (i.second->is_default) { 156 | app = ui.indicator + app; 157 | } 158 | 159 | bool dots = false; 160 | 161 | while (1) { 162 | if (has_volume) { 163 | if (i.second->mute) { 164 | snprintf( 165 | label, 166 | sizeof(label), 167 | "%s%s (muted)", 168 | app.c_str(), 169 | (dots) ? "..." : "" 170 | ); 171 | } else { 172 | snprintf( 173 | label, 174 | sizeof(label), 175 | "%s%s (%d%%)", 176 | app.c_str(), 177 | (dots) ? "..." : "", 178 | static_cast(perc * (1.5f * 100.f) + 0.1f) 179 | ); 180 | } 181 | } else { 182 | snprintf( 183 | label, 184 | sizeof(label), 185 | "%s%s", 186 | app.c_str(), 187 | (dots) ? "..." : "" 188 | ); 189 | } 190 | 191 | int output_len = strlen(label); 192 | int volume_len = (has_volume) ? 4 : 1; 193 | 194 | if ((output_len + toggle_len + volume_len) > ui.width) { 195 | if (app.length() > 0) { 196 | app.resize(app.length() - 1); 197 | } else { 198 | break; 199 | } 200 | 201 | dots = true; 202 | } else { 203 | break; 204 | } 205 | } 206 | 207 | mvwaddstr(ui.window, baseY + 1, 1, label); 208 | 209 | if (current_block == selected_block) { 210 | wattroff(ui.window, COLOR_PAIR(COLOR_SELECTED)); 211 | } else { 212 | wattroff(ui.window, COLOR_PAIR(COLOR_DEFAULT)); 213 | } 214 | 215 | baseY += BLOCK_SIZE; 216 | current_block++; 217 | } 218 | 219 | if (more_up) { 220 | mvwaddstr( 221 | ui.window, 222 | 0, 223 | (ui.width / 2) - 4, 224 | "\u2191\u2191\u2191\u2191" 225 | ); 226 | } 227 | 228 | if (more_down) { 229 | mvwaddstr( 230 | ui.window, 231 | ui.height - 2, 232 | (ui.width / 2) - 4, 233 | "\u2193\u2193\u2193\u2193" 234 | ); 235 | } 236 | } 237 | 238 | void Tab::handleEvents(const char* const &event) 239 | { 240 | if (object == nullptr) { 241 | return; 242 | } 243 | 244 | selected_index = pulse.exists(*object, selected_index); 245 | 246 | if (selected_index == static_cast(-1)) { 247 | return; 248 | } 249 | 250 | auto pai = object->find(selected_index); 251 | 252 | PaObject *selected_pobj = nullptr; 253 | 254 | if (pai != object->end()) { 255 | selected_pobj = pai->second; 256 | } 257 | 258 | if (!strcmp("mute", event)) { 259 | if (selected_pobj != nullptr) { 260 | selected_pobj->toggle_mute(); 261 | } 262 | } else if (!strcmp("move_first", event)) { 263 | auto i = object->begin(); 264 | 265 | if (i != object->end()) { 266 | selected_index = i->first; 267 | selected_block = 0; 268 | } 269 | 270 | } else if (!strcmp("move_last", event)) { 271 | auto i = object->rbegin(); 272 | 273 | if (i != object->rend()) { 274 | selected_index = i->first; 275 | selected_block = object->size() - 1; 276 | } 277 | } else if (!strcmp("move_up", event)) { 278 | auto i = std::prev(object->find(selected_index), 1); 279 | 280 | if (i != object->end()) { 281 | selected_index = i->first; 282 | selected_block = (selected_block > 0) ? selected_block - 1 : 0; 283 | } 284 | } else if (!strcmp("move_down", event)) { 285 | auto i = std::next(object->find(selected_index), 1); 286 | 287 | if (i != object->end()) { 288 | selected_index = i->first; 289 | selected_block++; 290 | } 291 | } else if (!strcmp("toggle_static", event)) { 292 | ui.static_bar = !ui.static_bar; 293 | } else if (!strcmp("volume_up", event)) { 294 | if (selected_pobj != nullptr) { 295 | selected_pobj->step_volume(1); 296 | } 297 | } else if (!strcmp("volume_down", event)) { 298 | if (selected_pobj != nullptr) { 299 | selected_pobj->step_volume(-1); 300 | } 301 | } else if (!strcmp("set_volume_0", event)) { 302 | if (selected_pobj != nullptr) { 303 | selected_pobj->set_volume(0); 304 | } 305 | } else if (!strcmp("set_volume_10", event)) { 306 | if (selected_pobj != nullptr) { 307 | selected_pobj->set_volume(.1f); 308 | } 309 | } else if (!strcmp("set_volume_20", event)) { 310 | if (selected_pobj != nullptr) { 311 | selected_pobj->set_volume(.2f); 312 | } 313 | } else if (!strcmp("set_volume_30", event)) { 314 | if (selected_pobj != nullptr) { 315 | selected_pobj->set_volume(.3f); 316 | } 317 | } else if (!strcmp("set_volume_40", event)) { 318 | if (selected_pobj != nullptr) { 319 | selected_pobj->set_volume(.4f); 320 | } 321 | } else if (!strcmp("set_volume_50", event)) { 322 | if (selected_pobj != nullptr) { 323 | selected_pobj->set_volume(.5f); 324 | } 325 | } else if (!strcmp("set_volume_60", event)) { 326 | if (selected_pobj != nullptr) { 327 | selected_pobj->set_volume(.6f); 328 | } 329 | } else if (!strcmp("set_volume_70", event)) { 330 | if (selected_pobj != nullptr) { 331 | selected_pobj->set_volume(.7f); 332 | } 333 | } else if (!strcmp("set_volume_80", event)) { 334 | if (selected_pobj != nullptr) { 335 | selected_pobj->set_volume(.8f); 336 | } 337 | } else if (!strcmp("set_volume_90", event)) { 338 | if (selected_pobj != nullptr) { 339 | selected_pobj->set_volume(.9f); 340 | } 341 | } else if (!strcmp("set_volume_100", event)) { 342 | if (selected_pobj != nullptr) { 343 | selected_pobj->set_volume(1.0f); 344 | } 345 | } else if (!strcmp("set_default", event)) { 346 | if (selected_pobj != nullptr) { 347 | selected_pobj->set_default(selected_pobj->pa_name); 348 | } 349 | } else if (!strcmp("switch", event)) { 350 | if (selected_pobj != nullptr && toggle != nullptr) { 351 | auto current_toggle = toggle->find(selected_pobj->getRelation()); 352 | current_toggle = std::next(current_toggle, 1); 353 | 354 | if (current_toggle == toggle->end()) { 355 | current_toggle = toggle->begin(); 356 | } 357 | 358 | selected_pobj->move(current_toggle->first); 359 | } else if (selected_pobj != nullptr) { 360 | uint32_t current_attribute = selected_pobj->getRelation(); 361 | 362 | if (current_attribute + 1 < selected_pobj->attributes.size()) { 363 | current_attribute++; 364 | } else { 365 | current_attribute = 0; 366 | } 367 | 368 | if (selected_pobj->attributes.size() > 0) { 369 | selected_pobj->set_active_attribute( 370 | selected_pobj->attributes[current_attribute]->name 371 | ); 372 | } 373 | } 374 | } else if (!strcmp("dropdown", event) || !strcmp("select", event)) { 375 | handleDropDown(selected_pobj); 376 | } 377 | } 378 | 379 | void Tab::handleMouse(int x, int y, int button) 380 | { 381 | if (object == nullptr) { 382 | return; 383 | } 384 | 385 | int baseY = 0; 386 | int current_block = 0; 387 | int BLOCK_SIZE = getBlockSize(); 388 | 389 | total_blocks = (ui.height - 2) / BLOCK_SIZE; 390 | int blocks_drawn = 0; 391 | 392 | bool more_up = false; 393 | bool more_down = false; 394 | 395 | for (auto &i : *object) { 396 | auto item = i.second; 397 | if (current_block <= selected_block - total_blocks) { 398 | current_block++; 399 | more_up = true; 400 | continue; 401 | } 402 | 403 | if (blocks_drawn >= total_blocks) { 404 | more_down = true; 405 | break; 406 | } 407 | 408 | blocks_drawn++; 409 | 410 | if (has_volume) { 411 | if (handleMouseVolumeBar(item, x, y, ui.width, ui.height, 0, baseY + 3, button)) { 412 | return; 413 | } 414 | } else { // Configuration 415 | if (item->active_attribute != nullptr) { 416 | if (handleMouseDropDown(item, x, y, ui.width - 2, 2, 1, baseY + 2)) { 417 | return; 418 | } 419 | } 420 | } 421 | 422 | unsigned int len = 0; 423 | unsigned int sink_pos = 0; 424 | PaObject* toggle_item = nullptr; 425 | 426 | if (toggle != nullptr) { 427 | auto rel = toggle->find(i.second->getRelation()); 428 | 429 | if (rel != toggle->end()) { 430 | char *name = rel->second->name; 431 | 432 | if (name != nullptr) { 433 | len = strlen(name); 434 | sink_pos = ui.width - 1 - len; 435 | toggle_item = rel->second; 436 | } 437 | } 438 | } else { 439 | if (i.second->active_attribute != nullptr && has_volume) { 440 | len = strlen(i.second->active_attribute->description); 441 | sink_pos = ui.width - 1 - len; 442 | toggle_item = i.second; 443 | } 444 | } 445 | 446 | if (toggle_item != nullptr) { 447 | auto save_selected_index = selected_index; 448 | auto save_selected_block = selected_block; 449 | 450 | selected_index = i.first; 451 | selected_block = current_block; 452 | 453 | bool done = handleMouseDropDown(toggle_item, x, y, len, 1, sink_pos, baseY + 1); 454 | 455 | selected_index = save_selected_index; 456 | selected_block = save_selected_block; 457 | 458 | if (done) { 459 | return; 460 | } 461 | } 462 | 463 | baseY += BLOCK_SIZE; 464 | current_block++; 465 | } 466 | 467 | if (more_up) { 468 | if (handleMouseMoreUp(x, y, 4, 1, (ui.width / 2) - 4, 0)) { 469 | return; 470 | } 471 | } 472 | 473 | if (more_down) { 474 | if (handleMouseMoreDown(x, y, 4, 1, (ui.width / 2) - 4, ui.height - 2)) { 475 | return; 476 | } 477 | } 478 | } 479 | 480 | void Tab::handleDropDown(PaObject* selected_pobj) 481 | { 482 | uint32_t selected = 0; 483 | 484 | int BLOCK_SIZE = getBlockSize(); 485 | 486 | if (selected_pobj != nullptr && toggle != nullptr) { 487 | selected = dropDown( 488 | -1, 489 | std::min( 490 | selected_block * (BLOCK_SIZE), 491 | (total_blocks - 1) * BLOCK_SIZE 492 | ), 493 | *toggle, 494 | selected_pobj->getRelation() 495 | ); 496 | 497 | if (selected != static_cast(-1)) { 498 | selected_pobj->move(selected); 499 | } 500 | } else if (selected_pobj != nullptr) { 501 | uint32_t w = 0; 502 | int x = 0; 503 | 504 | int y = std::min( 505 | selected_block * (BLOCK_SIZE), 506 | (total_blocks - 1) * BLOCK_SIZE 507 | ); 508 | 509 | 510 | if (has_volume) { 511 | x = -1; 512 | } else { 513 | x = 1; 514 | w = ui.width - 3; 515 | y += 2; 516 | } 517 | 518 | selected = dropDown( 519 | x, 520 | y, 521 | selected_pobj->attributes, 522 | selected_pobj->getRelation(), 523 | w 524 | ); 525 | 526 | if (selected != static_cast(-1)) { 527 | selected_pobj->set_active_attribute( 528 | selected_pobj->attributes[selected]->name 529 | ); 530 | } 531 | } 532 | } 533 | 534 | bool Tab::handleMouseVolumeBar( 535 | PaObject* item, 536 | int mousex, 537 | int mousey, 538 | int w, 539 | int h, 540 | int x, 541 | int y, 542 | int button 543 | ) 544 | { 545 | if (mousex < x || mousex >= x + w || mousey < (y - 1) || mousey >= y + 2) { 546 | return false; 547 | } 548 | 549 | if (button == 4) { 550 | item->step_volume(1); 551 | } else if (button == 5) { 552 | item->step_volume(-1); 553 | } else { 554 | item->set_volume(((mousex - x + 1) / (float)(w - x)) * 1.5f); 555 | } 556 | return true; 557 | } 558 | 559 | bool Tab::handleMouseDropDown( 560 | PaObject* item, 561 | int mousex, 562 | int mousey, 563 | int w, 564 | int h, 565 | int x, 566 | int y 567 | ) 568 | { 569 | if (mousex < x || mousex >= x + w || mousey < y || mousey >= y + 3) { 570 | return false; 571 | } 572 | 573 | handleDropDown(item); 574 | 575 | return true; 576 | } 577 | 578 | bool Tab::handleMouseMoreUp( 579 | int mousex, 580 | int mousey, 581 | int w, 582 | int h, 583 | int x, 584 | int y 585 | ) 586 | { 587 | if (mousex < x || mousex >= x + w || mousey < y || mousey > y) { 588 | return false; 589 | } 590 | 591 | handleEvents("move_up"); 592 | 593 | return true; 594 | } 595 | 596 | bool Tab::handleMouseMoreDown( 597 | int mousex, 598 | int mousey, 599 | int w, 600 | int h, 601 | int x, 602 | int y 603 | ) 604 | { 605 | if (mousex < x || mousex >= x + w || mousey < y || mousey > y) { 606 | return false; 607 | } 608 | 609 | handleEvents("move_down"); 610 | 611 | return true; 612 | } 613 | 614 | uint32_t Tab::dropDown( 615 | int x, 616 | int y, 617 | std::vector attributes, 618 | uint32_t current, 619 | uint32_t width, 620 | uint32_t height 621 | ) 622 | { 623 | if (attributes.empty()) { 624 | return -1; 625 | } 626 | 627 | std::map tmp; 628 | 629 | for (uint32_t i = 0; i < attributes.size(); i++) { 630 | tmp[i] = attributes[i]->description; 631 | } 632 | 633 | return dropDown(x, y, tmp, current, width, height); 634 | } 635 | 636 | uint32_t Tab::dropDown( 637 | int x, 638 | int y, 639 | std::map objects, 640 | uint32_t current, 641 | uint32_t width, 642 | uint32_t height 643 | ) 644 | { 645 | if (objects.empty()) { 646 | return -1; 647 | } 648 | 649 | std::map tmp; 650 | std::for_each( 651 | objects.begin(), 652 | objects.end(), 653 | [&tmp](std::pair const & obj) { 654 | tmp[obj.first] = obj.second->name; 655 | } 656 | ); 657 | 658 | return dropDown(x, y, tmp, current, width, height); 659 | } 660 | 661 | uint32_t Tab::dropDown( 662 | int x, 663 | int y, 664 | std::map objects, 665 | uint32_t current, 666 | uint32_t width, 667 | uint32_t height 668 | ) 669 | { 670 | if (objects.empty()) { 671 | return -1; 672 | } 673 | 674 | bool autowidth = false; 675 | bool autoheight = false; 676 | 677 | if (width == 0) { 678 | autowidth = true; 679 | } 680 | 681 | if (height == 0) { 682 | autoheight = true; 683 | } 684 | 685 | uint32_t selected = 0; 686 | 687 | std::vector items; 688 | MENU *menu = 0; 689 | WINDOW *menu_win = 0; 690 | 691 | for (auto &i : objects) { 692 | ITEM *item = new_item(i.second.c_str(), nullptr); 693 | set_item_opts(item, O_SELECTABLE); 694 | set_item_userptr(item, static_cast(&i)); 695 | 696 | items.push_back(item); 697 | 698 | if (i.first == current) { 699 | selected = items.size() - 1; 700 | } 701 | 702 | if (autowidth) { 703 | width = (width < strlen(i.second.c_str()) + 3) ? 704 | strlen(i.second.c_str()) : 705 | width; 706 | } 707 | 708 | if (autoheight) { 709 | height = (height < 5) ? height + 1 : 5; 710 | } 711 | } 712 | 713 | items.push_back(new_item(nullptr, nullptr)); 714 | menu = new_menu(&items[0]); 715 | 716 | if (x < 0) { 717 | x = ui.width - (width + 2); 718 | } 719 | 720 | menu_win = newwin(height + 2, width + 2, y, x); 721 | keypad(menu_win, true); 722 | 723 | set_menu_win(menu, menu_win); 724 | set_menu_sub(menu, derwin(menu_win, height + 1, width, 1, 1)); 725 | set_menu_format(menu, height, 1); 726 | 727 | wbkgd(menu_win, COLOR_PAIR(COLOR_BORDER)); 728 | //set_menu_back(menu, COLOR_PAIR(7)); 729 | set_menu_fore(menu, COLOR_PAIR(COLOR_DROPDOWN_SELECTED)); 730 | set_menu_grey(menu, COLOR_PAIR(COLOR_DROPDOWN_UNSELECTED)); 731 | 732 | //set_menu_mark(menu, "* "); 733 | set_menu_mark(menu, ""); 734 | menu_opts_on(menu, O_ONEVALUE); 735 | menu_opts_off(menu, O_SHOWDESC); 736 | menu_opts_off(menu, O_NONCYCLIC); 737 | 738 | box(menu_win, 0, 0); 739 | 740 | post_menu(menu); 741 | set_current_item(menu, items[selected]); 742 | 743 | wrefresh(menu_win); 744 | 745 | bool selecting = true; 746 | 747 | while (selecting) { 748 | int input = wgetch(menu_win); 749 | 750 | if (input == ERR) { 751 | continue; 752 | } 753 | 754 | #ifdef KEY_MOUSE 755 | if (input == KEY_MOUSE) { 756 | MEVENT mevent; 757 | int ok; 758 | 759 | ok = getmouse(&mevent); 760 | if (ok != OK) { 761 | continue; 762 | } 763 | 764 | if (mevent.bstate & BUTTON1_PRESSED) { 765 | if (mevent.y < y || mevent.y > y + (int)height + 1 || 766 | mevent.x < x || mevent.x > x + (int)width + 1) { 767 | clrtoeol(); 768 | 769 | ITEM *item = current_item(menu); 770 | selected = static_cast*>( 771 | item_userptr(item) 772 | )->first; 773 | 774 | selecting = false; 775 | continue; 776 | } 777 | } 778 | 779 | if (mevent.y == y || mevent.y == y + (int)height + 1 || 780 | mevent.x == x || mevent.x == x + (int)width + 1) { 781 | continue; 782 | } 783 | 784 | if (mevent.bstate & BUTTON1_PRESSED) { 785 | int top = top_row(menu); 786 | int idx = mevent.y - y - 1 + top; 787 | ITEM** its = menu_items(menu); 788 | int itc = item_count(menu); 789 | if (idx < itc) { 790 | ITEM* item = its[idx]; 791 | set_current_item(menu, item); 792 | selected = static_cast*>( 793 | item_userptr(item) 794 | )->first; 795 | } 796 | 797 | selecting = false; 798 | continue; 799 | } 800 | #if NCURSES_MOUSE_VERSION > 1 801 | else if (mevent.bstate & BUTTON4_PRESSED) { 802 | menu_driver(menu, REQ_UP_ITEM); 803 | } else if (mevent.bstate & BUTTON5_PRESSED) { 804 | menu_driver(menu, REQ_DOWN_ITEM); 805 | } 806 | #endif 807 | } 808 | #endif 809 | 810 | if (input == KEY_RESIZE) { 811 | selecting = false; 812 | continue; 813 | } 814 | 815 | std::string key = std::to_string(input); 816 | 817 | std::string event = config.getString(("keycode." + key).c_str(), "unbound"); 818 | 819 | if (!strcmp("unbound", event.c_str())) { 820 | continue; 821 | } 822 | 823 | if (!strcmp("select", event.c_str())) { 824 | clrtoeol(); 825 | 826 | ITEM *item = current_item(menu); 827 | selected = static_cast*>( 828 | item_userptr(item) 829 | )->first; 830 | 831 | selecting = false; 832 | } else if (!strcmp("move_down", event.c_str())) { 833 | menu_driver(menu, REQ_DOWN_ITEM); 834 | } else if (!strcmp("move_up", event.c_str())) { 835 | menu_driver(menu, REQ_UP_ITEM); 836 | } else if (!strcmp("page_up", event.c_str())) { 837 | menu_driver(menu, REQ_SCR_UPAGE); 838 | } else if (!strcmp("page_down", event.c_str())) { 839 | menu_driver(menu, REQ_SCR_DPAGE); 840 | } else if (!strcmp("quit", event.c_str())) { 841 | selecting = false; 842 | } 843 | 844 | wrefresh(menu_win); 845 | } 846 | 847 | unpost_menu(menu); 848 | free_menu(menu); 849 | 850 | for (auto i : items) { 851 | if (i != nullptr) 852 | free_item(i); 853 | } 854 | 855 | items.clear(); 856 | refresh(); 857 | 858 | return selected; 859 | } 860 | 861 | void Tab::borderBox(int w, int h, int px, int py) 862 | { 863 | wattron(ui.window, COLOR_PAIR(COLOR_BORDER)); 864 | 865 | mvwvline(ui.window, py, px, ACS_VLINE, h); 866 | mvwvline(ui.window, py, px + w, ACS_VLINE, h); 867 | 868 | mvwhline(ui.window, py, px, ACS_HLINE, w); 869 | mvwhline(ui.window, py + h, px, ACS_HLINE, w); 870 | 871 | mvwhline(ui.window, py, px, ACS_ULCORNER, 1); 872 | mvwhline(ui.window, py, px + w, ACS_URCORNER, 1); 873 | 874 | mvwhline(ui.window, py + h, px, ACS_LLCORNER, 1); 875 | mvwhline(ui.window, py + h, px + w, ACS_LRCORNER, 1); 876 | 877 | wattroff(ui.window, COLOR_PAIR(COLOR_BORDER)); 878 | } 879 | 880 | void Tab::selectBox(int w, int px, int py, bool selected) 881 | { 882 | if (selected) { 883 | wattron(ui.window, COLOR_PAIR(COLOR_SELECTED)); 884 | } else { 885 | wattron(ui.window, COLOR_PAIR(COLOR_DEFAULT)); 886 | } 887 | 888 | mvwaddstr(ui.window, py + 1, px + 2, "Digital Stereo (HDMI) Output"); 889 | 890 | if (selected) { 891 | wattroff(ui.window, COLOR_PAIR(COLOR_SELECTED)); 892 | } else { 893 | wattroff(ui.window, COLOR_PAIR(COLOR_DEFAULT)); 894 | } 895 | 896 | borderBox(w, 2, px, py); 897 | } 898 | 899 | void Tab::volumeBar(int w, int h, int px, int py, float vol, float peak) 900 | { 901 | auto dw = static_cast(w); 902 | 903 | int pw = static_cast(dw * peak); 904 | int vw = static_cast(dw * vol); 905 | int fw = w - pw; 906 | 907 | unsigned int color; 908 | 909 | if (!ui.hide_top) { 910 | fillW(w, h, 0, py - 1, ui.bar[BAR_TOP].c_str()); 911 | } else { 912 | py -= 1; 913 | } 914 | 915 | for (int i = 0; i < pw; i++) { 916 | if (i >= vw) { 917 | color = COLOR_VOLUME_PEAK; 918 | } else { 919 | color = getVolumeColor(static_cast( 920 | static_cast(i) / w * 100.0f 921 | )); 922 | } 923 | 924 | wattron(ui.window, COLOR_PAIR(color)); 925 | mvwaddstr(ui.window, py, i, ui.bar[BAR_FG].c_str()); 926 | wattroff(ui.window, COLOR_PAIR(color)); 927 | } 928 | 929 | for (int i = 0; i < fw; i++) { 930 | color = getBarColor(static_cast( 931 | static_cast(pw + i) / w * 100.0f 932 | )); 933 | 934 | wattron(ui.window, COLOR_PAIR(color)); 935 | mvwaddstr(ui.window, py, pw + i, ui.bar[BAR_BG].c_str()); 936 | wattroff(ui.window, COLOR_PAIR(color)); 937 | } 938 | 939 | if (!ui.hide_bottom) { 940 | fillW(w, h, 0, py + 1, ui.bar[BAR_BOTTOM].c_str()); 941 | } 942 | 943 | if (!ui.hide_indicator) { 944 | wattron(ui.window, COLOR_PAIR(COLOR_VOLUME_INDICATOR)); 945 | 946 | mvwaddstr( 947 | ui.window, 948 | py, 949 | vw - 1, 950 | ui.bar[BAR_INDICATOR].c_str() 951 | ); // Mark volume 952 | 953 | wattroff(ui.window, COLOR_PAIR(COLOR_VOLUME_INDICATOR)); 954 | } 955 | } 956 | 957 | unsigned int Tab::getVolumeColor(int p) 958 | { 959 | if (p < 33) { 960 | return COLOR_VOLUME_LOW; 961 | } else if (p < 66) { 962 | return COLOR_VOLUME_MID; 963 | } else { 964 | return COLOR_VOLUME_HIGH; 965 | } 966 | } 967 | 968 | unsigned int Tab::getBarColor(int p) 969 | { 970 | if (p < 33) { 971 | return COLOR_BAR_LOW; 972 | } else if (p < 66) { 973 | return COLOR_BAR_MID; 974 | } else { 975 | return COLOR_BAR_HIGH; 976 | } 977 | } 978 | 979 | void Tab::fillW(int w, int h, int offset_x, int offset_y, const char *str) 980 | { 981 | int wo = (w - offset_x); 982 | 983 | wattron(ui.window, COLOR_PAIR(COLOR_BORDER)); 984 | 985 | for (int i = 0; i < wo; i++) { 986 | mvwaddstr(ui.window, offset_y, offset_x + i, str); 987 | } 988 | 989 | wattroff(ui.window, COLOR_PAIR(COLOR_BORDER)); 990 | } 991 | -------------------------------------------------------------------------------- /src/ui/tab.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TAB_HPP_ 2 | #define TAB_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "pa.hpp" 9 | 10 | class Tab 11 | { 12 | public: 13 | bool has_volume; 14 | uint32_t selected_index; 15 | 16 | std::map *object; 17 | std::map *toggle; 18 | 19 | Tab() 20 | { 21 | object = nullptr; 22 | toggle = nullptr; 23 | selected_index = 0; 24 | selected_block = 0; 25 | total_blocks = 0; 26 | has_volume = true; 27 | } 28 | virtual ~Tab() = default; 29 | 30 | virtual void draw(); 31 | void handleEvents(const char* const &event); 32 | void handleMouse(int x, int y, int button); 33 | 34 | static void borderBox(int w, int h, int px, int py); 35 | static void selectBox(int w, int px, int py, bool selected); 36 | static void volumeBar( 37 | int w, 38 | int h, 39 | int px, 40 | int py, 41 | float vol, 42 | float peak 43 | ); 44 | static uint32_t dropDown( 45 | int x, 46 | int y, 47 | std::map objects, 49 | uint32_t current = 0, 50 | uint32_t width = 0, 51 | uint32_t height = 0 52 | ); 53 | static uint32_t dropDown( 54 | int x, 55 | int y, 56 | std::map objects, 58 | uint32_t current = 0, 59 | uint32_t width = 0, 60 | uint32_t height = 0 61 | ); 62 | static uint32_t dropDown( 63 | int x, 64 | int y, 65 | std::vector attributes, 66 | uint32_t current = 0, 67 | uint32_t width = 0, 68 | uint32_t height = 0 69 | ); 70 | private: 71 | static void fillW( 72 | int w, 73 | int h, 74 | int offset_x, 75 | int offset_y, 76 | const char *str 77 | ); 78 | static unsigned int getVolumeColor(int p); 79 | static unsigned int getBarColor(int p); 80 | 81 | int getBlockSize(); 82 | bool handleMouseVolumeBar( 83 | PaObject* item, 84 | int mousex, 85 | int mousey, 86 | int w, 87 | int h, 88 | int x, 89 | int y, 90 | int button 91 | ); 92 | bool handleMouseDropDown( 93 | PaObject* item, 94 | int mousex, 95 | int mousey, 96 | int w, 97 | int h, 98 | int x, 99 | int y 100 | ); 101 | bool handleMouseMoreUp( 102 | int mousex, 103 | int mousey, 104 | int w, 105 | int h, 106 | int x, 107 | int y 108 | ); 109 | bool handleMouseMoreDown( 110 | int mousex, 111 | int mousey, 112 | int w, 113 | int h, 114 | int x, 115 | int y 116 | ); 117 | void handleDropDown(PaObject* selected_pobj); 118 | 119 | int selected_block; 120 | int total_blocks; 121 | }; 122 | 123 | #endif // TAB_HPP_ 124 | -------------------------------------------------------------------------------- /src/ui/tabs/configuration.cpp: -------------------------------------------------------------------------------- 1 | #include "configuration.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "pa.hpp" 9 | #include "ui/ui.hpp" 10 | 11 | Configuration::Configuration() 12 | { 13 | object = &pulse.PA_CARDS; 14 | toggle = nullptr; 15 | 16 | selected_index = pulse.exists(*object, -1); 17 | 18 | has_volume = false; 19 | } 20 | -------------------------------------------------------------------------------- /src/ui/tabs/configuration.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CONFIGURATION_HPP_ 2 | #define CONFIGURATION_HPP_ 3 | 4 | #include "../tab.hpp" 5 | 6 | class Configuration : public Tab 7 | { 8 | public: 9 | Configuration(); 10 | ~Configuration() override = default; 11 | 12 | // void draw(); 13 | }; 14 | 15 | #endif // PLAYBACK_HPP_ 16 | -------------------------------------------------------------------------------- /src/ui/tabs/fallback.cpp: -------------------------------------------------------------------------------- 1 | #include "fallback.hpp" 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "ui/ui.hpp" 8 | 9 | void Fallback::draw() 10 | { 11 | static const char fallback[] = { 12 | "Establishing connection to PulseAudio. Please wait..." 13 | }; 14 | 15 | wattron(ui.window, COLOR_PAIR(COLOR_DEFAULT)); 16 | mvwaddstr( 17 | ui.window, 18 | (ui.height / 2) - 1, 19 | ui.width / 2 - strlen(fallback) / 2, 20 | fallback 21 | ); 22 | wattroff(ui.window, COLOR_PAIR(COLOR_DEFAULT)); 23 | } 24 | -------------------------------------------------------------------------------- /src/ui/tabs/fallback.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FALLBACK_HPP_ 2 | #define FALLBACK_HPP_ 3 | 4 | #include "ui/tab.hpp" 5 | 6 | class Fallback : public Tab 7 | { 8 | public: 9 | Fallback() = default; 10 | virtual ~Fallback() = default; 11 | 12 | void draw() override; 13 | static void handleEvents(const char *event) 14 | { 15 | // Do nothing 16 | } 17 | }; 18 | 19 | #endif // FALLBACK_HPP_ 20 | -------------------------------------------------------------------------------- /src/ui/tabs/input.cpp: -------------------------------------------------------------------------------- 1 | #include "input.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "pa.hpp" 9 | 10 | Input::Input() 11 | { 12 | object = &pulse.PA_SOURCES; 13 | toggle = nullptr; 14 | 15 | selected_index = pulse.exists(*object, -1); 16 | } 17 | -------------------------------------------------------------------------------- /src/ui/tabs/input.hpp: -------------------------------------------------------------------------------- 1 | #ifndef INPUT_HPP_ 2 | #define INPUT_HPP_ 3 | 4 | #include "../tab.hpp" 5 | 6 | class Input : public Tab 7 | { 8 | public: 9 | Input(); 10 | ~Input() override = default; 11 | }; 12 | 13 | #endif // PLAYBACK_HPP_ 14 | -------------------------------------------------------------------------------- /src/ui/tabs/output.cpp: -------------------------------------------------------------------------------- 1 | #include "output.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "pa.hpp" 9 | 10 | Output::Output() 11 | { 12 | object = &pulse.PA_SINKS; 13 | toggle = nullptr; 14 | 15 | selected_index = pulse.exists(*object, -1); 16 | } 17 | -------------------------------------------------------------------------------- /src/ui/tabs/output.hpp: -------------------------------------------------------------------------------- 1 | #ifndef OUTPUT_HPP_ 2 | #define OUTPUT_HPP_ 3 | 4 | #include "../tab.hpp" 5 | 6 | class Output : public Tab 7 | { 8 | public: 9 | Output(); 10 | ~Output() override = default; 11 | }; 12 | 13 | #endif // PLAYBACK_HPP_ 14 | -------------------------------------------------------------------------------- /src/ui/tabs/playback.cpp: -------------------------------------------------------------------------------- 1 | #include "playback.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "pa.hpp" 11 | 12 | Playback::Playback() 13 | { 14 | object = &pulse.PA_INPUTS; 15 | toggle = &pulse.PA_SINKS; 16 | 17 | selected_index = pulse.exists(*object, -1); 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/tabs/playback.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PLAYBACK_HPP_ 2 | #define PLAYBACK_HPP_ 3 | 4 | #include "../tab.hpp" 5 | 6 | class Playback : public Tab 7 | { 8 | public: 9 | Playback(); 10 | ~Playback() override = default; 11 | }; 12 | 13 | #endif // PLAYBACK_HPP_ 14 | -------------------------------------------------------------------------------- /src/ui/tabs/recording.cpp: -------------------------------------------------------------------------------- 1 | #include "recording.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "pa.hpp" 9 | 10 | Recording::Recording() 11 | { 12 | object = &pulse.PA_SOURCE_OUTPUTS; 13 | toggle = &pulse.PA_SOURCES; 14 | 15 | selected_index = pulse.exists(*object, -1); 16 | } 17 | -------------------------------------------------------------------------------- /src/ui/tabs/recording.hpp: -------------------------------------------------------------------------------- 1 | #ifndef RECORDING_HPP_ 2 | #define RECORDING_HPP_ 3 | 4 | #include "../tab.hpp" 5 | 6 | class Recording : public Tab 7 | { 8 | public: 9 | Recording(); 10 | ~Recording() override = default; 11 | }; 12 | 13 | #endif // PLAYBACK_HPP_ 14 | -------------------------------------------------------------------------------- /src/ui/ui.cpp: -------------------------------------------------------------------------------- 1 | #include "ui.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "config.hpp" 13 | 14 | #include "tabs/configuration.hpp" 15 | #include "tabs/fallback.hpp" 16 | #include "tabs/input.hpp" 17 | #include "tabs/output.hpp" 18 | #include "tabs/playback.hpp" 19 | #include "tabs/recording.hpp" 20 | 21 | #define KEY_ALT(x) (KEY_F(64 - 26) + ((x) - 'A')) 22 | 23 | Ui ui; 24 | 25 | constexpr char Ui::HELP_HEADER[]; 26 | constexpr char Ui::HELP_FOOTER[]; 27 | 28 | Ui::Ui() 29 | { 30 | disconnect = false; 31 | running = false; 32 | current_tab = nullptr; 33 | window = nullptr; 34 | statusbar = nullptr; 35 | width = 0; 36 | height = 0; 37 | tab_index = 0; 38 | hide_indicator = false; 39 | hide_top = false; 40 | hide_bottom = false; 41 | static_bar = false; 42 | } 43 | 44 | Ui::~Ui() 45 | { 46 | delete current_tab; 47 | } 48 | 49 | int Ui::init(int tab) 50 | { 51 | setlocale(LC_ALL, ""); 52 | initscr(); 53 | curs_set(0); 54 | 55 | noecho(); 56 | nonl(); 57 | raw(); 58 | 59 | start_color(); 60 | use_default_colors(); 61 | 62 | std::string theme = std::string("theme." + config.getString("theme", "default")); 63 | 64 | init_pair( 65 | COLOR_BAR_LOW, 66 | config.getInt((theme + ".bar_low.front").c_str(), COLOR_BLACK), 67 | config.getInt((theme + ".bar_low.back").c_str(), COLOR_GREEN) 68 | ); 69 | init_pair( 70 | COLOR_BAR_MID, 71 | config.getInt((theme + ".bar_mid.front").c_str(), COLOR_BLACK), 72 | config.getInt((theme + ".bar_mid.back").c_str(), COLOR_YELLOW) 73 | ); 74 | init_pair( 75 | COLOR_BAR_HIGH, 76 | config.getInt((theme + ".bar_high.front").c_str(), COLOR_BLACK), 77 | config.getInt((theme + ".bar_high.back").c_str(), COLOR_RED) 78 | ); 79 | init_pair( 80 | COLOR_VOLUME_LOW, 81 | config.getInt((theme + ".volume_low").c_str(), COLOR_GREEN), 82 | COLOR_BACKGROUND 83 | ); 84 | init_pair( 85 | COLOR_VOLUME_MID, 86 | config.getInt((theme + ".volume_mid").c_str(), COLOR_YELLOW), 87 | COLOR_BACKGROUND 88 | ); 89 | init_pair( 90 | COLOR_VOLUME_HIGH, 91 | config.getInt((theme + ".volume_high").c_str(), COLOR_RED), 92 | COLOR_BACKGROUND 93 | ); 94 | init_pair( 95 | COLOR_VOLUME_PEAK, 96 | config.getInt((theme + ".volume_peak").c_str(), COLOR_RED), 97 | COLOR_BACKGROUND 98 | ); 99 | init_pair( 100 | COLOR_VOLUME_INDICATOR, 101 | config.getInt((theme + ".volume_indicator").c_str(), COLOR_FOREGROUND), 102 | COLOR_BACKGROUND 103 | ); 104 | init_pair( 105 | COLOR_SELECTED, 106 | config.getInt((theme + ".selected").c_str(), COLOR_GREEN), 107 | COLOR_BACKGROUND 108 | ); 109 | init_pair( 110 | COLOR_DEFAULT, 111 | config.getInt((theme + ".default").c_str(), COLOR_FOREGROUND), 112 | COLOR_BACKGROUND 113 | ); 114 | init_pair( 115 | COLOR_BORDER, 116 | config.getInt((theme + ".border").c_str(), COLOR_FOREGROUND), 117 | COLOR_BACKGROUND 118 | ); 119 | 120 | init_pair( 121 | COLOR_DROPDOWN_SELECTED, 122 | config.getInt((theme + ".dropdown.selected_text").c_str(), COLOR_BLACK), 123 | config.getInt((theme + ".dropdown.selected").c_str(), COLOR_GREEN) 124 | ); 125 | 126 | init_pair( 127 | COLOR_DROPDOWN_UNSELECTED, 128 | config.getInt((theme + ".dropdown.unselected").c_str(), COLOR_FOREGROUND), 129 | COLOR_BACKGROUND 130 | ); 131 | 132 | bar[BAR_BG].assign( 133 | config.getString((theme + ".bar_style.bg").c_str(), u8"░") 134 | ); 135 | bar[BAR_FG].assign( 136 | config.getString((theme + ".bar_style.fg").c_str(), u8"█") 137 | ); 138 | 139 | if(!config.keyEmpty((theme + ".bar_style.indicator").c_str())) { 140 | bar[BAR_INDICATOR].assign( 141 | config.getString((theme + ".bar_style.indicator").c_str(), u8"█") 142 | ); 143 | } else { 144 | hide_indicator = true; 145 | } 146 | if(!config.keyEmpty((theme + ".bar_style.top").c_str())) { 147 | bar[BAR_TOP].assign( 148 | config.getString((theme + ".bar_style.top").c_str(), u8"▁") 149 | ); 150 | } else { 151 | hide_top = true; 152 | } 153 | if(!config.keyEmpty((theme + ".bar_style.bottom").c_str())) { 154 | bar[BAR_BOTTOM].assign( 155 | config.getString((theme + ".bar_style.bottom").c_str(), u8"▔") 156 | ); 157 | } else { 158 | hide_bottom = true; 159 | } 160 | 161 | static_bar = config.getBool((theme + ".static_bar").c_str(), false); 162 | 163 | indicator.assign( 164 | config.getString((theme + ".default_indicator").c_str(), u8"♦ ") 165 | ); 166 | 167 | running = true; 168 | switchTab(tab); 169 | 170 | getmaxyx(stdscr, height, width); 171 | 172 | statusbar = newwin(1, width, height - 1, 0); 173 | window = newwin(height - 1, width, 0, 0); 174 | 175 | keypad(window, true); 176 | nodelay(window, true); 177 | idlok(window, true); 178 | 179 | erase(); 180 | refresh(); 181 | 182 | wrefresh(window); 183 | wrefresh(statusbar); 184 | 185 | return 1; 186 | } 187 | 188 | void Ui::checkPulseAudio() 189 | { 190 | if (!pulse.connected && !disconnect) { 191 | disconnect = true; 192 | delete current_tab; 193 | current_tab = new Fallback(); 194 | } else if (pulse.connected && disconnect) { 195 | disconnect = false; 196 | switchTab(tab_index); 197 | } 198 | } 199 | 200 | void Ui::switchTab(int index) 201 | { 202 | if (disconnect) { 203 | return; 204 | } 205 | 206 | delete current_tab; 207 | 208 | tab_index = index; 209 | 210 | switch (index) { 211 | default: 212 | case TAB_PLAYBACK: 213 | tab_index = TAB_PLAYBACK; 214 | current_tab = new Playback(); 215 | break; 216 | 217 | case TAB_RECORDING: 218 | current_tab = new Recording(); 219 | break; 220 | 221 | case TAB_OUTPUT: 222 | current_tab = new Output(); 223 | break; 224 | 225 | case TAB_INPUT: 226 | current_tab = new Input(); 227 | break; 228 | 229 | case -1: 230 | case TAB_CONFIGURATION: 231 | tab_index = TAB_CONFIGURATION; 232 | current_tab = new Configuration(); 233 | break; 234 | } 235 | } 236 | 237 | void Ui::handleInput() 238 | { 239 | set_escdelay(0); 240 | 241 | #ifdef KEY_MOUSE 242 | mouseinterval(0); 243 | #if NCURSES_MOUSE_VERSION > 1 244 | mousemask(BUTTON1_PRESSED | BUTTON4_PRESSED | BUTTON5_PRESSED, NULL); 245 | #else 246 | mousemask(BUTTON1_PRESSED, NULL); 247 | #endif 248 | #endif 249 | 250 | int input = wgetch(window); 251 | std::string event{}; 252 | 253 | if (input == ERR) { 254 | return; 255 | } 256 | 257 | switch (input) { 258 | #ifdef KEY_MOUSE 259 | case KEY_MOUSE: { 260 | MEVENT mevent; 261 | int ok; 262 | 263 | ok = getmouse(&mevent); 264 | if (ok != OK) { 265 | return; 266 | } 267 | if (mevent.y == height - 1) { 268 | if (mevent.bstate & BUTTON1_PRESSED) { 269 | int x = 0; 270 | for (int i = 0; i < NUM_TABS; i++) { 271 | int len = strlen(tabs[i]); 272 | if (mevent.x >= x && mevent.x < x + len) { 273 | switchTab(i); 274 | return; 275 | } 276 | x += len + 1; 277 | } 278 | #if NCURSES_MOUSE_VERSION > 1 279 | } else if (mevent.bstate & BUTTON4_PRESSED) { 280 | switchTab(++tab_index); 281 | return; 282 | } else if (mevent.bstate & BUTTON5_PRESSED) { 283 | switchTab(--tab_index); 284 | return; 285 | #endif 286 | } 287 | } else { 288 | int button = 0; 289 | if (mevent.bstate & BUTTON1_PRESSED) { 290 | button = 1; 291 | } 292 | #if NCURSES_MOUSE_VERSION > 1 293 | else if (mevent.bstate & BUTTON4_PRESSED) { 294 | button = 4; 295 | } else if (mevent.bstate & BUTTON5_PRESSED) { 296 | button = 5; 297 | } 298 | #endif 299 | 300 | current_tab->handleMouse(mevent.x, mevent.y, button); 301 | } 302 | 303 | return; 304 | } 305 | #endif 306 | case KEY_RESIZE: 307 | getmaxyx(stdscr, height, width); 308 | wresize(window, height - 1, width); 309 | 310 | mvwin(statusbar, height - 1, 0); 311 | wresize(statusbar, 1, width); 312 | 313 | return; 314 | 315 | case 27: { // Fix for alt/escape (also f-keys on some terminals) 316 | input = wgetch(window); 317 | 318 | if (input != -1 && input != 79) { 319 | std::string key = std::to_string(input); 320 | 321 | event = config.getString( 322 | ("keycode.alt." + key).c_str(), 323 | "unbound" 324 | ); 325 | 326 | break; 327 | } 328 | 329 | if (input == 79) { 330 | input = wgetch(window); 331 | 332 | if (input != -1) { 333 | std::string key = std::to_string(input); 334 | 335 | event = config.getString( 336 | ("keycode.f." + key).c_str(), 337 | "unbound" 338 | ); 339 | 340 | break; 341 | } 342 | } 343 | 344 | input = 27; 345 | } 346 | 347 | default: 348 | std::string key = std::to_string(input); 349 | event = config.getString( 350 | ("keycode." + key).c_str(), 351 | "unbound" 352 | ); 353 | 354 | break; 355 | } 356 | 357 | if (!strcmp("unbound", event.c_str())) { 358 | return; 359 | } 360 | 361 | if (!strcmp("quit", event.c_str())) { 362 | kill(); 363 | } else if (!strcmp("tab_next", event.c_str())) { 364 | switchTab(++tab_index); 365 | } else if (!strcmp("tab_prev", event.c_str())) { 366 | switchTab(--tab_index); 367 | } else if (!strcmp("tab_playback", event.c_str())) { 368 | switchTab(TAB_PLAYBACK); 369 | } else if (!strcmp("tab_recording", event.c_str())) { 370 | switchTab(TAB_RECORDING); 371 | } else if (!strcmp("tab_output", event.c_str())) { 372 | switchTab(TAB_OUTPUT); 373 | } else if (!strcmp("tab_input", event.c_str())) { 374 | switchTab(TAB_INPUT); 375 | } else if (!strcmp("tab_config", event.c_str())) { 376 | switchTab(TAB_CONFIGURATION); 377 | } else if (!strcmp("help", event.c_str())) { 378 | show_help(); 379 | } else { 380 | current_tab->handleEvents(event.c_str()); 381 | } 382 | } 383 | 384 | void Ui::show_help() { 385 | if (LINES < 10) return; 386 | if (COLS < 40) return; 387 | 388 | // Create a new pad. The size of the pad is defined by HELP_LINES. 389 | const auto& c { config.getKeycodeNameEvents() }; 390 | const auto HELP_LINES = static_cast(c.size()); 391 | if (HELP_LINES < 1) return; // No help to show :( 392 | WINDOW *pad = newpad(HELP_LINES, COLS - 10); 393 | 394 | // Add some text to the pad 395 | int line = 0; 396 | // max_length left-justifies the string and pads it with spaces to the right, ensuring 397 | // that the equals sign always appears at the same position 398 | int max_length = 0; 399 | for (const auto& it : c) { 400 | max_length = std::max(max_length, static_cast(it.first.length())); 401 | } 402 | 403 | int start_pos = (getmaxx(pad) - (max_length + 10)) / 2; // 10 is an arbitrary number 404 | if (start_pos < 0) start_pos = 0; // Ensure start_pos is not negative 405 | for (const auto& it : c) { 406 | mvwprintw(pad, line++, start_pos, "%-*s = %s", max_length, it.first.c_str(), it.second.c_str()); 407 | } 408 | 409 | // Calculate the size and position of the window 410 | int window_height = LINES - 10; 411 | int window_width = COLS - 10; 412 | int window_top = (LINES - window_height) / 2; 413 | int window_left = (COLS - window_width) / 2; 414 | 415 | // Create a new window to display the pad 416 | WINDOW *win = newwin(window_height, window_width, window_top, window_left); 417 | box(win, 0, 0); 418 | 419 | // Add a title and a footer to the window 420 | mvwprintw(win, 0, (window_width - HELP_HEADER_SIZE) / 2, "%s", HELP_HEADER); 421 | mvwprintw(win, window_height - 1, (window_width - HELP_FOOTER_SIZE) / 2, "%s", HELP_FOOTER); 422 | 423 | wrefresh(win); 424 | 425 | int pad_top_line = 0; 426 | 427 | // Main loop 428 | while (true) { 429 | // Display the pad in the window 430 | prefresh(pad, pad_top_line, 0, window_top + 1, window_left + 1, window_top + window_height - 2, window_left + window_width - 2); 431 | 432 | // Wait for user input 433 | int ch = getch(); 434 | if (ch == KEY_UP || ch == 'k') { 435 | // Up arrow key pressed. Check if the top of the pad is reached. 436 | if (pad_top_line > 0) { 437 | --pad_top_line; 438 | } 439 | } else if (ch == KEY_DOWN || ch == 'j') { 440 | // Down arrow key pressed. Check if the bottom of the pad is reached. 441 | if (pad_top_line < HELP_LINES - (window_height - 2)) { 442 | ++pad_top_line; 443 | } 444 | } else if (ch == 'q') { 445 | // 'q' key pressed. Exit the loop. 446 | break; 447 | } 448 | } 449 | 450 | // Delete the window and the pad 451 | delwin(win); 452 | delwin(pad); 453 | } 454 | 455 | void Ui::kill() 456 | { 457 | endwin(); 458 | running = false; 459 | } 460 | 461 | void Ui::draw() 462 | { 463 | werase(window); 464 | current_tab->draw(); 465 | wrefresh(window); 466 | 467 | statusBar(); 468 | } 469 | 470 | void Ui::run() 471 | { 472 | while (running) { 473 | checkPulseAudio(); 474 | draw(); 475 | 476 | usleep(20000); 477 | 478 | handleInput(); 479 | } 480 | } 481 | 482 | void Ui::statusBar() 483 | { 484 | werase(statusbar); 485 | 486 | int len = 0; 487 | 488 | for (int i = 0; i < NUM_TABS; ++i) { 489 | if (tab_index == i) { 490 | wattron(statusbar, COLOR_PAIR(COLOR_SELECTED)); 491 | } else { 492 | wattron(statusbar, COLOR_PAIR(COLOR_DEFAULT)); 493 | } 494 | 495 | mvwaddstr(statusbar, 0, len, tabs[i]); 496 | len += strlen(tabs[i]) + 1; 497 | 498 | if (tab_index == i) { 499 | wattroff(statusbar, COLOR_PAIR(COLOR_SELECTED)); 500 | } else { 501 | wattroff(statusbar, COLOR_PAIR(COLOR_DEFAULT)); 502 | } 503 | } 504 | 505 | wrefresh(statusbar); 506 | } 507 | -------------------------------------------------------------------------------- /src/ui/ui.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UI_HPP_ 2 | #define UI_HPP_ 3 | 4 | #define _XOPEN_SOURCE_EXTENDED 1 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "pa.hpp" 13 | #include "tab.hpp" 14 | 15 | #define NUM_TABS 5 16 | 17 | #define COLOR_FOREGROUND (-1) 18 | #define COLOR_BACKGROUND (-1) 19 | 20 | enum TABS { 21 | TAB_PLAYBACK = 0, 22 | TAB_RECORDING, 23 | TAB_OUTPUT, 24 | TAB_INPUT, 25 | TAB_CONFIGURATION 26 | }; 27 | 28 | enum THEME { 29 | COLOR_BAR_LOW = 1, 30 | COLOR_BAR_MID, 31 | COLOR_BAR_HIGH, 32 | COLOR_VOLUME_LOW, 33 | COLOR_VOLUME_MID, 34 | COLOR_VOLUME_HIGH, 35 | COLOR_VOLUME_PEAK, 36 | COLOR_VOLUME_INDICATOR, 37 | COLOR_DEFAULT, 38 | COLOR_SELECTED, 39 | COLOR_DROPDOWN_SELECTED, 40 | COLOR_DROPDOWN_UNSELECTED, 41 | COLOR_BORDER 42 | }; 43 | 44 | enum BAR { 45 | BAR_BG = 0, 46 | BAR_FG, 47 | BAR_INDICATOR, 48 | BAR_TOP, 49 | BAR_BOTTOM, 50 | BAR_SIZE, 51 | }; 52 | 53 | class Ui 54 | { 55 | public: 56 | Ui(); 57 | virtual ~Ui(); 58 | 59 | int init(int tab); 60 | void run(); 61 | 62 | int width; 63 | int height; 64 | 65 | WINDOW *window; 66 | WINDOW *statusbar; 67 | 68 | std::string bar[BAR_SIZE + 1]; 69 | std::string indicator; 70 | 71 | bool hide_indicator; 72 | bool hide_top; 73 | bool hide_bottom; 74 | bool static_bar; 75 | private: 76 | bool running; 77 | bool disconnect; 78 | 79 | const char *tabs[NUM_TABS] = { 80 | "Playback", 81 | "Recording", 82 | "Output Devices", 83 | "Input Devices", 84 | "Configuration" 85 | }; 86 | 87 | int tab_index; 88 | Tab *current_tab; 89 | 90 | static constexpr char HELP_HEADER[] = { " HELP " }; 91 | static constexpr auto HELP_HEADER_SIZE {sizeof (HELP_HEADER) - 1}; 92 | static constexpr char HELP_FOOTER[] = {" Press 'j/k' to scroll 'q' to exit "}; 93 | static constexpr auto HELP_FOOTER_SIZE{sizeof (HELP_FOOTER) - 1}; 94 | 95 | static void resize(int signum); 96 | 97 | void statusBar(); 98 | void handleInput(); 99 | void kill(); 100 | void draw(); 101 | void switchTab(int index); 102 | void checkPulseAudio(); 103 | void show_help(); 104 | }; 105 | 106 | extern Ui ui; 107 | 108 | #endif // UI_HPP_ 109 | -------------------------------------------------------------------------------- /src/version.cpp.in: -------------------------------------------------------------------------------- 1 | #include "version.hpp" 2 | 3 | const char GIT_VERSION[] = "@GIT_VERSION@"; 4 | const char BUILD_TYPE[] = "@BUILD_TYPE@"; 5 | const char BUILD_DATE[] = "@BUILD_DATE@"; 6 | -------------------------------------------------------------------------------- /src/version.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VERSION_HPP_ 2 | #define VERSION_HPP_ 3 | 4 | extern const char GIT_VERSION[]; 5 | extern const char BUILD_TYPE[]; 6 | extern const char BUILD_DATE[]; 7 | 8 | const char FALLBACK_VERSION[] = "1.3"; 9 | 10 | #endif // VERSION_HPP_ 11 | --------------------------------------------------------------------------------