diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..ffd7221a --- /dev/null +++ b/.clang-format @@ -0,0 +1,13 @@ +--- +Language: Cpp +Standard: Cpp11 +BasedOnStyle: Google +ColumnLimit: 100 +NamespaceIndentation: All +AlignAfterOpenBracket: DontAlign +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +BreakConstructorInitializersBeforeComma: true +DerivePointerAlignment: false +PointerAlignment: Left +--- diff --git a/.exrc b/.exrc index 712d7b8a..203a52a6 100644 --- a/.exrc +++ b/.exrc @@ -1,5 +1,24 @@ -let &path.="include,src," -let g:alternateSearchPath = 'sfr:../src,sfr:../../src/modules,sfr:../../src/utils,sfr:../../src/interfaces,sfr:../../src/services,sfr:../../src/drawtypes,sfr:../include,sfr:../../include/modules,sfr:../../include/interfaces,sfr:../../include/utils,sfr:../../include/services,sfr:../../include/drawtypes,' +let &path.='include,src,' +let g:alternateSearchPath = '' + \ . 'sfr:../src' + \ . ',sfr:../../src/adapters' + \ . ',sfr:../../src/components' + \ . ',sfr:../../src/drawtypes' + \ . ',sfr:../../src/interfaces' + \ . ',sfr:../../src/modules' + \ . ',sfr:../../src/services' + \ . ',sfr:../../src/utils' + \ . ',sfr:../../src/x11' + \ . ',sfr:../include' + \ . ',sfr:../../include/adapters' + \ . ',sfr:../../include/components' + \ . ',sfr:../../include/drawtypes' + \ . ',sfr:../../include/interfaces' + \ . ',sfr:../../include/modules' + \ . ',sfr:../../include/services' + \ . ',sfr:../../include/utils' + \ . ',sfr:../../include/x11' + let g:alternateExtensions_cpp = 'hpp' -" let tag_path = expand("%:p:h") . "/tags" -set tags+=/home/jaagr/var/github/jaagr/lemonbuddy/tags +let tag_path='/home/jaagr/.local/src/c++/lemonbuddy/.tags' +set tags+=/home/jaagr/.local/src/c++/lemonbuddy/.tags diff --git a/.gitignore b/.gitignore index 07d06bfc..34ccafdf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ tags *.pyc *.tmp include/config.hpp +.tags diff --git a/.gitmodules b/.gitmodules index 7098b259..a11909e9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,3 @@ -[submodule "contrib/lemonbar-sm-git"] - path = contrib/lemonbar-sm-git - url = https://github.com/jaagr/bar - branch = v1.1 [submodule "lib/i3ipcpp"] path = lib/i3ipcpp url = https://github.com/jaagr/i3ipcpp @@ -9,4 +5,7 @@ [submodule "lib/xpp"] path = lib/xpp url = https://github.com/jaagr/xpp - branch = 1.0.0 + branch = 1.1.1 +[submodule "lib/gsl"] + path = lib/gsl + url = https://github.com/Microsoft/GSL diff --git a/.travis.yml b/.travis.yml index dfd40fb0..ad64b853 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,16 @@ sudo: required dist: trusty +addons: + apt: + sources: + - ubuntu-toolchain-r-test + - llvm-toolchain-precise-3.8 + packages: + - gcc-5 + - g++-5 + - clang-3.8 + language: cpp compiler: - clang @@ -9,27 +19,50 @@ cache: ccache env: global: - - LLVM_VERSION=3.8.0 - - LLVM_ARCHIVE_PATH=$HOME/clang+llvm.tar.xz + - BUILD_TYPE="Release" + - LLVM_VERSION="3.8.1" + - LLVM_URL="http://llvm.org/releases/${LLVM_VERSION}/llvm-${LLVM_VERSION}.src.tar.xz" + - LIBCXX_URL="http://llvm.org/releases/${LLVM_VERSION}/libcxx-${LLVM_VERSION}.src.tar.xz" + - LIBCXXABI_URL="http://llvm.org/releases/${LLVM_VERSION}/libcxxabi-${LLVM_VERSION}.src.tar.xz" + - CMAKE_URL="https://cmake.org/files/v3.6/cmake-3.6.2-Linux-x86_64.tar.gz" before_install: - - wget http://llvm.org/releases/$LLVM_VERSION/clang+llvm-$LLVM_VERSION-x86_64-linux-gnu-ubuntu-14.04.tar.xz -O $LLVM_ARCHIVE_PATH - - mkdir $HOME/clang-$LLVM_VERSION - - tar xf $LLVM_ARCHIVE_PATH -C $HOME/clang-$LLVM_VERSION --strip-components 1 - - export PATH=$HOME/clang-$LLVM_VERSION/bin:$PATH - sudo apt-add-repository -y "ppa:george-edison55/george-edison" - - sudo sed -i "s/trusty/wily/g" /etc/apt/sources.list - sudo apt-get -qq update - - sudo apt-get install -y cmake cmake-data libxcb1-dev python-xcbgen xcb-proto libboost-dev libiw-dev libasound2-dev libmpdclient-dev + - sudo apt-get install -y cmake cmake-data libxcb1-dev libxcb-util0-dev libxcb-randr0-dev libxcb-ewmh-dev libxcb-icccm4-dev xcb-proto libboost-dev libiw-dev libasound2-dev libmpdclient-dev libjsoncpp-dev libsigc++-2.0-0c2a libsigc++-2.0-dev libfreetype6-dev + +install: + - DEPS_DIR="${TRAVIS_BUILD_DIR}/deps" + - LLVM_ROOT="${DEPS_DIR}/llvm-${LLVM_VERSION}" + - | + mkdir -p "${DEPS_DIR}" && cd "${DEPS_DIR}" + if [[ -z "$(ls -A ${DEPS_DIR}/cmake/bin 2>/dev/null)" ]]; then + mkdir -p cmake && travis_retry wget --no-check-certificate --quiet -O - "${CMAKE_URL}" | tar --strip-components=1 -xz -C cmake + fi + export PATH="${DEPS_DIR}/cmake/bin:${PATH}" + if [[ -z "$(ls -A ${LLVM_ROOT}/install/include 2>/dev/null)" ]]; then + mkdir -p "${LLVM_ROOT}" "${LLVM_ROOT}/build" "${LLVM_ROOT}/projects/libcxx" "${LLVM_ROOT}/projects/libcxxabi" + travis_retry wget --quiet -O - "${LLVM_URL}" | tar --strip-components=1 -xJ -C "${LLVM_ROOT}" + travis_retry wget --quiet -O - "${LIBCXX_URL}" | tar --strip-components=1 -xJ -C "${LLVM_ROOT}/projects/libcxx" + travis_retry wget --quiet -O - "${LIBCXXABI_URL}" | tar --strip-components=1 -xJ -C "${LLVM_ROOT}/projects/libcxxabi" + (cd "${LLVM_ROOT}/build" && cmake .. -DCMAKE_CXX_COMPILER="${CXX}" -DCMAKE_C_COMPILER="${CC}" -DCMAKE_INSTALL_PREFIX="${LLVM_ROOT}/install" -DCMAKE_BUILD_TYPE="${BUILD_TYPE}") + (cd "${LLVM_ROOT}/build/projects/libcxx" && make install) + (cd "${LLVM_ROOT}/build/projects/libcxxabi" && make install) + export CXXFLAGS="-I ${LLVM_ROOT}/install/include/c++/v1" + export LDFLAGS="-L ${LLVM_ROOT}/install/lib -lc++ -lc++abi" + export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${LLVM_ROOT}/install/lib" + fi before_script: - - if [ "$CXX" = "clang++" ]; then export CPPFLAGS="-I $HOME/clang-$LLVM_VERSION/include/c++/v1" CXXFLAGS="-Qunused-arguments"; fi + - export PYTHONPATH="/usr/lib/python2.7/dist-packages:${PYTHONPATH}" + - if [ "${CXX}" = "clang++" ]; then export CXX="clang++-3.8" CC="clang-3.8" CXXFLAGS="${CXXFLAGS} -Qunused-arguments"; fi + - if [ "${CXX}" = "g++" ]; then export CXX="g++-5" CC="gcc-5"; fi + - eval "${CXX} --version" + - eval "${CC} --version" - cmake --version - - eval "$CXX --version" - - eval "$CC --version" - - mkdir build - - cd build - - cmake -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" -DCMAKE_CXX_FLAGS="$CXXFLAGS" .. + - mkdir -p "${TRAVIS_BUILD_DIR}/build" + - cd "${TRAVIS_BUILD_DIR}/build" + - cmake -DCMAKE_C_COMPILER="${CC}" -DCMAKE_CXX_COMPILER="${CXX}" -DCMAKE_CXX_FLAGS="${CXXFLAGS}" -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" .. script: make diff --git a/.ycm_extra_conf.py b/.ycm_extra_conf.py index 12a1e8aa..1255cadf 100644 --- a/.ycm_extra_conf.py +++ b/.ycm_extra_conf.py @@ -48,8 +48,21 @@ def DirectoryOfThisScript(): flags.append('-I'+ DirectoryOfThisScript() +'/src') flags.append('-I'+ DirectoryOfThisScript() +'/include') +flags.append('-I'+ DirectoryOfThisScript() +'/lib/gsl') +flags.append('-I'+ DirectoryOfThisScript() +'/lib/cpp_freetype/include') flags.append('-I'+ DirectoryOfThisScript() +'/lib/i3ipcpp/include') flags.append('-I'+ DirectoryOfThisScript() +'/lib/xpp/include') +flags.append('-I'+ DirectoryOfThisScript() +'/lib/lemonbar/include') +flags.append('-I'+ DirectoryOfThisScript() +'/lib/fastdelegate/include') +flags.append('-I'+ DirectoryOfThisScript() +'/lib/boost/include') +flags.append('-I'+ DirectoryOfThisScript() +'/tests') +flags.append('-I/usr/include/freetype2') +flags.append('-I/usr/include/pango-1.0') +flags.append('-I/usr/include/cairomm-1.0') +flags.append('-I/usr/include/pangomm-1.4') +flags.append('-I/usr/include/glibmm-2.4') +flags.append('-I/usr/lib/cairomm-1.0/include') +flags.append('-I/usr/include') def MakeRelativePathsInFlagsAbsolute( flags, working_directory ): if not working_directory: diff --git a/CMakeLists.txt b/CMakeLists.txt index a81faf2b..75d8943b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,39 +1,57 @@ # # Build configuration # -cmake_minimum_required(VERSION 3.1) -project(lemonbuddy C CXX) +cmake_minimum_required(VERSION 3.1 FATAL_ERROR) +project(lemonbuddy CXX) -# Include the local cmake modules -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/cmake/modules) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14 -Wall -Wno-unused-parameter -Wno-unused-local-typedefs -Wno-c99-extensions") +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG -O0 -g2") +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2") + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +SET(CMAKE_EXPORT_COMPILE_COMMANDS ON) if(NOT CMAKE_BUILD_TYPE) message(STATUS "No build type specified; using Release") set(CMAKE_BUILD_TYPE "Release") endif() -string(ASCII 27 ANSI) +include(cmake/utils.cmake) +include(cmake/clang-cpp-tools.cmake) -# -# Internal values and switches -# +# Figure out default option values {{{ -# Keep track if the i3 option is explicitly defined -if(ENABLE_I3) - set(ENABLE_I3_NODEF ON) +find_package(ALSA QUIET) +find_package(Libiw QUIET) +find_package(LibMPDClient QUIET) +find_program(CCACHE_BINARY ccache) +find_program(I3_BINARY i3) +if(CCACHE_BINARY) + set(CCACHE_FOUND ON) +endif() +if(I3_BINARY) + set(I3_FOUND ON) endif() -option(ENABLE_CCACHE "Enable ccache support" ON) -option(ENABLE_ALSA "Enable alsa support" ON) -option(ENABLE_I3 "Enable i3 support" ON) -option(ENABLE_MPD "Enable mpd support" ON) -option(ENABLE_NETWORK "Enable network support" ON) +# }}} +# Project settings {{{ + +option(ENABLE_CCACHE "Enable ccache support" ${CCACHE_FOUND}) +option(ENABLE_CCACHE "Enable ccache support" ${CCACHE_FOUND}) +option(ENABLE_ALSA "Enable alsa support" ${ALSA_FOUND}) +option(ENABLE_I3 "Enable i3 support" ${I3_FOUND}) +option(ENABLE_MPD "Enable mpd support" ${LIBMPDCLIENT_FOUND}) +option(ENABLE_NETWORK "Enable network support" ${LIBIW_FOUND}) if(ENABLE_ALSA) set(SETTING_ALSA_SOUNDCARD "default" CACHE STRING "Name of the ALSA soundcard driver") endif() - set(SETTING_CONNECTION_TEST_IP "8.8.8.8" CACHE STRING "Address to ping when testing network connection") set(SETTING_PATH_BACKLIGHT_VAL "/sys/class/backlight/%card%/brightness" @@ -54,154 +72,105 @@ set(SETTING_PATH_MEMORY_INFO "/proc/meminfo" CACHE STRING "Path to file containing memory info") if(ENABLE_CCACHE) - find_program(CCACHE_FOUND "ccache") - if(CCACHE_FOUND) - set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "ccache") - set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK "ccache") - endif() + set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "ccache") + set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK "ccache") endif() -# -# Locate and insert libs -# -find_package("Boost" REQUIRED) -find_package("Threads" REQUIRED) +# }}} +# Locate dependencies {{{ -set(PROJECT_INCL_DIRS "${PROJECT_SOURCE_DIR}/include" - ${BOOST_INCLUDE_DIR}) -set(PROJECT_LINK_LIBS - ${BOOST_LIBRARIES} - ${CMAKE_THREAD_LIBS_INIT}) +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Boost REQUIRED) +find_package(Threads REQUIRED) +find_package(Freetype REQUIRED Freetype2) +find_package(PkgConfig) +find_package(X11 REQUIRED COMPONENTS Xft) +find_package(X11_XCB REQUIRED) -if(ENABLE_I3) - find_program(I3_EXECUTABLE "i3") - if(NOT I3_EXECUTABLE) - if(NOT ENABLE_I3_NODEF) - message(WARNING "${ANSI}[41;1mDisabling \"i3 module\" support (prerequisites failed)${ANSI}[0m") - set(ENABLE_I3 OFF) - endif() - endif() - if(ENABLE_I3) - add_subdirectory("${PROJECT_SOURCE_DIR}/lib/i3ipcpp" EXCLUDE_FROM_ALL) - set(PROJECT_INCL_DIRS - ${PROJECT_INCL_DIRS} - ${SIGCPP_INCLUDE_DIRS} - ${I3IPCPP_INCLUDE_DIRS}) - set(PROJECT_LINK_LIBS - ${PROJECT_LINK_LIBS} - ${I3IPCPP_LIBRARIES}) - endif() -endif() +pkg_check_modules(FONTCONFIG REQUIRED fontconfig) + +link_libraries(${X11_Xft_LIB}) +link_libraries(${X11_XCB_LIB}) +link_libraries(${BOOST_LIBRARIES}) +link_libraries(${CMAKE_THREAD_LIBS_INIT}) if(ENABLE_ALSA) - find_package("ALSA") - if(ALSA_FOUND) - set(PROJECT_INCL_DIRS ${PROJECT_INCL_DIRS} ${ALSA_INCLUDE_DIR}) - set(PROJECT_LINK_LIBS ${PROJECT_LINK_LIBS} ${ALSA_LIBRARY}) - else(ALSA_FOUND) - message(WARNING "${ANSI}[41;1mDisabling \"volume module\" support (prerequisites failed)${ANSI}[0m") - set(ENABLE_ALSA OFF) - endif() + find_package(ALSA REQUIRED) endif() - if(ENABLE_MPD) - find_package("LibMPDClient") - if(LIBMPDCLIENT_FOUND) - set(PROJECT_INCL_DIRS ${PROJECT_INCL_DIRS} ${LIBMPDCLIENT_INCLUDE_DIR}) - set(PROJECT_LINK_LIBS ${PROJECT_LINK_LIBS} ${LIBMPDCLIENT_LIBRARY}) - else(LIBMPDCLIENT_FOUND) - message(WARNING "${ANSI}[41;1mDisabling \"mpd module\" support (prerequisites failed)${ANSI}[0m") - set(ENABLE_MPD OFF) - endif() + find_package(LibMPDClient REQUIRED) endif() - if(ENABLE_NETWORK) - find_package("Libiw") - if(LIBIW_FOUND) - set(PROJECT_INCL_DIRS ${PROJECT_INCL_DIRS} ${LIBIW_INCLUDE_DIR}) - set(PROJECT_LINK_LIBS ${PROJECT_LINK_LIBS} ${LIBIW_LIBRARY}) - else(LIBIW_FOUND) - message(WARNING "${ANSI}[41;1mDisabling \"network module\" support (prerequisites failed)${ANSI}[0m") - set(ENABLE_NETWORK OFF) - endif() + find_package(Libiw REQUIRED) +endif() +if(ENABLE_I3) + add_subdirectory(${PROJECT_SOURCE_DIR}/lib/i3ipcpp EXCLUDE_FROM_ALL) endif() -# -# Load the xpp library -# +include_directories( + ${BOOST_INCLUDE_DIR} + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_SOURCE_DIR}/lib/boost/include + ${PROJECT_SOURCE_DIR}/lib/fastdelegate/include) + set(XCB_PROTOS xproto randr) -add_subdirectory("${PROJECT_SOURCE_DIR}/lib/xpp") -set(PROJECT_INCL_DIRS ${PROJECT_INCL_DIRS} ${XPP_INCLUDE_DIRS}) -set(PROJECT_LINK_LIBS ${PROJECT_LINK_LIBS} ${XPP_LIBRARIES}) +add_subdirectory(${PROJECT_SOURCE_DIR}/lib/xpp) -# -# Execute versioning script -# -execute_process(COMMAND ./version.sh WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_QUIET ERROR_QUIET) +# }}} +# Build source tree {{{ + +add_subdirectory(${PROJECT_SOURCE_DIR}/man) +add_subdirectory(${PROJECT_SOURCE_DIR}/src ${PROJECT_BINARY_DIR}/bin) +add_subdirectory(${PROJECT_SOURCE_DIR}/examples ${PROJECT_BINARY_DIR}/examples) +add_subdirectory(${PROJECT_SOURCE_DIR}/tests ${PROJECT_BINARY_DIR}/tests EXCLUDE_FROM_ALL) + +# }}} +# Build summary {{{ -# -# Install executable and wrapper -# message(STATUS "---------------------------") message(STATUS " Build type: ${CMAKE_BUILD_TYPE}") message(STATUS " Compiler C: ${CMAKE_C_COMPILER}") message(STATUS " Compiler C++: ${CMAKE_CXX_COMPILER}") +message(STATUS " Compiler flags: ${CMAKE_CXX_FLAGS}") + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + message(STATUS " + debug flags:: ${CMAKE_CXX_FLAGS_DEBUG}") +elseif(CMAKE_BUILD_TYPE STREQUAL "Release") + message(STATUS " + release flags:: ${CMAKE_CXX_FLAGS_RELEASE}") +elseif(CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") + message(STATUS " + minsizerel flags:: ${CMAKE_CXX_FLAGS_MINSIZEREL}") +elseif(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + message(STATUS " + relwithdebinfo flags:: ${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") +endif() message(STATUS "---------------------------") + message(STATUS " Enable ccache support ${ENABLE_CCACHE}") message(STATUS " Enable alsa support ${ENABLE_ALSA}") message(STATUS " Enable i3 support ${ENABLE_I3}") message(STATUS " Enable mpd support ${ENABLE_MPD}") message(STATUS " Enable network support ${ENABLE_NETWORK}") + +if(DISABLE_MODULES) + message(STATUS " Disable modules ON") +endif() +if(DISABLE_TRAY) + message(STATUS " Disable systray ON") +endif() +if(DISABLE_DRAW) + message(STATUS " Disable drawing ON") +endif() + message(STATUS "---------------------------") -add_subdirectory("${PROJECT_SOURCE_DIR}/man") -add_subdirectory("${PROJECT_SOURCE_DIR}/src" EXCLUDE_FROM_ALL) -link_directories(${PROJECT_LINK_DIRS}) -include_directories(${PROJECT_BINARY_DIR} ${PROJECT_SOURCE_DIR} ${PROJECT_INCL_DIRS}) -link_libraries(${PROJECT_LINK_LIBS}) +# }}} +# Uninstall target {{{ -add_executable(${PROJECT_NAME} ${FILES} - "examples/config" - "examples/config.bspwm" - "examples/config.i3") - -set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 14) -set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD_REQUIRED ON) - -target_compile_options(${PROJECT_NAME} PRIVATE - -Wall -Wextra -Wpedantic -Wno-unused-parameter - $<$:-g3 -DDEBUG> - $<$:-O3 -Wno-unused-variable>) - -target_link_libraries(${PROJECT_NAME} ${PROJECT_LINK_LIBS}) - -configure_file("${CMAKE_SOURCE_DIR}/include/config.hpp.cmake" "${CMAKE_SOURCE_DIR}/include/config.hpp" ESCAPE_QUOTES @ONLY) - -install(TARGETS ${PROJECT_NAME} - DESTINATION "bin" - COMPONENT "binaries") -install(PROGRAMS "${CMAKE_CURRENT_SOURCE_DIR}/scripts/lemonbuddy_wrapper" - DESTINATION "bin" COMPONENT "binaries") -install(PROGRAMS "${CMAKE_CURRENT_SOURCE_DIR}/scripts/lemonbuddy_terminate" - DESTINATION "bin" COMPONENT "binaries") - -install(FILES "examples/config" - DESTINATION "share/examples/${PROJECT_NAME}" - COMPONENT "config") -install(FILES "examples/config.bspwm" - DESTINATION "share/examples/${PROJECT_NAME}" - COMPONENT "config") -install(FILES "examples/config.i3" - DESTINATION "share/examples/${PROJECT_NAME}" - COMPONENT "config") - -# -# Uninstall target -# configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/uninstall.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/cmake/uninstall.cmake" + ${PROJECT_SOURCE_DIR}/cmake/uninstall.cmake.in + ${PROJECT_BINARY_DIR}/cmake/uninstall.cmake IMMEDIATE @ONLY) add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} - -P "${CMAKE_CURRENT_BINARY_DIR}/cmake/uninstall.cmake") + -P ${PROJECT_BINARY_DIR}/cmake/uninstall.cmake) + +# }}} diff --git a/README.md b/README.md index 1b7cfe84..a9730658 100644 --- a/README.md +++ b/README.md @@ -100,13 +100,13 @@ available for more people. ### Dependencies -A C++ compiler with C++14 support. For example [`clang`](http://clang.llvm.org/get_started.html). +A compiler with c++14 support. For example [`clang`](http://clang.llvm.org/get_started.html). -- lemonbar with xft support _(personally I use [this fork](https://github.com/osense/bar))_ - cmake - boost - libxcb - xcb-proto +- freetype2 Optional dependencies for module support: @@ -117,8 +117,8 @@ Optional dependencies for module support: ~~~ sh $ pacman -S cmake boost libxcb xcb-proto wireless_tools alsa-lib libmpdclient jsoncpp libsigc++ -$ xbps-install cmake boost-devel libxcb-devel alsa-lib-devel i3-devel libmpdclient-devel jsoncpp-devel libsigc++-devel wireless_tools-devel -$ apt-get install cmake libxcb1-dev xcb-proto python-xcbgen libboost-dev libiw-dev libasound2-dev libmpdclient-dev libjsoncpp-dev libsigc++-dev +$ xbps-install cmake boost-devel libxcb-devel alsa-lib-devel i3-devel libmpdclient-devel jsoncpp-devel freetype-devel libsigc++-devel wireless_tools-devel +$ apt-get install cmake libxcb1-dev xcb-proto python-xcbgen libboost-dev libiw-dev libasound2-dev libmpdclient-dev libjsoncpp-dev libsigc++-2.0-dev libfreetype6-dev ~~~ @@ -127,7 +127,7 @@ $ apt-get install cmake libxcb1-dev xcb-proto python-xcbgen libboost-dev libiw-d Please [report any problems](https://github.com/jaagr/lemonbuddy/issues/new) you run into when building the project. It helps alot. ~~~ sh - $ git clone --branch 1.4.4 --recursive https://github.com/jaagr/lemonbuddy + $ git clone --branch 1.4.6 --recursive https://github.com/jaagr/lemonbuddy $ mkdir lemonbuddy/build $ cd lemonbuddy/build $ cmake .. @@ -281,7 +281,7 @@ The configuration syntax is based on the `ini` file format. ; ; The rest of the drawtypes follow the same pattern. ; - ; label-NAME[-(foreground|background|(under|over)line|font|padding)] = ? + ; label-NAME[-(foreground|background|(under|over)line|font|padding|maxlen|ellipsis)] = ? ; icon-NAME[-(foreground|background|(under|over)line|font|padding)] = ? ; ramp-NAME-[0-9]+[-(foreground|background|(under|over)line|font|padding)] = ? ; animation-NAME-[0-9]+[-(foreground|background|(under|over)line|font|padding)] = ? @@ -300,6 +300,10 @@ The configuration syntax is based on the `ini` file format. format-offline = format-offline-offset = -8 + ; Cap the song label without trailing ellipsis + label-song-maxlen = 30 + label-song-ellipsis = false + ; By only specifying alpha value, it will be applied to the bar's default foreground label-time-foreground = #66 @@ -324,8 +328,8 @@ The configuration syntax is based on the `ini` file format. ; Limit the amount of events sent to lemonbar within a set timeframe: ; - "Allow updates within of time" ; Default values: - throttle_limit = 5 - throttle_ms = 50 + throttle_limit = 3 + throttle_ms = 60 ~~~ @@ -361,6 +365,19 @@ The configuration syntax is based on the `ini` file format. foreground = #eefafafa linecolor = ${bar/example.background} + ; Borders + ; Size to be used for all borders + border-size = 2 + ; Color to be used for all borders + border-color = #ff9900 + ; Per-border values + ;border-top = 1 + ;border-top-color = #ff9900 + ;border-bottom = 2 + ;border-bottom-color = #5d00ff + ;border-left = 3 + ;border-right-color = #ff0059 + ; Amount of spaces to add at the start/end of the whole bar padding_left = 5 padding_right = 2 @@ -378,10 +395,6 @@ The configuration syntax is based on the `ini` file format. ; The separator will be inserted between the output of each module separator = | - ; This value is used by Lemonbar and it specifies the clickable - ; areas available -> %{A:action:}...%{A} - clickareas = 30 - ; Value to be used to set the WM_NAME atom ; This defaults to "lemonbuddy-[BAR]_[MONITOR]" wm_name = mybar @@ -393,6 +406,14 @@ The configuration syntax is based on the `ini` file format. modules-left = cpu ram modules-center = label modules-right = clock + + ; Position of the tray container + ; If undefined, tray support will be disabled + ; + ; Available positions: + ; left + ; right + tray-position = right ~~~ ### Modules @@ -958,6 +979,12 @@ See [the bspwm module](#module-internalbspwm) for details on `label-dimmed`. ramp-volume-0 = 🔈 ramp-volume-1 = 🔉 ramp-volume-2 = 🔊 + + ; If defined, it will replace when + ; headphones are plugged in to `headphone_control_numid` + ; If undefined, will be used for both + ramp-headphones-0 =  + ramp-headphones-1 =  ~~~ @@ -1026,6 +1053,14 @@ See [the bspwm module](#module-internalbspwm) for details on `label-dimmed`. ; Will be ignored if `tail = true` ; Default: 1 interval = 90 + + ; Limit the length of the output string + ; Default: 0 + maxlen = 20 + + ; Add trailing ellipsis when truncating the string + ; Default: true + ellipsis = true ~~~ ##### Extra formatting (example) @@ -1061,6 +1096,7 @@ See [the bspwm module](#module-internalbspwm) for details on `label-dimmed`. type = custom/script exec = xtitle -s tail = true + maxlen = 25 ~~~ diff --git a/TODO b/TODO new file mode 100644 index 00000000..b43693de --- /dev/null +++ b/TODO @@ -0,0 +1,6 @@ +task: sandbox modules +task: rewrite README for 2.0 +task: rewrite i3 module +fix: hide mpd controls if playlist is empty +bug: allow empty formats +bug: ntpd update crash diff --git a/cmake/clang-cpp-tools.cmake b/cmake/clang-cpp-tools.cmake new file mode 100644 index 00000000..5121f1b9 --- /dev/null +++ b/cmake/clang-cpp-tools.cmake @@ -0,0 +1,22 @@ +# +# Additional targets to perform clang-format/clang-tidy +# + +file(GLOB_RECURSE SOURCE_FILES *.[chi]pp) + +# Add clang-format target if executable is found +# -------------------------------------------------- +find_program(CLANG_FORMAT "clang-format") +if(CLANG_FORMAT) + add_custom_target(clang-format COMMAND + ${CLANG_FORMAT} -i -style=file ${SOURCE_FILES}) +endif() + +# Add clang-tidy target if executable is found +# -------------------------------------------------- +find_program(CLANG_TIDY "clang-tidy") +if(CLANG_TIDY) + add_custom_target(clang-tidy COMMAND ${CLANG_TIDY} + ${SOURCE_FILES} -config='' -- -std=c++11 + ${INCLUDE_DIRECTORIES}) +endif() diff --git a/cmake/modules/FindCppUnit.cmake b/cmake/modules/FindCppUnit.cmake new file mode 100644 index 00000000..4ee00940 --- /dev/null +++ b/cmake/modules/FindCppUnit.cmake @@ -0,0 +1,54 @@ +# +# Find the CppUnit includes and library +# +# This module defines +# CPPUNIT_INCLUDE_DIR, where to find tiff.h, etc. +# CPPUNIT_LIBRARIES, the libraries to link against to use CppUnit. +# CPPUNIT_FOUND, If false, do not try to use CppUnit. + +# also defined, but not for general use are +# CPPUNIT_LIBRARY, where to find the CppUnit library. +# CPPUNIT_DEBUG_LIBRARY, where to find the CppUnit library in debug +# mode. + +SET(CPPUNIT_FOUND "NO") + +FIND_PATH(CPPUNIT_INCLUDE_DIR cppunit/TestCase.h /usr/local/include /usr/include) + +# With Win32, important to have both +IF(WIN32) + FIND_LIBRARY(CPPUNIT_LIBRARY cppunit + ${CPPUNIT_INCLUDE_DIR}/../lib + /usr/local/lib + /usr/lib) + FIND_LIBRARY(CPPUNIT_DEBUG_LIBRARY cppunitd + ${CPPUNIT_INCLUDE_DIR}/../lib + /usr/local/lib + /usr/lib) +ELSE(WIN32) + # On unix system, debug and release have the same name + FIND_LIBRARY(CPPUNIT_LIBRARY cppunit + ${CPPUNIT_INCLUDE_DIR}/../lib + /usr/local/lib + /usr/lib) + FIND_LIBRARY(CPPUNIT_DEBUG_LIBRARY cppunit + ${CPPUNIT_INCLUDE_DIR}/../lib + /usr/local/lib + /usr/lib) +ENDIF(WIN32) + +IF(CPPUNIT_INCLUDE_DIR) + IF(CPPUNIT_LIBRARY) + SET(CPPUNIT_FOUND "YES") + SET(CPPUNIT_LIBRARIES ${CPPUNIT_LIBRARY} ${CMAKE_DL_LIBS}) + SET(CPPUNIT_DEBUG_LIBRARIES ${CPPUNIT_DEBUG_LIBRARY} ${CMAKE_DL_LIBS}) + ELSE (CPPUNIT_LIBRARY) + IF (CPPUNIT_FIND_REQUIRED) + MESSAGE(SEND_ERROR "Could not find library CppUnit.") + ENDIF (CPPUNIT_FIND_REQUIRED) + ENDIF(CPPUNIT_LIBRARY) +ELSE(CPPUNIT_INCLUDE_DIR) + IF (CPPUNIT_FIND_REQUIRED) + MESSAGE(SEND_ERROR "Could not find library CppUnit.") + ENDIF(CPPUNIT_FIND_REQUIRED) +ENDIF(CPPUNIT_INCLUDE_DIR) diff --git a/cmake/utils.cmake b/cmake/utils.cmake new file mode 100644 index 00000000..f5a6298e --- /dev/null +++ b/cmake/utils.cmake @@ -0,0 +1,152 @@ +# +# Collection of cmake utility functions +# + +# message_colored : Outputs a colorized message {{{ + +function(message_colored message_level text color) + string(ASCII 27 esc) + message(${message_level} "${esc}[${color}m${text}${esc}[0m") +endfunction() + +# }}} +# make_executable : Builds an executable target {{{ + +function(make_executable target_name) + set(zero_value_args) + set(one_value_args PACKAGE) + set(multi_value_args SOURCES INCLUDE_DIRS PKG_DEPENDS CMAKE_DEPENDS TARGET_DEPENDS RAW_DEPENDS) + + cmake_parse_arguments(BIN + "${zero_value_args}" "${one_value_args}" + "${multi_value_args}" ${ARGN}) + + # add defined INCLUDE_DIRS + include_directories(${BIN_INCLUDE_DIRS}) + + # add INCLUDE_DIRS for all external dependencies + foreach(DEP ${BIN_TARGET_DEPENDS} ${BIN_PKG_DEPENDS} ${BIN_CMAKE_DEPENDS}) + string(TOUPPER ${DEP} DEP) + include_directories(${${DEP}_INCLUDE_DIRS}) + include_directories(${${DEP}_INCLUDEDIR}) + endforeach() + + # create target + add_executable(${target_name} ${BIN_SOURCES}) + + # set the output file basename the same for static and shared + set_target_properties(${target_name} + PROPERTIES OUTPUT_NAME ${target_name}) + + # link libraries from pkg-config imports + foreach(DEP ${BIN_PKG_DEPENDS}) + string(TOUPPER ${DEP} DEP) + target_link_libraries(${target_name} ${${DEP}_LDFLAGS}) + endforeach() + + # link libraries from cmake imports + foreach(DEP ${BIN_CMAKE_DEPENDS}) + string(TOUPPER ${DEP} DEP) + target_link_libraries(${target_name} ${${DEP}_LIB} + ${${DEP}_LIBRARY} + ${${DEP}_LIBRARIES}) + endforeach() + + # link libraries that are build as part of this project + target_link_libraries(${target_name} ${BIN_TARGET_DEPENDS} + ${BIN_RAW_DEPENDS}) + + # install targets + install(TARGETS ${target_name} + RUNTIME DESTINATION bin + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib) +endfunction() + +# }}} +# make_library : Builds a library target {{{ + +function(make_library target_name) + set(zero_value_args SHARED STATIC) + set(one_value_args PACKAGE HEADER_INSTALL_DIR) + set(multi_value_args SOURCES HEADERS INCLUDE_DIRS PKG_DEPENDS CMAKE_DEPENDS TARGET_DEPENDS RAW_DEPENDS) + + cmake_parse_arguments(LIB + "${zero_value_args}" "${one_value_args}" + "${multi_value_args}" ${ARGN}) + + # make the header paths absolute + foreach(HEADER ${LIB_HEADERS}) + set(LIB_HEADERS_ABS ${LIB_HEADERS_ABS} ${PROJECT_SOURCE_DIR}/include/${HEADER}) + endforeach() + + # add defined INCLUDE_DIRS + foreach(DIR ${LIB_INCLUDE_DIRS}) + string(TOUPPER ${DIR} DIR) + include_directories(${DIR}) + include_directories(${${DIR}_INCLUDE_DIRS}) + endforeach() + + # add INCLUDE_DIRS for all external dependencies + foreach(DEP ${LIB_TARGET_DEPENDS} ${LIB_PKG_DEPENDS} ${LIB_CMAKE_DEPENDS}) + string(TOUPPER ${DEP} DEP) + include_directories(${${DEP}_INCLUDE_DIRS} ${${DEP}_INCLUDEDIRS}) + endforeach() + + if(LIB_SHARED) + list(APPEND library_targets ${target_name}_shared) + endif() + if(LIB_STATIC) + list(APPEND library_targets ${target_name}_static) + endif() + + foreach(library_target_name ${library_targets}) + message(STATUS "${library_target_name}") + add_library(${library_target_name} ${LIB_HEADERS_ABS} ${LIB_SOURCES}) + + # link libraries from pkg-config imports + foreach(DEP ${LIB_PKG_DEPENDS}) + string(TOUPPER ${DEP} DEP) + target_link_libraries(${library_target_name} ${${DEP}_LDFLAGS}) + endforeach() + + # link libraries from cmake imports + foreach(DEP ${LIB_CMAKE_DEPENDS}) + string(TOUPPER ${DEP} DEP) + target_link_libraries(${library_target_name} ${${DEP}_LIB} + ${${DEP}_LIBRARY} + ${${DEP}_LIBRARIES}) + endforeach() + + # link libraries that are build as part of this project + foreach(DEP ${LIB_TARGET_DEPENDS}) + string(TOUPPER ${DEP} DEP) + if(LIB_BUILD_SHARED) + target_link_libraries(${library_target_name} ${DEP}_shared) + endif() + if(LIB_BUILD_STATIC) + target_link_libraries(${library_target_name} ${DEP}_static) + endif() + endforeach() + + if(${LIB_RAW_DEPENDS}) + if(LIB_BUILD_STATIC) + target_link_libraries(${library_target_name} ${LIB_RAW_DEPENDS}) + endif() + endif() + + # set the output file basename + set_target_properties(${library_target_name} PROPERTIES OUTPUT_NAME ${target_name}) + + # install headers + install(FILES ${LIBRARY_HEADERS} DESTINATION include/${LIB_HEADERS_ABS}) + + # install targets + install(TARGETS ${LIBRARY_TARGETS} + RUNTIME DESTINATION bin + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib) + endforeach() +endfunction() + +# }}} diff --git a/config b/config index f7c73cd1..619f42a2 100644 --- a/config +++ b/config @@ -203,19 +203,19 @@ full_at = 99 ; ; ; -format-charging = Charging +format-charging = ; Available tags: ; (default) ; ; -format-discharging = Discharging +format-discharging = ; Available tags: ; (default) ; ; -format-full = Fully charged +format-full = ; Available tokens: ; %percentage% (default) diff --git a/contrib/lemonbar-sm-git b/contrib/lemonbar-sm-git deleted file mode 160000 index 8ed285ec..00000000 --- a/contrib/lemonbar-sm-git +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8ed285ec2289761e6585090724f73093d62f290f diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 00000000..e92328f3 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,13 @@ +install(FILES config + DESTINATION share/examples/lemonbuddy + COMPONENT config) + +install(FILES config.bspwm + DESTINATION share/examples/lemonbuddy + COMPONENT config) + +if(ENABLE_I3) + install(FILES config.i3 + DESTINATION share/examples/lemonbuddy + COMPONENT config) +endif() diff --git a/examples/config b/examples/config index 4ba35d55..37498946 100644 --- a/examples/config +++ b/examples/config @@ -15,7 +15,7 @@ ;throttle_ms = 50 [bar/example] -;monitor = eDP1 +monitor = eDP-1 bottom = true dock = false @@ -45,7 +45,9 @@ font-0 = sans:size=8;0 font-1 = font awesome:size=10:weight=heavy;0 modules-left = label -modules-right = volume cpu ram clock +;modules-center = counter +;modules-right = volume cpu ram clock +modules-right = volume clock [module/label] type = custom/text @@ -97,4 +99,14 @@ format-muted-padding = 2 label-volume = Volume: %percentage% label-muted = Sound is muted +; [module/counter] +; type = custom/script +; exec = echo %counter% +; interval = 1 +; format-background = #393484 +; format-underline = #69d294 +; format-overline = #69d294 +; format-padding = 2 + + ; vim:ft=dosini diff --git a/examples/scrots/bar-full-dracula.png b/examples/scrots/bar-full-dracula.png deleted file mode 100644 index 309b6ea2..00000000 Binary files a/examples/scrots/bar-full-dracula.png and /dev/null differ diff --git a/examples/scrots/bar-full.png b/examples/scrots/bar-full.png deleted file mode 100644 index 21f7437e..00000000 Binary files a/examples/scrots/bar-full.png and /dev/null differ diff --git a/examples/scrots/bar-lower.png b/examples/scrots/bar-lower.png deleted file mode 100644 index c8202a80..00000000 Binary files a/examples/scrots/bar-lower.png and /dev/null differ diff --git a/examples/scrots/bar-upper.png b/examples/scrots/bar-upper.png deleted file mode 100644 index 6bf2f8e8..00000000 Binary files a/examples/scrots/bar-upper.png and /dev/null differ diff --git a/include/adapters/alsa.hpp b/include/adapters/alsa.hpp new file mode 100644 index 00000000..0974c24c --- /dev/null +++ b/include/adapters/alsa.hpp @@ -0,0 +1,247 @@ +#pragma once + +#include +#include +#include + +#include + +#include "common.hpp" +#include "config.hpp" +// #include "utils/threading.hpp" + +LEMONBUDDY_NS + +DEFINE_ERROR(alsa_exception); +DEFINE_CHILD_ERROR(alsa_ctl_interface_error, alsa_exception); +DEFINE_CHILD_ERROR(alsa_mixer_error, alsa_exception); + +// class definition : alsa_ctl_interface {{{ + +template +void throw_exception(string&& message, int error_code) { + const char* snd_error = snd_strerror(error_code); + if (snd_error != nullptr) + message += ": "+ string{snd_error}; + throw T(message.c_str()); +} + +class alsa_ctl_interface { + public: + explicit alsa_ctl_interface(int numid) { + int err = 0; + + snd_ctl_elem_info_alloca(&m_info); + snd_ctl_elem_value_alloca(&m_value); + snd_ctl_elem_id_alloca(&m_id); + + snd_ctl_elem_id_set_numid(m_id, numid); + snd_ctl_elem_info_set_id(m_info, m_id); + + if ((err = snd_ctl_open(&m_ctl, ALSA_SOUNDCARD, SND_CTL_NONBLOCK | SND_CTL_READONLY)) < 0) + throw_exception("Could not open control '"+ string{ALSA_SOUNDCARD} +"'", err); + + if ((err = snd_ctl_elem_info(m_ctl, m_info)) < 0) + throw_exception("Could not get control datal", err); + + snd_ctl_elem_info_get_id(m_info, m_id); + + if ((err = snd_hctl_open(&m_hctl, ALSA_SOUNDCARD, 0)) < 0) + throw_exception("Failed to open hctl", err); + if ((err = snd_hctl_load(m_hctl)) < 0) + throw_exception("Failed to load hctl", err); + if ((m_elem = snd_hctl_find_elem(m_hctl, m_id)) == nullptr) + throw alsa_ctl_interface_error( + "Could not find control with id " + to_string(snd_ctl_elem_id_get_numid(m_id))); + + if ((err = snd_ctl_subscribe_events(m_ctl, 1)) < 0) + throw alsa_ctl_interface_error( + "Could not subscribe to events: " + to_string(snd_ctl_elem_id_get_numid(m_id))); + + // log_trace("Successfully initialized control interface with ID: "+ Intstring(numid)); + } + + ~alsa_ctl_interface() { + // std::lock_guard lck(m_lock); + snd_ctl_close(m_ctl); + snd_hctl_close(m_hctl); + } + + bool wait(int timeout = -1) { + assert(m_ctl); + + // std::lock_guard lck(m_lock); + + int err = 0; + + if ((err = snd_ctl_wait(m_ctl, timeout)) < 0) + throw_exception("Failed to wait for events", err); + + snd_ctl_event_t* event; + snd_ctl_event_alloca(&event); + + if ((err = snd_ctl_read(m_ctl, event)) < 0) + return false; + if (snd_ctl_event_get_type(event) != SND_CTL_EVENT_ELEM) + return false; + + auto mask = snd_ctl_event_elem_get_mask(event); + + return mask & SND_CTL_EVENT_MASK_VALUE; + } + + bool test_device_plugged() { + // std::lock_guard lck(m_lock); + // if (!wait(0)) + // return false; + + assert(m_elem); + assert(m_value); + + int err = 0; + if ((err = snd_hctl_elem_read(m_elem, m_value)) < 0) + throw_exception("Could not read control value", err); + return snd_ctl_elem_value_get_boolean(m_value, 0); + } + + void process_events() {} + + private: + // threading_util::spin_lock m_lock; + + snd_hctl_t* m_hctl = nullptr; + snd_hctl_elem_t* m_elem = nullptr; + + snd_ctl_t* m_ctl = nullptr; + snd_ctl_elem_info_t* m_info = nullptr; + snd_ctl_elem_value_t* m_value = nullptr; + snd_ctl_elem_id_t* m_id = nullptr; +}; + +// }}} +// class definition : alsa_mixer {{{ + +class alsa_mixer { + public: + explicit alsa_mixer(string mixer_control_name) { + snd_mixer_selem_id_t* mixer_id; + + snd_mixer_selem_id_alloca(&mixer_id); + + int err = 0; + + if ((err = snd_mixer_open(&m_hardwaremixer, 1)) < 0) + throw_exception("Failed to open hardware mixer", err); + if ((err = snd_mixer_attach(m_hardwaremixer, ALSA_SOUNDCARD)) < 0) + throw_exception("Failed to attach hardware mixer control", err); + if ((err = snd_mixer_selem_register(m_hardwaremixer, nullptr, nullptr)) < 0) + throw_exception("Failed to register simple mixer element", err); + if ((err = snd_mixer_load(m_hardwaremixer)) < 0) + throw_exception("Failed to load mixer", err); + + snd_mixer_selem_id_set_index(mixer_id, 0); + snd_mixer_selem_id_set_name(mixer_id, mixer_control_name.c_str()); + + if ((m_mixerelement = snd_mixer_find_selem(m_hardwaremixer, mixer_id)) == nullptr) + throw alsa_mixer_error("Cannot find simple element"); + + // log_trace("Successfully initialized mixer: "+ mixer_control_name); + } + + ~alsa_mixer() { + // std::lock_guard lck(m_lock); + snd_mixer_elem_remove(m_mixerelement); + snd_mixer_detach(m_hardwaremixer, ALSA_SOUNDCARD); + snd_mixer_close(m_hardwaremixer); + } + + bool wait(int timeout = -1) { + assert(m_hardwaremixer); + + // std::lock_guard lck(m_lock); + + int err = 0; + + if ((err = snd_mixer_wait(m_hardwaremixer, timeout)) < 0) + throw_exception("Failed to wait for events", err); + + return process_events() > 0; + } + + int process_events() { + int num_events = snd_mixer_handle_events(m_hardwaremixer); + + if (num_events < 0) + throw_exception("Failed to process pending events", num_events); + + return num_events; + } + + int get_volume() { + // std::lock_guard lck(m_lock); + long chan_n = 0, vol_total = 0, vol, vol_min, vol_max; + + snd_mixer_selem_get_playback_volume_range(m_mixerelement, &vol_min, &vol_max); + + for (int i = 0; i < SND_MIXER_SCHN_LAST; i++) { + if (snd_mixer_selem_has_playback_channel(m_mixerelement, (snd_mixer_selem_channel_id_t)i)) { + snd_mixer_selem_get_playback_volume(m_mixerelement, (snd_mixer_selem_channel_id_t)i, &vol); + vol_total += vol; + chan_n++; + } + } + + return 100.0f * (vol_total / chan_n) / vol_max + 0.5f; + } + + void set_volume(float percentage) { + if (is_muted()) + return; + + // std::lock_guard lck(m_lock); + + long vol_min, vol_max; + + snd_mixer_selem_get_playback_volume_range(m_mixerelement, &vol_min, &vol_max); + snd_mixer_selem_set_playback_volume_all(m_mixerelement, vol_max * percentage / 100); + } + + void set_mute(bool mode) { + // std::lock_guard lck(m_lock); + snd_mixer_selem_set_playback_switch_all(m_mixerelement, mode); + } + + void toggle_mute() { + // std::lock_guard lck(m_lock); + int state; + snd_mixer_selem_get_playback_switch(m_mixerelement, SND_MIXER_SCHN_FRONT_LEFT, &state); + snd_mixer_selem_set_playback_switch_all(m_mixerelement, !state); + } + + bool is_muted() { + // std::lock_guard lck(m_lock); + int state = 0; + for (int i = 0; i < SND_MIXER_SCHN_LAST; i++) { + if (snd_mixer_selem_has_playback_channel(m_mixerelement, (snd_mixer_selem_channel_id_t)i)) { + snd_mixer_selem_get_playback_switch(m_mixerelement, SND_MIXER_SCHN_FRONT_LEFT, &state); + if (state == 0) + return true; + } + } + + return false; + } + + protected: + void error_handler(string message) {} + + private: + // threading_util::spin_lock m_lock; + + snd_mixer_t* m_hardwaremixer = nullptr; + snd_mixer_elem_t* m_mixerelement = nullptr; +}; + +// }}} + +LEMONBUDDY_NS_END diff --git a/include/adapters/mpd.hpp b/include/adapters/mpd.hpp new file mode 100644 index 00000000..0ab82881 --- /dev/null +++ b/include/adapters/mpd.hpp @@ -0,0 +1,491 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "common.hpp" +#include "components/logger.hpp" +#include "utils/math.hpp" + +LEMONBUDDY_NS + +namespace mpd { + DEFINE_ERROR(mpd_exception); + DEFINE_CHILD_ERROR(client_error, mpd_exception); + DEFINE_CHILD_ERROR(server_error, mpd_exception); + + // type details {{{ + + namespace details { + struct mpd_connection_deleter { + void operator()(mpd_connection* conn) { + if (conn != nullptr) + mpd_connection_free(conn); + } + }; + + struct mpd_status_deleter { + void operator()(mpd_status* status) { + mpd_status_free(status); + } + }; + + struct mpd_song_deleter { + void operator()(mpd_song* song) { + mpd_song_free(song); + } + }; + + using mpd_connection_t = unique_ptr; + using mpd_status_t = unique_ptr; + using mpd_song_t = unique_ptr; + } + + inline void check_connection(mpd_connection* conn) { + if (conn == nullptr) + throw client_error("Not connected to MPD server", MPD_ERROR_STATE); + } + + inline void check_errors(mpd_connection* conn) { + mpd_error code = mpd_connection_get_error(conn); + + if (code == MPD_ERROR_SUCCESS) + return; + + auto msg = mpd_connection_get_error_message(conn); + + if (code == MPD_ERROR_SERVER) { + mpd_connection_clear_error(conn); + throw server_error(msg, mpd_connection_get_server_error(conn)); + } else { + mpd_connection_clear_error(conn); + throw client_error(msg, code); + } + } + + enum class mpdstate { + UNKNOWN = 1 << 0, + STOPPED = 1 << 1, + PLAYING = 1 << 2, + PAUSED = 1 << 4, + }; + + // }}} + // class: mpdsong {{{ + + class mpdsong { + public: + explicit mpdsong(details::mpd_song_t&& song) : m_song(forward(song)) {} + + operator bool() { + return m_song.get() != nullptr; + } + + string get_artist() { + assert(m_song); + auto tag = mpd_song_get_tag(m_song.get(), MPD_TAG_ARTIST, 0); + if (tag == nullptr) + return ""; + return string{tag}; + } + + string get_album() { + assert(m_song); + auto tag = mpd_song_get_tag(m_song.get(), MPD_TAG_ALBUM, 0); + if (tag == nullptr) + return ""; + return string{tag}; + } + + string get_title() { + assert(m_song); + auto tag = mpd_song_get_tag(m_song.get(), MPD_TAG_TITLE, 0); + if (tag == nullptr) + return ""; + return string{tag}; + } + + unsigned get_duration() { + assert(m_song); + return mpd_song_get_duration(m_song.get()); + } + + private: + details::mpd_song_t m_song; + }; + + // }}} + // class: mpdconnection {{{ + + class mpdstatus; + class mpdconnection { + public: + explicit mpdconnection(const logger& logger, string host, unsigned int port = 6600, string password = "", unsigned int timeout = 15) + : m_log(logger), m_host(host), m_port(port), m_password(password), m_timeout(timeout) {} + + void connect() { + try { + m_log.trace("mpdconnection.connect: %s, %i, \"%s\", timeout: %i", m_host, m_port, m_password, m_timeout); + m_connection.reset(mpd_connection_new(m_host.c_str(), m_port, m_timeout * 1000)); + check_errors(m_connection.get()); + + if (!m_password.empty()) { + noidle(); + assert(!m_listactive); + mpd_run_password(m_connection.get(), m_password.c_str()); + check_errors(m_connection.get()); + } + + m_fd = mpd_connection_get_fd(m_connection.get()); + check_errors(m_connection.get()); + } catch (const client_error& e) { + disconnect(); + throw e; + } + } + + void disconnect() { + m_connection.reset(); + m_idle = false; + m_listactive = false; + } + + bool connected() { + if (!m_connection) + return false; + return m_connection.get() != nullptr; + } + + bool retry_connection(int interval = 1) { + if (connected()) + return true; + + while (true) { + try { + connect(); + return true; + } catch (const mpd_exception& e) { + } + + this_thread::sleep_for(chrono::duration(interval)); + } + + return false; + } + + int get_fd() { + return m_fd; + } + + void idle() { + check_connection(m_connection.get()); + if (m_idle) + return; + mpd_send_idle(m_connection.get()); + check_errors(m_connection.get()); + m_idle = true; + } + + int noidle() { + check_connection(m_connection.get()); + int flags = 0; + if (m_idle && mpd_send_noidle(m_connection.get())) { + m_idle = false; + flags = mpd_recv_idle(m_connection.get(), true); + mpd_response_finish(m_connection.get()); + check_errors(m_connection.get()); + } + return flags; + } + + unique_ptr get_status() { + check_prerequisites(); + auto status = make_unique(this); + check_errors(m_connection.get()); + // if (update) + // status->update(-1, this); + return status; + } + + unique_ptr get_status_safe() { + try { + return get_status(); + } catch (const mpd_exception& e) { + return {}; + } + } + + unique_ptr get_song() { + check_prerequisites_commands_list(); + mpd_send_current_song(m_connection.get()); + details::mpd_song_t song{mpd_recv_song(m_connection.get()), details::mpd_song_deleter{}}; + mpd_response_finish(m_connection.get()); + check_errors(m_connection.get()); + if (song.get() != nullptr) { + return make_unique(std::move(song)); + } + return unique_ptr{}; + } + + void play() { + try { + check_prerequisites_commands_list(); + mpd_run_play(m_connection.get()); + check_errors(m_connection.get()); + } catch (const mpd_exception& e) { + m_log.err("mpdconnection.play: %s", e.what()); + } + } + + void pause(bool state) { + try { + check_prerequisites_commands_list(); + mpd_run_pause(m_connection.get(), state); + check_errors(m_connection.get()); + } catch (const mpd_exception& e) { + m_log.err("mpdconnection.pause: %s", e.what()); + } + } + + void toggle() { + try { + check_prerequisites_commands_list(); + mpd_run_toggle_pause(m_connection.get()); + check_errors(m_connection.get()); + } catch (const mpd_exception& e) { + m_log.err("mpdconnection.toggle: %s", e.what()); + } + } + + void stop() { + try { + check_prerequisites_commands_list(); + mpd_run_stop(m_connection.get()); + check_errors(m_connection.get()); + } catch (const mpd_exception& e) { + m_log.err("mpdconnection.stop: %s", e.what()); + } + } + + void prev() { + try { + check_prerequisites_commands_list(); + mpd_run_previous(m_connection.get()); + check_errors(m_connection.get()); + } catch (const mpd_exception& e) { + m_log.err("mpdconnection.prev: %s", e.what()); + } + } + + void next() { + try { + check_prerequisites_commands_list(); + mpd_run_next(m_connection.get()); + check_errors(m_connection.get()); + } catch (const mpd_exception& e) { + m_log.err("mpdconnection.next: %s", e.what()); + } + } + + void seek(int songid, int pos) { + try { + check_prerequisites_commands_list(); + mpd_run_seek_id(m_connection.get(), songid, pos); + check_errors(m_connection.get()); + } catch (const mpd_exception& e) { + m_log.err("mpdconnection.seek: %s", e.what()); + } + } + + void set_repeat(bool mode) { + try { + check_prerequisites_commands_list(); + mpd_run_repeat(m_connection.get(), mode); + check_errors(m_connection.get()); + } catch (const mpd_exception& e) { + m_log.err("mpdconnection.set_repeat: %s", e.what()); + } + } + + void set_random(bool mode) { + try { + check_prerequisites_commands_list(); + mpd_run_random(m_connection.get(), mode); + check_errors(m_connection.get()); + } catch (const mpd_exception& e) { + m_log.err("mpdconnection.set_random: %s", e.what()); + } + } + + void set_single(bool mode) { + try { + check_prerequisites_commands_list(); + mpd_run_single(m_connection.get(), mode); + check_errors(m_connection.get()); + } catch (const mpd_exception& e) { + m_log.err("mpdconnection.set_single: %s", e.what()); + } + } + + operator details::mpd_connection_t::element_type*() { + return m_connection.get(); + } + + protected: + void check_prerequisites() { + check_connection(m_connection.get()); + noidle(); + } + + void check_prerequisites_commands_list() { + noidle(); + assert(!m_listactive); + check_prerequisites(); + } + + private: + const logger& m_log; + details::mpd_connection_t m_connection; + + bool m_listactive = false; + bool m_idle = false; + int m_fd = -1; + + string m_host; + unsigned int m_port; + string m_password; + unsigned int m_timeout; + }; + + // }}} + // class: mpdstatus {{{ + + class mpdstatus { + public: + explicit mpdstatus(mpdconnection* conn, bool autoupdate = true) { + fetch_data(conn); + if (autoupdate) + update(-1, conn); + } + + void fetch_data(mpdconnection* conn) { + m_status.reset(mpd_run_status(*conn)); + m_updated_at = chrono::system_clock::now(); + m_songid = mpd_status_get_song_id(m_status.get()); + m_random = mpd_status_get_random(m_status.get()); + m_repeat = mpd_status_get_repeat(m_status.get()); + m_single = mpd_status_get_single(m_status.get()); + m_elapsed_time = mpd_status_get_elapsed_time(m_status.get()); + m_total_time = mpd_status_get_total_time(m_status.get()); + } + + void update(int event, mpdconnection* connection) { + if (connection == nullptr || (event & (MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS)) == false) + return; + + fetch_data(connection); + + m_elapsed_time_ms = m_elapsed_time * 1000; + + auto state = mpd_status_get_state(m_status.get()); + + switch (state) { + case MPD_STATE_PAUSE: + m_state = mpdstate::PAUSED; + break; + case MPD_STATE_PLAY: + m_state = mpdstate::PLAYING; + break; + case MPD_STATE_STOP: + m_state = mpdstate::STOPPED; + break; + default: + m_state = mpdstate::UNKNOWN; + } + } + + void update_timer() { + auto diff = chrono::system_clock::now() - m_updated_at; + auto dur = chrono::duration_cast(diff); + m_elapsed_time_ms += dur.count(); + m_elapsed_time = m_elapsed_time_ms / 1000 + 0.5f; + m_updated_at = chrono::system_clock::now(); + } + + bool random() const { + return m_random; + } + + bool repeat() const { + return m_repeat; + } + + bool single() const { + return m_single; + } + + bool match_state(mpdstate state) const { + return state == m_state; + } + + int get_songid() const { + return m_songid; + } + + unsigned get_total_time() const { + return m_total_time; + } + + unsigned get_elapsed_time() const { + return m_elapsed_time; + } + + unsigned get_elapsed_percentage() { + if (m_total_time == 0) + return 0; + return static_cast(float(m_elapsed_time) / float(m_total_time) * 100.0 + 0.5f); + } + + string get_formatted_elapsed() { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%lu:%02lu", m_elapsed_time / 60, m_elapsed_time % 60); + return {buffer}; + } + + string get_formatted_total() { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%lu:%02lu", m_total_time / 60, m_total_time % 60); + return {buffer}; + } + + int get_seek_position(int percentage) { + if (m_total_time == 0) + return 0; + math_util::cap(0, 100, percentage); + return float(m_total_time) * percentage / 100.0f + 0.5f; + } + + private: + details::mpd_status_t m_status; + unique_ptr m_song; + mpdstate m_state = mpdstate::UNKNOWN; + chrono::system_clock::time_point m_updated_at; + + bool m_random = false; + bool m_repeat = false; + bool m_single = false; + + int m_songid; + + unsigned long m_total_time; + unsigned long m_elapsed_time; + unsigned long m_elapsed_time_ms; + }; + + // }}} +} + +LEMONBUDDY_NS_END diff --git a/include/adapters/net.hpp b/include/adapters/net.hpp new file mode 100644 index 00000000..5b64f032 --- /dev/null +++ b/include/adapters/net.hpp @@ -0,0 +1,433 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef inline +#undef inline +#endif + +#include "common.hpp" +#include "config.hpp" +#include "utils/command.hpp" +#include "utils/file.hpp" +#include "utils/string.hpp" + +LEMONBUDDY_NS + +namespace net { + DEFINE_ERROR(network_error); + DEFINE_ERROR(wired_network_error); + DEFINE_ERROR(wireless_network_error); + + // types {{{ + + struct bytes_t { + uint32_t transmitted = 0; + uint32_t received = 0; + std::chrono::system_clock::time_point time; + }; + + struct linkdata_t { + string ip_address; + bytes_t previous; + bytes_t current; + }; + + // }}} + // class: network {{{ + + class network { + public: + explicit network(string interface) : m_interface(interface) { + if (if_nametoindex(m_interface.c_str()) == 0) + throw network_error("Invalid network interface \"" + m_interface + "\""); + if ((m_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) + throw network_error("Failed to open socket"); + std::memset(&m_data, 0, sizeof(m_data)); + std::strncpy(m_data.ifr_name, m_interface.data(), IFNAMSIZ - 1); + } + + ~network() { + if (m_fd != -1) + close(m_fd); + } + + bool test_interface() { + if ((ioctl(m_fd, SIOCGIFFLAGS, &m_data)) == -1) + throw network_error("Failed to get flags"); + if ((m_data.ifr_flags & IFF_UP) == 0) + return false; + if ((m_data.ifr_flags & IFF_RUNNING) == 0) + return false; + return true; + } + + bool test_connection() { + int status = EXIT_FAILURE; + + try { + m_ping = command_util::make_command( + "ping -c 2 -W 2 -I " + m_interface + " " + string(CONNECTION_TEST_IP)); + status = m_ping->exec(true); + m_ping.reset(); + } catch (std::exception& e) { + } + + return (status == EXIT_SUCCESS); + } + + bool test() { + try { + return test_interface() && test_connection(); + } catch (network_error& e) { + return false; + } + } + + bool connected() { + try { + if (!test_interface()) + return false; + return file_util::get_contents("/sys/class/net/" + m_interface + "/carrier")[0] == '1'; + } catch (network_error& e) { + return false; + } + } + + bool query_interface() { + auto now = chrono::system_clock::now(); + if ((now - m_last_query) < chrono::seconds(1)) + return true; + m_last_query = now; + + struct ifaddrs* ifaddr; + getifaddrs(&ifaddr); + bool match = false; + + for (auto ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) { + if (m_interface.compare(0, m_interface.length(), ifa->ifa_name) != 0) + continue; + match = true; + + switch (ifa->ifa_addr->sa_family) { + case AF_INET: + char ip_buffer[NI_MAXHOST]; + getnameinfo(ifa->ifa_addr, sizeof(sockaddr_in), ip_buffer, NI_MAXHOST, nullptr, 0, + NI_NUMERICHOST); + m_linkdata.ip_address = string(ip_buffer); + break; + + case AF_PACKET: + if (ifa->ifa_data == nullptr) + continue; + struct rtnl_link_stats* link_state = + reinterpret_cast(ifa->ifa_data); + m_linkdata.previous = m_linkdata.current; + m_linkdata.current.transmitted = link_state->tx_bytes; + m_linkdata.current.received = link_state->rx_bytes; + m_linkdata.current.time = chrono::system_clock::now(); + break; + } + } + + freeifaddrs(ifaddr); + + return match; + } + + string ip() { + if (!test_interface()) + throw network_error("Interface is not up"); + if (!query_interface()) + throw network_error("Failed to query interface"); + return m_linkdata.ip_address; + } + + string downspeed() { + if (!query_interface()) + throw network_error("Failed to query interface"); + + float bytes_diff = m_linkdata.current.received - m_linkdata.previous.received; + float time_diff = + chrono::duration_cast(m_linkdata.current.time - m_linkdata.previous.time) + .count(); + float speed = bytes_diff / time_diff; + + speed /= 1000; // convert to KB + int suffix_n = 0; + vector suffixes{"KB", "MB", "GB"}; + + while (speed >= 1000 && suffix_n < (int)suffixes.size() - 1) { + suffix_n++; + speed /= 1000; + } + + return string_util::from_stream(stringstream() << std::setw(3) << std::setfill(' ') + << std::setprecision(0) << std::fixed << speed + << " " << suffixes[suffix_n] << "/s"); + } + + string upspeed() { + if (!query_interface()) + throw network_error("Failed to query interface"); + + float bytes_diff = m_linkdata.current.transmitted - m_linkdata.previous.transmitted; + float time_diff = + chrono::duration_cast(m_linkdata.current.time - m_linkdata.previous.time) + .count(); + float speed = bytes_diff / time_diff; + + speed /= 1000; // convert to KB + int suffix_n = 0; + vector suffixes{"KB", "MB", "GB"}; + + while (speed >= 1000 && suffix_n < (int)suffixes.size() - 1) { + suffix_n++; + speed /= 1000; + } + + return string_util::from_stream(stringstream() << std::setw(3) << std::setfill(' ') + << std::setprecision(0) << std::fixed << speed + << " " << suffixes[suffix_n] << "/s"); + } + + protected: + unique_ptr m_ping; + string m_interface; + string m_ip; + struct ifreq m_data; + int m_fd = 0; + + linkdata_t m_linkdata; + + chrono::system_clock::time_point m_last_query; + }; + + // }}} + // class: wired_network {{{ + + class wired_network : public network { + public: + explicit wired_network(string interface) : network(interface) { + struct ethtool_cmd e; + e.cmd = ETHTOOL_GSET; + + m_data.ifr_data = (caddr_t)&e; + + if (ioctl(m_fd, SIOCETHTOOL, &m_data) == 0) + m_linkspeed = (e.speed == USHRT_MAX ? 0 : e.speed); + } + + string link_speed() { + return string((m_linkspeed == 0 ? "???" : to_string(m_linkspeed)) + " Mbit/s"); + } + + private: + int m_linkspeed = 0; + }; + + // }}} + // class: wireless_network {{{ + + struct wireless_info { + std::bitset<5> flags; + string essid{IW_ESSID_MAX_SIZE + 1}; + int quality = 0; + int quality_max = 0; + int quality_avg = 0; + int signal = 0; + int signal_max = 0; + int noise = 0; + int noise_max = 0; + int bitrate = 0; + double frequency = 0; + }; + + enum wireless_flags { + ESSID = 0, + QUALITY = 1, + SIGNAL = 2, + NOISE = 3, + FREQUENCY = 4, + }; + + class wireless_network : public network { + public: + wireless_network(string interface) : network(interface) { + std::strcpy((char*)&m_iw.ifr_ifrn.ifrn_name, m_interface.c_str()); + + if (!m_info) + m_info.reset(new wireless_info()); + } + + string essid() { + if (!query_interface()) + return ""; + if (!m_info->flags.test(wireless_flags::ESSID)) + return ""; + return m_info->essid; + } + + float signal_quality() { + if (!query_interface()) + return 0; + if (m_info->flags.test(wireless_flags::QUALITY)) + return 2 * (signal_dbm() + 100); + return 0; + } + + float signal_dbm() { + if (!query_interface()) + return 0; + if (m_info->flags.test(wireless_flags::QUALITY)) + return m_info->quality + m_info->noise - 256; + return 0; + } + + protected: + bool query_interface() { + if ((chrono::system_clock::now() - m_last_query) < chrono::seconds(1)) + return true; + + network::query_interface(); + + auto ifname = m_interface.c_str(); + auto socket_fd = iw_sockets_open(); + + if (socket_fd == -1) + return false; + + auto on_exit = scope_util::make_exit_handler<>([&]() { iw_sockets_close(socket_fd); }); + { + wireless_config wcfg; + + if (iw_get_basic_config(socket_fd, ifname, &wcfg) == -1) + return false; + + // reset flags + m_info->flags.none(); + + if (wcfg.has_essid && wcfg.essid_on) { + m_info->essid = {wcfg.essid, 0, IW_ESSID_MAX_SIZE}; + m_info->flags |= wireless_flags::ESSID; + } + + if (wcfg.has_freq) { + m_info->frequency = wcfg.freq; + m_info->flags |= wireless_flags::FREQUENCY; + } + + if (wcfg.mode == IW_MODE_ADHOC) + return true; + + iwrange range; + if (iw_get_range_info(socket_fd, ifname, &range) == -1) + return false; + + iwstats stats; + if (iw_get_stats(socket_fd, ifname, &stats, &range, 1) == -1) + return false; + + if (stats.qual.updated & IW_QUAL_RCPI) { + if (!(stats.qual.updated & IW_QUAL_QUAL_INVALID)) { + m_info->quality = stats.qual.qual; + m_info->quality_max = range.max_qual.qual; + m_info->quality_avg = range.avg_qual.qual; + m_info->flags |= wireless_flags::QUALITY; + } + + if (stats.qual.updated & IW_QUAL_RCPI) { + if (!(stats.qual.updated & IW_QUAL_LEVEL_INVALID)) { + m_info->signal = stats.qual.level / 2.0 - 110 + 0.5; + m_info->flags |= wireless_flags::SIGNAL; + } + if (!(stats.qual.updated & IW_QUAL_NOISE_INVALID)) { + m_info->noise = stats.qual.noise / 2.0 - 110 + 0.5; + m_info->flags |= wireless_flags::NOISE; + } + } else { + if ((stats.qual.updated & IW_QUAL_DBM) || stats.qual.level > range.max_qual.level) { + if (!(stats.qual.updated & IW_QUAL_LEVEL_INVALID)) { + m_info->signal = stats.qual.level; + if (m_info->signal > 63) + m_info->signal -= 256; + m_info->flags |= wireless_flags::SIGNAL; + } + if (!(stats.qual.updated & IW_QUAL_NOISE_INVALID)) { + m_info->noise = stats.qual.noise; + if (m_info->noise > 63) + m_info->noise -= 256; + m_info->flags |= wireless_flags::NOISE; + } + } else { + if (!(stats.qual.updated & IW_QUAL_LEVEL_INVALID)) { + m_info->signal = stats.qual.level; + m_info->signal_max = range.max_qual.level; + m_info->flags |= wireless_flags::SIGNAL; + } + if (!(stats.qual.updated & IW_QUAL_NOISE_INVALID)) { + m_info->noise = stats.qual.noise; + m_info->noise_max = range.max_qual.noise; + m_info->flags |= wireless_flags::NOISE; + } + } + } + } else { + if (!(stats.qual.updated & IW_QUAL_QUAL_INVALID)) { + m_info->quality = stats.qual.qual; + m_info->flags |= wireless_flags::QUALITY; + } + if (!(stats.qual.updated & IW_QUAL_LEVEL_INVALID)) { + m_info->quality = stats.qual.level; + m_info->flags |= wireless_flags::SIGNAL; + } + if (!(stats.qual.updated & IW_QUAL_NOISE_INVALID)) { + m_info->quality = stats.qual.noise; + m_info->flags |= wireless_flags::NOISE; + } + } + + // struct iwreq wrq; + // if (iw_get_ext(socket_fd, ifname, SIOCGIWRATE, &wrq) != -1) + // m_info->bitrate = wrq.u.bitrate.value; + + return true; + } + } + + private: + struct iwreq m_iw; + shared_ptr m_info; + }; + + // }}} + + inline bool is_wireless_interface(string ifname) { + return file_util::exists("/sys/class/net/" + ifname + "/wireless"); + } +} + +LEMONBUDDY_NS_END diff --git a/include/bar.hpp b/include/bar.hpp deleted file mode 100644 index efe83e92..00000000 --- a/include/bar.hpp +++ /dev/null @@ -1,102 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "config.hpp" -#include "exception.hpp" -#include "utils/xcb.hpp" - -DefineBaseException(ConfigurationError); - -class Registry; - -struct CompiledWithoutModuleSupport : public ConfigurationError -{ - explicit CompiledWithoutModuleSupport(std::string module_name) - : ConfigurationError(std::string(APP_NAME) + " was not compiled with support for module \""+ module_name +"\"") {} -}; - -struct Font -{ - std::string id; - int offset = 0; - - Font(std::string id, int offset) - : id(id), offset(offset){} -}; - -enum Cmd -{ - LEFT_CLICK = 1, - MIDDLE_CLICK = 2, - RIGHT_CLICK = 3, - SCROLL_UP = 4, - SCROLL_DOWN = 5, -}; - -struct Options -{ - std::shared_ptr monitor; - - std::string wm_name; - std::string locale; - - std::string background = "#ffffff"; - std::string foreground = "#000000"; - std::string linecolor = "#000000"; - - int width = 0; - int height = 0; - - int offset_x = 0; - int offset_y = 0; - - bool bottom = false; - bool dock = true; - int clickareas = 25; - - std::string separator; - int spacing = 1; - int lineheight = 1; - - int padding_left = 0; - int padding_right = 0; - int module_margin_left = 0; - int module_margin_right = 2; - - std::vector> fonts; - - std::string get_geom() - { - std::stringstream ss; - ss << this->width << "x" << this->height << "+"; - ss << this->offset_x << "+" << this->offset_y; - return ss.str(); - } -}; - -class Bar -{ - std::string config_path; - - std::vector mod_left; - std::vector mod_center; - std::vector mod_right; - - public: - Bar(); - - std::shared_ptr opts; - std::shared_ptr registry; - - void load(std::shared_ptr registry); - - std::string get_output(); - std::string get_exec_line(); -}; - -std::shared_ptr get_bar(); -std::shared_ptr bar_opts(); diff --git a/include/common.hpp b/include/common.hpp new file mode 100644 index 00000000..69d64b77 --- /dev/null +++ b/include/common.hpp @@ -0,0 +1,125 @@ +#pragma once + +#ifdef DEBUG +#define BOOST_DI_CFG_DIAGNOSTICS_LEVEL 2 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LEMONBUDDY_NS \ + namespace lemonbuddy { \ + inline namespace v2_0_0 { +#define LEMONBUDDY_NS_END \ + } \ + } +#define LEMONBUDDY_NS_PATH "lemonbuddy::v2_0_0" + +#define PIPE_READ 0 +#define PIPE_WRITE 1 + +#define LOG(m) std::cout << m << std::endl + +#ifdef DEBUG +#include "debug.hpp" +#endif + +LEMONBUDDY_NS + +//================================================== +// Include common types (i.e, unclutter editor!) +//================================================== + +namespace di = boost::di; +namespace chrono = std::chrono; +namespace this_thread = std::this_thread; + +using namespace std::chrono_literals; + +using std::string; +using std::stringstream; +using std::size_t; +using std::bind; +using std::forward; +using std::function; +using std::shared_ptr; +using std::unique_ptr; +using std::make_unique; +using std::make_shared; +using std::make_pair; +using std::array; +using std::map; +using std::vector; +using std::to_string; +using std::strerror; +using std::getenv; +using std::thread; + +using boost::optional; + +using stateflag = std::atomic; + +//================================================== +// Errors and exceptions +//================================================== + +namespace factory { + template + unique_ptr generic_instance(Deps... deps) { + return make_unique(deps...); + } + + template + shared_ptr generic_singleton(Deps... deps) { + static auto instance = make_shared(deps...); + return instance; + } +} + +//================================================== +// Errors and exceptions +//================================================== + +class application_error : public std::runtime_error { + public: + int m_code; + + explicit application_error(string&& message, int code = 0) + : std::runtime_error(forward(message)), m_code(code) {} +}; + +class system_error : public application_error { + public: + explicit system_error() : application_error(strerror(errno), errno) {} + explicit system_error(string&& message) + : application_error(forward(message) + " (reason: " + strerror(errno) + ")", errno) {} +}; + +#define DEFINE_CHILD_ERROR(error, parent) \ + class error : public parent { \ + using parent::parent; \ + } +#define DEFINE_ERROR(error) DEFINE_CHILD_ERROR(error, application_error) + +//================================================== +// Various tools and helpers functions +//================================================== + +auto has_env = [](const char* var) { return getenv(var) != nullptr; }; +auto read_env = [](const char* var, string&& fallback = "") { + const char* value{getenv(var)}; + return value != nullptr ? value : fallback; +}; + +LEMONBUDDY_NS_END diff --git a/include/components/bar.hpp b/include/components/bar.hpp new file mode 100644 index 00000000..a66accfb --- /dev/null +++ b/include/components/bar.hpp @@ -0,0 +1,767 @@ +#pragma once + +#include +#include + +#include "common.hpp" +#include "components/config.hpp" +#include "components/logger.hpp" +#include "components/parser.hpp" +#include "components/types.hpp" +#include "components/x11/connection.hpp" +#include "components/x11/draw.hpp" +#include "components/x11/fontmanager.hpp" +#include "components/x11/randr.hpp" +#include "components/x11/tray.hpp" +#include "components/x11/types.hpp" +#include "components/x11/window.hpp" +#include "components/x11/xlib.hpp" +#include "components/x11/xutils.hpp" +#include "utils/math.hpp" +#include "utils/string.hpp" +#include "utils/threading.hpp" + +LEMONBUDDY_NS + +class bar { + public: + /** + * Construct bar + */ + explicit bar(connection& conn, const config& config, const logger& logger, + unique_ptr fontmanager) + : m_connection(conn) + , m_conf(config) + , m_log(logger) + , m_fontmanager(forward(fontmanager)) {} + + /** + * Cleanup signal handlers and destroy the bar window + */ + ~bar() { + std::lock_guard lck(m_lock); + parser_signals::alignment_change.disconnect(this, &bar::on_alignment_change); + parser_signals::attribute_set.disconnect(this, &bar::on_attribute_set); + parser_signals::attribute_unset.disconnect(this, &bar::on_attribute_unset); + parser_signals::attribute_toggle.disconnect(this, &bar::on_attribute_toggle); + parser_signals::action_block_open.disconnect(this, &bar::on_action_block_open); + parser_signals::action_block_close.disconnect(this, &bar::on_action_block_close); + parser_signals::color_change.disconnect(this, &bar::on_color_change); + parser_signals::font_change.disconnect(this, &bar::on_font_change); + parser_signals::pixel_offset.disconnect(this, &bar::on_pixel_offset); + parser_signals::ascii_text_write.disconnect(this, &bar::draw_character); + parser_signals::unicode_text_write.disconnect(this, &bar::draw_character); + if (m_tray.align != alignment::NONE) + tray_signals::report_slotcount.disconnect(this, &bar::on_tray_report); + m_window.destroy(); + } + + /** + * Configure injection module + */ + template > + static di::injector configure() { + // clang-format off + return di::make_injector( + connection::configure(), + config::configure(), + logger::configure(), + fontmanager::configure()); + // clang-format on + } + + /** + * Create required components + * + * This is done outside the constructor due to boost::di noexcept + */ + void bootstrap(bool nodraw = false) { //{{{ + m_screen = m_connection.screen(); + m_visual = m_connection.visual_type(m_screen, 32).get(); + auto monitors = randr_util::get_monitors(m_connection, m_connection.screen()->root); + auto bs = m_conf.bar_section(); + + // Look for the defined monitor {{{ + + if (monitors.empty()) + throw application_error("No monitors found"); + + auto monitor_name = m_conf.get(bs, "monitor", ""); + if (monitor_name.empty()) + monitor_name = monitors[0]->name; + + for (auto&& monitor : monitors) { + if (monitor_name.compare(monitor->name) == 0) { + m_bar.monitor = std::move(monitor); + break; + } + } + + if (m_bar.monitor) + m_log.trace("bar: Found matching monitor %s (%ix%i+%i+%i)", m_bar.monitor->name, + m_bar.monitor->w, m_bar.monitor->h, m_bar.monitor->x, m_bar.monitor->y); + else + throw application_error("Could not find monitor: " + monitor_name); + + // }}} + // Set bar colors {{{ + + m_bar.background = color::parse(m_conf.get(bs, "background", m_bar.background.hex())); + m_bar.foreground = color::parse(m_conf.get(bs, "foreground", m_bar.foreground.hex())); + m_bar.linecolor = color::parse(m_conf.get(bs, "linecolor", m_bar.linecolor.hex())); + + // }}} + // Set border values {{{ + + auto bsize = m_conf.get(bs, "border-size", 0); + auto bcolor = m_conf.get(bs, "border-color", ""); + + m_borders.emplace(border::TOP, border_settings{}); + m_borders[border::TOP].size = m_conf.get(bs, "border-top", bsize); + m_borders[border::TOP].color = color::parse(m_conf.get(bs, "border-top-color", bcolor)); + + m_borders.emplace(border::BOTTOM, border_settings{}); + m_borders[border::BOTTOM].size = m_conf.get(bs, "border-bottom", bsize); + m_borders[border::BOTTOM].color = + color::parse(m_conf.get(bs, "border-bottom-color", bcolor)); + + m_borders.emplace(border::LEFT, border_settings{}); + m_borders[border::LEFT].size = m_conf.get(bs, "border-left", bsize); + m_borders[border::LEFT].color = + color::parse(m_conf.get(bs, "border-left-color", bcolor)); + + m_borders.emplace(border::RIGHT, border_settings{}); + m_borders[border::RIGHT].size = m_conf.get(bs, "border-right", bsize); + m_borders[border::RIGHT].color = + color::parse(m_conf.get(bs, "border-right-color", bcolor)); + + // }}} + // Set size and position {{{ + + m_bar.dock = m_conf.get(bs, "dock", true); + m_bar.bottom = m_conf.get(bs, "bottom", false); + m_bar.lineheight = m_conf.get(bs, "lineheight", 0); + m_bar.offset_x = m_conf.get(bs, "offset_x", 0); + m_bar.offset_y = m_conf.get(bs, "offset_y", 0); + m_bar.padding_left = m_conf.get(bs, "padding_left", 0); + m_bar.padding_right = m_conf.get(bs, "padding_right", 0); + m_bar.module_margin_left = + m_conf.get(bs, "module_margin_left", 0); + m_bar.module_margin_right = + m_conf.get(bs, "module_margin_right", 0); + + auto w = m_conf.get(bs, "width", "100%"); + auto h = m_conf.get(bs, "height", "24"); + + m_bar.width = std::atoi(w.c_str()); + if (w.find("%") != string::npos) + m_bar.width = m_bar.monitor->w * (m_bar.width / 100.0) + 0.5f; + + m_bar.height = std::atoi(h.c_str()); + if (h.find("%") != string::npos) + m_bar.height = m_bar.monitor->h * (m_bar.height / 100.0) + 0.5f; + + // apply offsets + m_bar.width -= m_bar.offset_x * 2; + m_bar.x = m_bar.offset_x + m_bar.monitor->x; + m_bar.y = m_bar.offset_y + m_bar.monitor->y; + + // apply borders + m_bar.height += m_borders[border::TOP].size; + m_bar.height += m_borders[border::BOTTOM].size; + + if (m_bar.bottom) + m_bar.y = m_bar.monitor->y + m_bar.monitor->h - m_bar.height - m_bar.offset_y; + + if (m_bar.width <= 0 || m_bar.width > m_bar.monitor->w) + throw application_error("Resulting bar width is out of bounds"); + if (m_bar.height <= 0 || m_bar.height > m_bar.monitor->h) + throw application_error("Resulting bar height is out of bounds"); + + m_bar.width = math_util::cap(m_bar.width, 0, m_bar.monitor->w); + m_bar.height = math_util::cap(m_bar.height, 0, m_bar.monitor->h); + + m_bar.vertical_mid = + (m_bar.height + m_borders[border::TOP].size - m_borders[border::BOTTOM].size) / 2; + + m_log.trace("bar: Resulting bar geom %ix%i+%i+%i", m_bar.width, m_bar.height, m_bar.x, m_bar.y); + + // }}} + // Set the WM_NAME value {{{ + + m_bar.wmname = "lemonbuddy-" + bs.substr(4) + "_" + m_bar.monitor->name; + m_bar.wmname = m_conf.get(bs, "wm_name", m_bar.wmname); + m_bar.wmname = string_util::replace(m_bar.wmname, " ", "-"); + + // }}} + // Checking nodraw {{{ + + if (nodraw) { + m_log.trace("bar: Abort bootstrap routine (reason: nodraw)"); + return; + } + + // }}} + // Setup graphic components and create the window {{{ + + m_log.trace("bar: Create colormap"); + { + m_connection.create_colormap_checked( + XCB_COLORMAP_ALLOC_NONE, m_colormap, m_screen->root, m_visual->visual_id); + } + + m_log.trace("bar: Create window %s", m_connection.id(m_window)); + { + uint32_t mask = 0; + xcb_params_cw_t params; + // clang-format off + XCB_AUX_ADD_PARAM(&mask, ¶ms, back_pixel, m_bar.background.value()); + XCB_AUX_ADD_PARAM(&mask, ¶ms, border_pixel, m_bar.background.value()); + XCB_AUX_ADD_PARAM(&mask, ¶ms, colormap, m_colormap); + XCB_AUX_ADD_PARAM(&mask, ¶ms, override_redirect, m_bar.dock); + XCB_AUX_ADD_PARAM(&mask, ¶ms, event_mask, XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_BUTTON_PRESS); + // clang-format on + m_window.create_checked(m_bar.x, m_bar.y, m_bar.width, m_bar.height, mask, ¶ms); + m_window.map_checked(); + } + + m_log.trace("bar: Set WM_NAME"); + { + xcb_icccm_set_wm_name( + m_connection, m_window, XCB_ATOM_STRING, 8, m_bar.wmname.length(), m_bar.wmname.c_str()); + xcb_icccm_set_wm_class(m_connection, m_window, 21, "lemonbuddy\0Lemonbuddy"); + } + + m_log.trace("bar: Set _NET_WM_WINDOW_TYPE"); + { + xcb_atom_t win_types[2] = {_NET_WM_WINDOW_TYPE_DOCK, _NET_WM_WINDOW_TYPE_NORMAL}; + m_connection.change_property_checked( + XCB_PROP_MODE_REPLACE, m_window, _NET_WM_WINDOW_TYPE, XCB_ATOM_ATOM, 32, 2, &win_types); + } + + m_log.trace("bar: Set _NET_WM_STATE"); + { + xcb_atom_t win_states[2] = {_NET_WM_STATE_STICKY, _NET_WM_STATE_SKIP_TASKBAR}; + m_connection.change_property_checked( + XCB_PROP_MODE_REPLACE, m_window, _NET_WM_STATE, XCB_ATOM_ATOM, 32, 2, &win_states); + } + + m_log.trace("bar: Set _NET_WM_PID"); + { + int pid = getpid(); + m_connection.change_property_checked( + XCB_PROP_MODE_REPLACE, m_window, _NET_WM_PID, XCB_ATOM_CARDINAL, 32, 1, &pid); + } + + m_log.trace("bar: Create pixmap"); + { + m_connection.create_pixmap_checked( + m_visual->visual_id == m_screen->root_visual ? XCB_COPY_FROM_PARENT : 32, m_pixmap, + m_window, m_bar.width, m_bar.height); + } + + // }}} + // Create graphic contexts {{{ + + // XCB_GC_LINE_WIDTH + // XCB_GC_LINE_STYLE + // -- XCB_LINE_STYLE_SOLID + // xcb_poly_line (connection, XCB_COORD_MODE_PREVIOUS, window, foreground, 4, polyline); + // xcb_poly_line(conn, XCB_COORD_MODE_ORIGIN, drawable, gc, 2, (xcb_point_t[]) { {10, 10}, {100, + // 10} }); + + m_log.trace("bar: Create graphic contexts"); + { + // clang-format off + vector colors { + m_bar.background.value(), + m_bar.foreground.value(), + m_bar.linecolor.value(), + m_bar.linecolor.value(), + m_borders[border::TOP].color.value(), + m_borders[border::BOTTOM].color.value(), + m_borders[border::LEFT].color.value(), + m_borders[border::RIGHT].color.value(), + }; + // clang-format on + + for (int i = 1; i <= 8; i++) { + uint32_t mask = 0; + uint32_t value_list[32]; + xcb_params_gc_t params; + XCB_AUX_ADD_PARAM(&mask, ¶ms, foreground, colors[i - 1]); + XCB_AUX_ADD_PARAM(&mask, ¶ms, graphics_exposures, 0); + xutils::pack_values(mask, ¶ms, value_list); + m_gcontexts.emplace(gc(i), gcontext{m_connection, m_connection.generate_id()}); + m_connection.create_gc_checked(m_gcontexts.at(gc(i)), m_pixmap, mask, &value_list); + } + } + + // }}} + // Load fonts {{{ + + auto fonts_loaded = false; + auto fontindex = 0; + auto fonts = m_conf.get_list(bs, "font"); + + for (auto f : fonts) { + fontindex++; + vector fd = string_util::split(f, ';'); + string pattern{fd[0]}; + int offset{0}; + + if (fd.size() > 1) + offset = std::stoi(fd[1], 0, 10); + + if (m_fontmanager->load(pattern, fontindex, offset)) + fonts_loaded = true; + else + m_log.warn("Unable to load font '%s'", fd[0]); + } + + if (!fonts_loaded) { + m_log.warn("Loading fallback font"); + + if (!m_fontmanager->load("fixed")) + throw application_error("Unable to load fonts"); + } + + m_fontmanager->allocate_color(m_bar.foreground); + + // }}} + // Set tray settings {{{ + + try { + auto tray_position = m_conf.get(bs, "tray-position"); + + if (tray_position == "left") + m_tray.align = alignment::LEFT; + else if (tray_position == "right") + m_tray.align = alignment::RIGHT; + else + m_tray.align = alignment::NONE; + } catch (const key_error& err) { + m_tray.align = alignment::NONE; + } + + if (m_tray.align != alignment::NONE) { + m_tray.background = m_bar.background.value(); + m_tray.height = m_bar.height; + m_tray.height -= m_borders.at(border::BOTTOM).size; + m_tray.height -= m_borders.at(border::TOP).size; + + if (m_tray.height % 2 != 0) { + m_tray.height--; + } + + if (m_tray.height > 24) { + m_tray.spacing = (m_tray.height - 24) / 2; + m_tray.height = 24; + } + + m_tray.width = m_tray.height; + m_tray.orig_y = m_bar.y + m_borders.at(border::TOP).size; + + if (m_tray.align == alignment::RIGHT) + m_tray.orig_x = m_bar.x + m_bar.width - m_borders.at(border::RIGHT).size; + else + m_tray.orig_x = m_bar.x + m_borders.at(border::LEFT).size; + } + + // }}} + // Connect signal handlers {{{ + + parser_signals::alignment_change.connect(this, &bar::on_alignment_change); + parser_signals::attribute_set.connect(this, &bar::on_attribute_set); + parser_signals::attribute_unset.connect(this, &bar::on_attribute_unset); + parser_signals::attribute_toggle.connect(this, &bar::on_attribute_toggle); + parser_signals::action_block_open.connect(this, &bar::on_action_block_open); + parser_signals::action_block_close.connect(this, &bar::on_action_block_close); + parser_signals::color_change.connect(this, &bar::on_color_change); + parser_signals::font_change.connect(this, &bar::on_font_change); + parser_signals::pixel_offset.connect(this, &bar::on_pixel_offset); + parser_signals::ascii_text_write.connect(this, &bar::draw_character); + parser_signals::unicode_text_write.connect(this, &bar::draw_character); + + if (m_tray.align != alignment::NONE) + tray_signals::report_slotcount.connect(this, &bar::on_tray_report); + + // }}} + + m_connection.flush(); + } //}}} + + /** + * Parse input string and redraw the bar window + * + * @param data Input string + * @param force Unless true, do not parse unchanged data + */ + void parse(string data, bool force = false) { //{{{ + std::lock_guard lck(m_lock); + { + if (data == m_prevdata && !force) + return; + + m_prevdata = data; + + // TODO: move to fontmanager + m_xftdraw = XftDrawCreate(xlib::get_display(), m_pixmap, xlib::get_visual(), m_colormap); + + m_bar.align = alignment::LEFT; + m_xpos = m_borders[border::LEFT].size; + m_attributes = 0; + m_actions.clear(); + + draw_background(); + + if (m_tray.align == alignment::LEFT && m_tray.slots) + m_xpos += ((m_tray.width + m_tray.spacing) * m_tray.slots) + m_tray.spacing; + + try { + parser parser(m_bar); + parser(data); + } catch (const unrecognized_token& err) { + m_log.err("Unrecognized syntax token '%s'", err.what()); + } + + if (m_tray.align == alignment::RIGHT && m_tray.slots) + draw_shift(m_xpos, ((m_tray.width + m_tray.spacing) * m_tray.slots) + m_tray.spacing); + + draw_border(border::ALL); + + flush(); + + XftDrawDestroy(m_xftdraw); + } + } //}}} + + /** + * Copy the contents of the pixmap's onto the bar window + */ + void flush() { //{{{ + m_connection.copy_area( + m_pixmap, m_window, m_gcontexts.at(gc::FG), 0, 0, 0, 0, m_bar.width, m_bar.height); + m_connection.copy_area( + m_pixmap, m_window, m_gcontexts.at(gc::BT), 0, 0, 0, 0, m_bar.width, m_bar.height); + m_connection.copy_area( + m_pixmap, m_window, m_gcontexts.at(gc::BB), 0, 0, 0, 0, m_bar.width, m_bar.height); + m_connection.copy_area( + m_pixmap, m_window, m_gcontexts.at(gc::BL), 0, 0, 0, 0, m_bar.width, m_bar.height); + m_connection.copy_area( + m_pixmap, m_window, m_gcontexts.at(gc::BR), 0, 0, 0, 0, m_bar.width, m_bar.height); + } //}}} + + /** + * Get the bar settings container + */ + const bar_settings settings() const { // {{{ + return m_bar; + } // }}} + + /** + * Get the tray settings container + */ + const tray_settings tray() const { // {{{ + return m_tray; + } // }}} + + protected: + /** + * Handle alignment update + */ + void on_alignment_change(alignment align) { //{{{ + if (align == m_bar.align) + return; + m_log.trace("bar: alignment_change(%i)", static_cast(align)); + m_bar.align = align; + m_xpos = 0; + } //}}} + + /** + * Handle attribute on state + */ + void on_attribute_set(attribute attr) { //{{{ + int val{static_cast(attr)}; + if ((m_attributes & val) != 0) + return; + m_log.trace("bar: attribute_set(%i)", val); + m_attributes |= val; + } //}}} + + /** + * Handle attribute off state + */ + void on_attribute_unset(attribute attr) { //{{{ + int val{static_cast(attr)}; + if ((m_attributes & val) == 0) + return; + m_log.trace("bar: attribute_unset(%i)", val); + m_attributes ^= val; + } //}}} + + /** + * Handle attribute toggle state + */ + void on_attribute_toggle(attribute attr) { //{{{ + int val{static_cast(attr)}; + m_log.trace("bar: attribute_toggle(%i)", val); + m_attributes ^= val; + } //}}} + + /** + * Handle action block start + */ + void on_action_block_open(mousebtn btn, string cmd) { //{{{ + if (btn == mousebtn::NONE) + btn = mousebtn::LEFT; + m_log.trace("bar: action_block_open(%i, %s)", static_cast(btn), cmd); + action_block action; + action.active = true; + action.mousebtn = btn; + action.start_x = m_xpos; + action.command = cmd; + action.command = string_util::replace_all(action.command, ":", "\\:"); + m_actions.emplace_back(action); + } //}}} + + /** + * Handle action block end + */ + void on_action_block_close(mousebtn btn) { //{{{ + m_log.trace("bar: action_block_close(%i)", static_cast(btn)); + auto n_actions = m_actions.size(); + while (n_actions--) { + action_block& action = m_actions[n_actions]; + if (!action.active || action.mousebtn != btn) + continue; + action.end_x = m_xpos; + action.active = false; + } + } //}}} + + /** + * Handle color change + */ + void on_color_change(gc gc_, color color_) { //{{{ + m_log.trace( + "bar: color_change(%i, %s -> %s)", static_cast(gc_), color_.hex(), color_.rgb()); + + const uint32_t value_list[32]{color_.value()}; + m_connection.change_gc(m_gcontexts.at(gc_), XCB_GC_FOREGROUND, &value_list); + + if (gc_ == gc::FG) + m_fontmanager->allocate_color(color_); + } //}}} + + /** + * Handle font change + */ + void on_font_change(int index) { //{{{ + m_log.trace("bar: font_change(%i)", index); + m_fontmanager->set_preferred_font(index); + } //}}} + + /** + * Handle pixel offsetting + */ + void on_pixel_offset(int px) { //{{{ + m_log.trace("bar: pixel_offset(%i)", px); + draw_shift(m_xpos, px); + m_xpos += px; + } //}}} + + /** + * Proess systray report + */ + void on_tray_report(uint16_t slots) { // {{{ + if (m_tray.slots == slots) + return; + + m_log.trace("bar: tray_report(%lu)", slots); + m_tray.slots = slots; + + if (!m_prevdata.empty()) + parse(m_prevdata, true); + } // }}} + + /** + * Draw background onto the pixmap + */ + void draw_background() { //{{{ + draw_util::fill( + m_connection, m_pixmap, m_gcontexts.at(gc::BG), 0, 0, m_bar.width, m_bar.height); + } //}}} + + /** + * Draw borders onto the pixmap + */ + void draw_border(border border_) { //{{{ + switch (border_) { + case border::NONE: + break; + + case border::TOP: + if (m_borders[border::TOP].size > 0) { + draw_util::fill(m_connection, m_pixmap, m_gcontexts.at(gc::BT), + m_borders[border::LEFT].size, 0, + m_bar.width - m_borders[border::LEFT].size - m_borders[border::RIGHT].size, + m_borders[border::TOP].size); + } + break; + + case border::BOTTOM: + if (m_borders[border::BOTTOM].size > 0) { + draw_util::fill(m_connection, m_pixmap, m_gcontexts.at(gc::BB), + m_borders[border::LEFT].size, m_bar.height - m_borders[border::BOTTOM].size, + m_bar.width - m_borders[border::LEFT].size - m_borders[border::RIGHT].size, + m_borders[border::BOTTOM].size); + } + break; + + case border::LEFT: + if (m_borders[border::LEFT].size > 0) { + draw_util::fill(m_connection, m_pixmap, m_gcontexts.at(gc::BL), 0, 0, + m_borders[border::LEFT].size, m_bar.height); + } + break; + + case border::RIGHT: + if (m_borders[border::RIGHT].size > 0) { + draw_util::fill(m_connection, m_pixmap, m_gcontexts.at(gc::BR), + m_bar.width - m_borders[border::RIGHT].size, 0, m_borders[border::RIGHT].size, + m_bar.height); + } + break; + + case border::ALL: + draw_border(border::TOP); + draw_border(border::BOTTOM); + draw_border(border::LEFT); + draw_border(border::RIGHT); + break; + } + } //}}} + + /** + * Draw over- and underline onto the pixmap + */ + void draw_lines(int x, int w) { //{{{ + if (!m_bar.lineheight) + return; + + if (m_attributes & static_cast(attribute::o)) + draw_util::fill(m_connection, m_pixmap, m_gcontexts.at(gc::OL), x, + m_borders[border::TOP].size, w, m_bar.lineheight); + + if (m_attributes & static_cast(attribute::u)) + draw_util::fill(m_connection, m_pixmap, m_gcontexts.at(gc::UL), x, + m_bar.height - m_borders[border::BOTTOM].size - m_bar.lineheight, w, m_bar.lineheight); + } //}}} + + /** + * Shift the contents of the pixmap horizontally + */ + int draw_shift(int x, int chr_width) { //{{{ + int delta = 0; + + switch (m_bar.align) { + case alignment::CENTER: + m_connection.copy_area(m_pixmap, m_pixmap, m_gcontexts.at(gc::FG), m_bar.width / 2 - x / 2, + 0, m_bar.width / 2 - (x + chr_width) / 2, 0, x, m_bar.height); + x = m_bar.width / 2 - (x + chr_width) / 2 + x; + delta = chr_width / 2; + break; + case alignment::RIGHT: + m_connection.copy_area(m_pixmap, m_pixmap, m_gcontexts.at(gc::FG), m_bar.width - x, 0, + m_bar.width - x - chr_width, 0, x, m_bar.height); + x = m_bar.width - chr_width - m_borders[border::RIGHT].size; + delta = chr_width; + break; + + default: + break; + } + + draw_util::fill(m_connection, m_pixmap, m_gcontexts.at(gc::BG), x, 0, chr_width, m_bar.height); + + if (delta != 0 && !m_actions.empty()) { + for (auto&& action : m_actions) { + if (action.active || action.align != m_bar.align) + continue; + action.start_x -= delta; + action.end_x -= delta; + } + } + + return x; + } //}}} + + /** + * Draw text contents + */ + void draw_character(uint16_t character) { // {{{ + // TODO: cache + auto& font = m_fontmanager->match_char(character); + + if (!font) { + // m_log.warn("No suitable font found"); + return; + } + + if (font->ptr && font->ptr != m_gcfont) { + m_gcfont = font->ptr; + m_fontmanager->set_gcontext_font(m_gcontexts.at(gc::FG), m_gcfont); + } + + // TODO: cache + auto chr_width = m_fontmanager->char_width(font, character); + + auto x = draw_shift(m_xpos, chr_width); + auto y = m_bar.vertical_mid + font->height / 2 - font->descent + font->offset_y; + + // m_log.trace("Draw char(%c, width: %i) at pos(%i,%i)", character, chr_width, x, y); + + if (font->xft != nullptr) { + auto color = m_fontmanager->xftcolor(); + XftDrawString16(m_xftdraw, &color, font->xft, x, y, &character, 1); + } else { + character = (character >> 8) | (character << 8); + draw_util::xcb_poly_text_16_patched( + m_connection, m_pixmap, m_gcontexts.at(gc::FG), x, y, 1, &character); + } + + draw_lines(x, chr_width); + m_xpos += chr_width; + } // }}} + + private: + connection& m_connection; + const config& m_conf; + const logger& m_log; + unique_ptr m_fontmanager; + + threading_util::spin_lock m_lock; + + xcb_screen_t* m_screen; + xcb_visualtype_t* m_visual; + + window m_window{m_connection}; + colormap m_colormap{m_connection, m_connection.generate_id()}; + pixmap m_pixmap{m_connection, m_connection.generate_id()}; + + bar_settings m_bar; + tray_settings m_tray; + map m_borders; + map m_gcontexts; + vector m_actions; + + string m_prevdata; + int m_xpos{0}; + int m_attributes{0}; + + xcb_font_t m_gcfont{0}; + XftDraw* m_xftdraw; +}; + +LEMONBUDDY_NS_END diff --git a/include/components/builder.hpp b/include/components/builder.hpp new file mode 100644 index 00000000..ebf63f49 --- /dev/null +++ b/include/components/builder.hpp @@ -0,0 +1,463 @@ +#pragma once + +#include "common.hpp" +#include "components/config.hpp" +#include "components/types.hpp" +#include "config.hpp" +#include "drawtypes/label.hpp" +#include "utils/math.hpp" +#include "utils/string.hpp" + +LEMONBUDDY_NS + +#define DEFAULT_SPACING -1 + +#ifndef BUILDER_SPACE_TOKEN +#define BUILDER_SPACE_TOKEN "%__" +#endif + +using namespace drawtypes; + +class builder { + public: + explicit builder(const bar_settings bar, bool lazy = true) : m_bar(bar), m_lazy(lazy) {} + + void set_lazy(bool mode) { + m_lazy = mode; + } + + string flush() { + if (m_lazy) { + while (m_counters[syntaxtag::A] > 0) cmd_close(true); + while (m_counters[syntaxtag::B] > 0) background_close(true); + while (m_counters[syntaxtag::F] > 0) color_close(true); + while (m_counters[syntaxtag::T] > 0) font_close(true); + while (m_counters[syntaxtag::U] > 0) line_color_close(true); + while (m_counters[syntaxtag::u] > 0) underline_close(true); + while (m_counters[syntaxtag::o] > 0) overline_close(true); + } + + string output = m_output.data(); + + // reset values + m_output.clear(); + for (auto& counter : m_counters) counter.second = 0; + for (auto& value : m_colors) value.second = ""; + m_fontindex = 1; + + return string_util::replace_all(output, string{BUILDER_SPACE_TOKEN}, " "); + } + + void append(string text) { + string str(text); + auto len = str.length(); + if (len > 2 && str[0] == '"' && str[len - 1] == '"') + m_output += str.substr(1, len - 2); + else + m_output += str; + } + + void node(string str, bool add_space = false) { + string::size_type n, m; + string s(str); + + while (true) { + if (s.empty()) { + break; + + } else if ((n = s.find("%{F-}")) == 0) { + color_close(!m_lazy); + s.erase(0, 5); + + } else if ((n = s.find("%{F#")) == 0 && (m = s.find("}")) != string::npos) { + if (m - n - 4 == 2) + color_alpha(s.substr(n + 3, m - 3)); + else + color(s.substr(n + 3, m - 3)); + s.erase(n, m + 1); + + } else if ((n = s.find("%{B-}")) == 0) { + background_close(!m_lazy); + s.erase(0, 5); + + } else if ((n = s.find("%{B#")) == 0 && (m = s.find("}")) != string::npos) { + background(s.substr(n + 3, m - 3)); + s.erase(n, m + 1); + + } else if ((n = s.find("%{T-}")) == 0) { + font_close(!m_lazy); + s.erase(0, 5); + + } else if ((n = s.find("%{T")) == 0 && (m = s.find("}")) != string::npos) { + font(std::atoi(s.substr(n + 3, m - 3).c_str())); + s.erase(n, m + 1); + + } else if ((n = s.find("%{U-}")) == 0) { + line_color_close(!m_lazy); + s.erase(0, 5); + + } else if ((n = s.find("%{U#")) == 0 && (m = s.find("}")) != string::npos) { + line_color(s.substr(n + 3, m - 3)); + s.erase(n, m + 1); + + } else if ((n = s.find("%{+u}")) == 0) { + underline(); + s.erase(0, 5); + + } else if ((n = s.find("%{+o}")) == 0) { + overline(); + s.erase(0, 5); + + } else if ((n = s.find("%{-u}")) == 0) { + underline_close(true); + s.erase(0, 5); + + } else if ((n = s.find("%{-o}")) == 0) { + overline_close(true); + s.erase(0, 5); + + } else if ((n = s.find("%{A}")) == 0) { + cmd_close(true); + s.erase(0, 4); + + } else if ((n = s.find("%{")) == 0 && (m = s.find("}")) != string::npos) { + append(s.substr(n, m + 1)); + s.erase(n, m + 1); + + } else if ((n = s.find("%{")) > 0) { + append(s.substr(0, n)); + s.erase(0, n); + + } else + break; + } + + if (!s.empty()) + append(s); + if (add_space) + space(); + } + + void node(string str, int font_index, bool add_space = false) { + font(font_index); + node(str, add_space); + font_close(); + } + + // void node(progressbar_t bar, float perc, bool add_space = false) { + // if (!bar) + // return; + // node(bar->get_output(math_util::cap(0, 100, perc)), add_space); + // } + + void node(label_t label, bool add_space = false) { + if (!label || !*label) + return; + + auto text = label->m_text; + + if (label->m_maxlen > 0 && text.length() > label->m_maxlen) { + text = text.substr(0, label->m_maxlen) + "..."; + } + + if ((label->m_overline.empty() && m_counters[syntaxtag::o] > 0) || + (m_counters[syntaxtag::o] > 0 && label->m_margin > 0)) + overline_close(true); + if ((label->m_underline.empty() && m_counters[syntaxtag::u] > 0) || + (m_counters[syntaxtag::u] > 0 && label->m_margin > 0)) + underline_close(true); + + if (label->m_margin > 0) + space(label->m_margin); + + if (!label->m_overline.empty()) + overline(label->m_overline); + if (!label->m_underline.empty()) + underline(label->m_underline); + + background(label->m_background); + color(label->m_foreground); + + if (label->m_padding > 0) + space(label->m_padding); + + node(text, label->m_font, add_space); + + if (label->m_padding > 0) + space(label->m_padding); + + color_close(m_lazy && label->m_margin > 0); + background_close(m_lazy && label->m_margin > 0); + + if (!label->m_underline.empty() || (label->m_margin > 0 && m_counters[syntaxtag::u] > 0)) + underline_close(m_lazy && label->m_margin > 0); + if (!label->m_overline.empty() || (label->m_margin > 0 && m_counters[syntaxtag::o] > 0)) + overline_close(m_lazy && label->m_margin > 0); + + if (label->m_margin > 0) + space(label->m_margin); + } + + // void node(ramp_t ramp, float perc, bool add_space = false) { + // if (!ramp) + // return; + // node(ramp->get_by_percentage(math_util::cap(0, 100, perc)), add_space); + // } + + // void node(animation_t animation, bool add_space = false) { + // if (!animation) + // return; + // node(animation->get(), add_space); + // } + + void offset(int pixels = 0) { + if (pixels != 0) + tag_open('O', std::to_string(pixels)); + } + + void space(int width = DEFAULT_SPACING) { + if (width == DEFAULT_SPACING) + width = m_bar.spacing; + if (width <= 0) + return; + string str(width, ' '); + append(str); + } + + void remove_trailing_space(int width = DEFAULT_SPACING) { + if (width == DEFAULT_SPACING) + width = m_bar.spacing; + if (width <= 0) + return; + string::size_type spacing = width; + string str(spacing, ' '); + if (m_output.length() >= spacing && m_output.substr(m_output.length() - spacing) == str) + m_output = m_output.substr(0, m_output.length() - spacing); + } + + void invert() { + tag_open('R', ""); + } + + void font(int index) { + if (index <= 0 && m_counters[syntaxtag::T] > 0) + font_close(true); + if (index <= 0 || index == m_fontindex) + return; + if (m_lazy && m_counters[syntaxtag::T] > 0) + font_close(true); + + m_counters[syntaxtag::T]++; + m_fontindex = index; + tag_open('T', std::to_string(index)); + } + + void font_close(bool force = false) { + if ((!force && m_lazy) || m_counters[syntaxtag::T] <= 0) + return; + + m_counters[syntaxtag::T]--; + m_fontindex = 1; + tag_close('T'); + } + + void background(string color) { + if (color.length() == 2 || (color.find("#") == 0 && color.length() == 3)) { + color = "#" + color.substr(color.length() - 2); + auto bg = m_bar.background.hex(); + color += bg.substr(bg.length() - (bg.length() < 6 ? 3 : 6)); + } else if (color.length() >= 7 && color == "#" + string(color.length() - 1, color[1])) { + color = color.substr(0, 4); + } + + if (color.empty() && m_counters[syntaxtag::B] > 0) + background_close(true); + if (color.empty() || color == m_colors[syntaxtag::B]) + return; + if (m_lazy && m_counters[syntaxtag::B] > 0) + background_close(true); + + m_counters[syntaxtag::B]++; + m_colors[syntaxtag::B] = color; + tag_open('B', color); + } + + void background_close(bool force = false) { + if ((!force && m_lazy) || m_counters[syntaxtag::B] <= 0) + return; + + m_counters[syntaxtag::B]--; + m_colors[syntaxtag::B] = ""; + tag_close('B'); + } + + void color(string color_) { + auto color(color_); + if (color.length() == 2 || (color.find("#") == 0 && color.length() == 3)) { + color = "#" + color.substr(color.length() - 2); + auto bg = m_bar.foreground.hex(); + color += bg.substr(bg.length() - (bg.length() < 6 ? 3 : 6)); + } else if (color.length() >= 7 && color == "#" + string(color.length() - 1, color[1])) { + color = color.substr(0, 4); + } + + if (color.empty() && m_counters[syntaxtag::F] > 0) + color_close(true); + if (color.empty() || color == m_colors[syntaxtag::F]) + return; + if (m_lazy && m_counters[syntaxtag::F] > 0) + color_close(true); + + m_counters[syntaxtag::F]++; + m_colors[syntaxtag::F] = color; + tag_open('F', color); + } + + void color_alpha(string alpha_) { + auto alpha(alpha_); + string val = m_bar.foreground.hex(); + if (alpha.find("#") == std::string::npos) { + alpha = "#" + alpha; + } + + if (alpha.size() == 4) { + color(alpha); + return; + } + + if (val.size() < 6 && val.size() > 2) { + val.append(val.substr(val.size() - 3)); + } + + color((alpha.substr(0, 3) + val.substr(val.size() - 6)).substr(0, 9)); + } + + void color_close(bool force = false) { + if ((!force && m_lazy) || m_counters[syntaxtag::F] <= 0) + return; + + m_counters[syntaxtag::F]--; + m_colors[syntaxtag::F] = ""; + tag_close('F'); + } + + void line_color(string color) { + if (color.empty() && m_counters[syntaxtag::U] > 0) + line_color_close(true); + if (color.empty() || color == m_colors[syntaxtag::U]) + return; + if (m_lazy && m_counters[syntaxtag::U] > 0) + line_color_close(true); + + m_counters[syntaxtag::U]++; + m_colors[syntaxtag::U] = color; + tag_open('U', color); + } + + void line_color_close(bool force = false) { + if ((!force && m_lazy) || m_counters[syntaxtag::U] <= 0) + return; + + m_counters[syntaxtag::U]--; + m_colors[syntaxtag::U] = ""; + tag_close('U'); + } + + void overline(string color = "") { + if (!color.empty()) + line_color(color); + if (m_counters[syntaxtag::o] > 0) + return; + + m_counters[syntaxtag::o]++; + append("%{+o}"); + } + + void overline_close(bool force = false) { + if ((!force && m_lazy) || m_counters[syntaxtag::o] <= 0) + return; + + m_counters[syntaxtag::o]--; + append("%{-o}"); + } + + void underline(string color = "") { + if (!color.empty()) + line_color(color); + if (m_counters[syntaxtag::u] > 0) + return; + + m_counters[syntaxtag::u]++; + append("%{+u}"); + } + + void underline_close(bool force = false) { + if ((!force && m_lazy) || m_counters[syntaxtag::u] <= 0) + return; + + m_counters[syntaxtag::u]--; + append("%{-u}"); + } + + void cmd(mousebtn index, string action, bool condition = true) { + int button = static_cast(index); + + if (!condition || action.empty()) + return; + + action = string_util::replace_all(action, ":", "\\:"); + action = string_util::replace_all(action, "$", "\\$"); + action = string_util::replace_all(action, "}", "\\}"); + action = string_util::replace_all(action, "{", "\\{"); + action = string_util::replace_all(action, "%", "\x0025"); + + append("%{A" + std::to_string(button) + ":" + action + ":}"); + m_counters[syntaxtag::A]++; + } + + void cmd_close(bool force = false) { + if (m_counters[syntaxtag::A] > 0 || force) + append("%{A}"); + if (m_counters[syntaxtag::A] > 0) + m_counters[syntaxtag::A]--; + } + + protected: + void tag_open(char tag, string value) { + append("%{" + string({tag}) + value + "}"); + } + + void tag_close(char tag) { + append("%{" + string({tag}) + "-}"); + } + + private: + const bar_settings m_bar; + + string m_output; + bool m_lazy = true; + + map m_counters{ + // clang-format off + {syntaxtag::A, 0}, + {syntaxtag::B, 0}, + {syntaxtag::F, 0}, + {syntaxtag::T, 0}, + {syntaxtag::U, 0}, + {syntaxtag::O, 0}, + {syntaxtag::R, 0}, + // clang-format on + }; + + map m_colors{ + // clang-format off + {syntaxtag::B, ""}, + {syntaxtag::F, ""}, + {syntaxtag::U, ""}, + // clang-format on + }; + + int m_fontindex = 1; +}; + +LEMONBUDDY_NS_END; diff --git a/include/components/command_line.hpp b/include/components/command_line.hpp new file mode 100644 index 00000000..f83789dc --- /dev/null +++ b/include/components/command_line.hpp @@ -0,0 +1,229 @@ +#pragma once + +#include +#include +#include +#include + +#include "common.hpp" + +LEMONBUDDY_NS + +namespace command_line { + + class option; + using choices = vector; + using options = vector