diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d7a66d..d51f873 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,30 @@ cmake_minimum_required(VERSION 3.24) project(cppbtbl) +set(SOURCE_DIR ${PROJECT_SOURCE_DIR}/src) + +set(CMAKE_COLOR_DIAGNOSTICS ON) + find_package(sdbus-c++ REQUIRED) -add_executable(cppbtbl cppbtbl.cpp) +include_directories(${SOURCE_DIR}) + +file(GLOB COMPILE_FILES ${SOURCE_DIR}/*.cpp) +add_executable(${PROJECT_NAME} ${COMPILE_FILES}) + target_link_libraries(cppbtbl PRIVATE SDBusCpp::sdbus-c++) +set_target_properties(${PROJECT_NAME} + PROPERTIES + LINKER_LANGUAGE CXX + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS OFF +) + +add_compile_options(-Wall -Wextra -Wshadow -Wpedantic -Wno-c++98-compat -Wfloat-conversion -Wno-unused-parameter -Wimplicit-fallthrough -Wconversion) + install( - TARGETS cppbtbl + TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_PREFIX}/bin ) diff --git a/README.md b/README.md index a5b8ab4..0f1e7a1 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,30 @@ cppbtbl depends on sdbus-c++ which itself depends on libsystemd. Regardless of t Note that if you are using systemd, you won't have any issues using cppbtbl. +## Configuration + +`cppbtbl` supports a variety of flags, which can be seen by doing `cppbtbl --help`. + +Each of these flags (in its long form) can be written inside a configuration file (with the format `name=value`) that is found inside either +`$XDG_CONFIG_HOME/cppbtbl/config` (if defined) or `$HOME/.config/cppbtbl/config`. + +An example of this config file can be as follows: + +``` +format=custom +output={icon} ({percentage}%) +icons=1,2,3,4,5 +``` + +You can also specify the `dont-follow` option here (if needed) by just typing it with no following characters. + +``` +format=custom +output={icon} ({percentage}%) +icons=1,2,3,4,5 +dont-follow +``` + ## Waybar ```json @@ -49,49 +73,34 @@ Note that if you are using systemd, you won't have any issues using cppbtbl. Change format and icons accordingly, and the module should work! -## Other status bars +## Polybar -Two extra formats are offered to make cppbtbl usable in this case too. Those formats are `icononly` and `icon+devicename`. Please note that the default icons provided are part of the FontAwesome font, and (as of right now) the only way of changing them is by editing the source code. +``` +[module/bt-battery] +type = custom/script -A better way to handle this, would be to make a script that reads `cppbtbl`'s output and re-writes it in whatever way is best for you. For example: +exec = cppbtbl -```bash -#!/usr/bin/bash -DEVICES=() -PERCENTAGES=() -timeout= -while true; do - eval "read $timeout line" - if [ "$?" -ne "0" ]; then - [ -z "$timeout" ] && break - timeout= - # timeout, flush devices info - ... - continue - fi - if [ -z "$line" ]; then - timeout= - # clear ARRAYs, all devices have been disconnected - DEVICES=(); PERCENTAGES=() - echo '' - continue - fi - - IFS=":"; read -a split <<< "${line//: /:}"; unset IFS - DEVICES[${#DEVICES}]="${split[0]}" - PERCENTAGES[${#PERCENTAGES}]="${split[1]}" - # time out read comamnd after 1 second (we supposed that if no other data is available within one second, we need to flush) - timeout="-t 1" -done < <(cppbtbl -f raw) +; This is necessary, because cppbtbl will continue running +; so let's make sure polybar knows about it +tail = true ``` -Please note that this is just an example, and it could likely be executed better. +Now, you need to define your custom options, see [#configuration](#configuration). +Please **make sure** to use `--format=custom`. +## Other status bars +In other status bars, just like we saw in Polybar, you can use `--format custom` and defining your own `--output`-format and your `--icons`-set -Or a better way would be to just modify the source code to your likings. +For example `cppbtbl --format custom --output "{icon} ({percentage}%)" --icons 'icon1,icon2,icon3'`. +Keep in mind that by default the `cppbtbl` process will always be running and listening for connection/disconnection events, therefore your bar must be +able to handle that. In case you want to use a "polling" approach, you can use the `--dont-follow` option, which will close the program once it +returned the desired output. +Please note that the "polling" approach won't be closely as efficient as the "listening" one, because too high of an interval will cause massive CPU usage, +too low and you'll get a delay between your device being connected and it being shown in your bar. ### A thank-you goes to [@Justasic](https://github.com/Justasic) for helping me a lot in making this. diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..3aea963 --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,162 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "utils.h" + +static struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"format", required_argument, NULL, 'f'}, + {"output", required_argument, NULL, 'o'}, + {"dont-follow", no_argument, NULL, 'e'}, + {"icons", required_argument, NULL, 'i'} +}; + + +std::optional get_config_home() { + if (char *home = std::getenv("XDG_CONFIG_HOME"); home != nullptr) + return home; + + if (char *home = std::getenv("HOME"); home != nullptr) + return std::filesystem::path(home) / ".config"; + + + std::cerr << "Unable to get config home. Ignoring config." << std::endl; + return std::nullopt; +} + +int _parse_format(ProgramOptions *opts) { + auto output_format = optarg_to_format(); + if (!output_format) { + std::cerr << "Invalid format!\n" + << "Valid values are: waybar, custom, raw." << std::endl; + + return 1; + } + + opts->output_format = *output_format; + return -1; +} + +int parse_config(ProgramOptions *opts) { + auto config_home = get_config_home(); + if (!config_home) return -1; + + std::filesystem::path config_file = *config_home / "cppbtbl" / "config"; + + // std::cerr << "[DEBUG] config_file_path = " << config_file << std::endl; + + if (!std::filesystem::exists(config_file)) + return -1; + + std::ifstream file(config_file); + std::string line; + + // first arg is program name, we don't need it + std::vector _argv = { "cppbtbl" }; + while (std::getline(file, line)) { + line.insert(0, "--"); + + const char *copy = strdup(line.c_str()); + // std::cerr << static_cast(line.c_str()) << ": " << line << std::endl; + _argv.push_back(std::move(copy)); + } + + // std::cerr << "[DEBUG] _argv.size() = " << _argv.size() << std::endl; + + // reset getopt + optind = 1; + int opt; + while ((opt = getopt_long_only( + _argv.size(), + const_cast(_argv.data()), + "ef:o:i:", + long_options, + nullptr + )) != -1) { + //std::cerr << "[DEBUG] getopt_long_only opt=" << (char)opt << ", optarg=" << optarg << std::endl; + switch(opt) { + case 'f': + if (opts->output_format != format_raw_default) break; + if (int e = _parse_format(opts); e != -1) + return e; + break; + case 'o': + if (opts->custom_format != nullptr) break; + // i need a copy here, because the pointer gets free'd later on + opts->custom_format = strdup(optarg); + break; + case 'e': + opts->dont_follow = true; + break; + case 'i': + if (!opts->icons.empty()) break; + opts->icons = split(optarg, ','); + break; + default: + return 1; + } + } + + // clear all duplicated strings + std::for_each( + ++_argv.begin(), + _argv.end(), + [opts](const char* ptr) { + // std::cerr << (void *)ptr << ": " << ptr << std::endl; + std::free((void *)ptr); + //std::cerr << "opts->custom_format: " << opts->custom_format << std::endl; + } + ); + + return -1; +} + +int parse_opts(int argc, char *const argv[], ProgramOptions *opts) { + + // reset getopt + optind = 1; + int opt; + while ((opt = getopt_long(argc, argv, "hef:o:i:", long_options, nullptr)) != -1) { + switch (opt) { + case 'h': + help(argv[0]); + return 0; + case 'f': + if (int e = _parse_format(opts); e != -1) + return e; + break; + case 'o': + opts->custom_format = std::move(optarg); + break; + case 'e': + opts->dont_follow = true; + break; + case 'i': + opts->icons = split(optarg, ','); + break; + default: + return 1; + } + } + if (int e = parse_config(opts); e != -1) { + std::cerr << "Error occurred while parsing config." << std::endl; + return 1; + } + + if (opts->output_format == format_custom and + (!opts->custom_format or opts->icons.empty())) { + incorrect_format_usage(argv[0]); + return 1; + } + + return -1; +} \ No newline at end of file diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..5740b1f --- /dev/null +++ b/src/config.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include + +enum OutputFormat { + format_raw_default, // when its the default value + format_raw, + format_waybar, + format_custom +}; + +struct ProgramOptions { + std::vector icons; + OutputFormat output_format = format_raw_default; + char *custom_format; + bool dont_follow; +}; + +extern int parse_opts(int argc, char *const argv[], ProgramOptions *opts); \ No newline at end of file diff --git a/cppbtbl.cpp b/src/main.cpp similarity index 60% rename from cppbtbl.cpp rename to src/main.cpp index b3a5108..a306433 100644 --- a/cppbtbl.cpp +++ b/src/main.cpp @@ -9,6 +9,9 @@ #include #include +#include "config.h" +#include "utils.h" + // battery check interval (if a device is connected) #define CHECK_INTERVAL 15 * 1000 @@ -18,35 +21,15 @@ #define PROPERTIES_IFACE "org.freedesktop.DBus.Properties" -const int icon_length = 5; -const char *icons[5] = { "", "", "", "", "" }; std::set watch_list; -enum OutputFormat { - format_waybar, - format_icon_only, - format_icon_device_name, - format_raw, - - format_invalid -}; -OutputFormat out_format = format_raw; - -const char *get_icon(int percentage) { - int icon_idx = (percentage * icon_length / 100) + 0.5; - return icons[icon_idx]; -} +// runtime options +struct ProgramOptions opts; -void replace_all(std::string &s, const std::string &search, const std::string &replace) { - for( size_t pos = 0; ; pos += replace.length() ) { - // Locate the substring to replace - pos = s.find( search, pos ); - if( pos == std::string::npos ) break; - // Replace by erasing and inserting - s.erase( pos, search.length() ); - s.insert( pos, replace ); - } +std::string get_icon(int percentage) { + int icon_idx = (percentage * opts.icons.size() / 100) + 0.5; + return opts.icons[icon_idx]; } void _get_battery_infos() { @@ -82,19 +65,27 @@ void _get_battery_infos() { std::string tooltip_str = tooltip.str(); tooltip_str.erase(tooltip_str.length() - 1); - switch (out_format) { + switch (opts.output_format) { case format_waybar: replace_all(tooltip_str, "\n", "\\n"); replace_all(tooltip_str, "\"", "\\\""); // lazy af solution, but should work std::cout << "{\"percentage\":" << least_percentage << ",\"tooltip\":\"" << tooltip_str << "\"}" << std::endl; break; - case format_icon_device_name: - std::cout << get_icon(least_percentage) << ": " << least_device_name << std::endl; - break; - case format_icon_only: - std::cout << get_icon(least_percentage) << std::endl; + case format_custom: + { + // make an std::string copy of the char* + std::string output(opts.custom_format); + std::string icon = get_icon(least_percentage); + // replace all variables + replace(output, "{icon}", icon); + replace(output, "{percentage}", std::to_string(least_percentage)); + replace(output, "{name}", least_device_name); + + std::cout << output << std::endl; + } break; + case format_raw_default: case format_raw: std::cout << tooltip_str << std::endl; break; @@ -142,56 +133,15 @@ void _device_removed_signal(sdbus::Signal &signal) { _device_removed(device_path); } -void help(char *name) { - std::cout - << "Usage: " << name << " -f [format] [-e]\n" - << "-f/--format [format] valid options: waybar, icononly, icon+devicename, raw (default: raw)\n" - << "-h/--help show this help screen\n" - << "-e/--dont-follow output info and exit" << std::endl; -} - -OutputFormat optarg_to_format() { - if (strcmp(optarg, "waybar") == 0) return format_waybar; - if (strcmp(optarg, "icononly") == 0) return format_icon_only; - if (strcmp(optarg, "icon+devicename") == 0) return format_icon_device_name; - if (strcmp(optarg, "raw") == 0) return format_raw; - - return format_invalid; -} -int main(int argc, char *argv[]) { - static struct option long_options[] = { - {"help", no_argument, 0, 'h'}, - {"format", required_argument, 0, 'f'}, - {"dont-follow", no_argument, 0, 'e'} - }; - bool dont_follow = false; - int opt; - while ((opt = getopt_long(argc, argv, "hef:", long_options, nullptr)) != -1) { - switch (opt) { - case 'h': - help(argv[0]); - return 0; - case 'f': - out_format = optarg_to_format(); - if (out_format == format_invalid) { - std::cerr << "Invalid format!\n" - << "Valid values are: waybar, icononly, icon+devicename, raw." << std::endl; - - return 1; - } - break; - case 'e': - dont_follow = true; - break; - default: - return 1; - } - } +int main(int argc, char *const *argv) { + if (int exitcode = parse_opts(argc, argv, &opts); + exitcode != -1) + return exitcode; // enumerate currently connected devices, and if applicable, add them to the watch_list - auto proxy = sdbus::createProxy(UPOWER_IFACE, UPOWER_PATH); - auto method = proxy->createMethodCall(UPOWER_IFACE, "EnumerateDevices"); + std::unique_ptr proxy = sdbus::createProxy(UPOWER_IFACE, UPOWER_PATH); + sdbus::MethodCall method = proxy->createMethodCall(UPOWER_IFACE, "EnumerateDevices"); auto reply = proxy->callMethod(method); std::vector res; @@ -202,7 +152,7 @@ int main(int argc, char *argv[]) { _device_added(obj); } - if (dont_follow) { + if (opts.dont_follow) { _get_battery_infos(); return 0; } @@ -217,7 +167,7 @@ int main(int argc, char *argv[]) { // `poll` event loop + timer for polling connected devices' battery while (true) { - auto processed = connection->processPendingRequest(); + bool processed = connection->processPendingRequest(); if (processed) continue; // Process next one @@ -234,8 +184,8 @@ int main(int argc, char *argv[]) { auto r = poll(fds, 1, timeout); - if (r < 0 && errno == EINTR) - continue; + // if (r < 0 && errno == EINTR) + // continue; } } \ No newline at end of file diff --git a/src/utils.cpp b/src/utils.cpp new file mode 100644 index 0000000..adb9e8c --- /dev/null +++ b/src/utils.cpp @@ -0,0 +1,72 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +std::optional optarg_to_format() { + if (!optarg) return std::nullopt; + + if (strcmp(optarg, "waybar") == 0) return format_waybar; + if (strcmp(optarg, "custom") == 0) return format_custom; + if (strcmp(optarg, "raw") == 0) return format_raw; + + return std::nullopt; +} + + +std::size_t replace(std::string &s, const std::string &search, const std::string &replace, size_t pos=0) { + pos = s.find(search, pos); + + if(pos == std::string::npos) + return std::string::npos; + // Replace by erasing and inserting + s.replace(pos, search.length(), replace); + + return pos; +} + +void replace_all(std::string &s, const std::string &search, const std::string &rep) { + for(size_t pos = 0; ; pos += rep.length()) { + if ((pos = replace(s, search, rep, pos)) == std::string::npos) + break; + } +} + +// Help menu +void help(char *name) { + std::cout + << "Usage: " << name << " -f [format] [-o [format]] [-e]\n" + << "-f/--format [format] valid options: waybar, custom, raw (default: raw)\n" + << "-o/--output [format] the custom format to be used if -f is 'custom',\n" + << " this must be a string, and you can include use the following variables:\n" + << " {icon}, {percentage}, {name}.\n" + << " please note that the 'icon' variable will be taken from the predefined\n" + << " list of fontawesome icons.\n" + << "-i/--icons [list] comma-separated list of icons, one of which will be used\n" + << " in proportion to the device's percentage as the {icon}\n" + << " parameter in '--output'\n" + << " example: ',,,,'\n" + << "-h/--help show this help screen\n" + << "-e/--dont-follow output info and exit" << std::endl; +} + +void incorrect_format_usage(char *name) { + std::cerr << "Can't use '--format custom' without the --output and --icons parameters.\n" + << "Example of correct usage: " << name << " --format custom --output '{icon} {percentage}' --icons ',,,,'" << std::endl; +} + +std::vector split(char* haystack, const char delim) { + std::vector tokens; + std::string item; + std::stringstream ss(haystack); + while (std::getline(ss, item, delim)) { + tokens.push_back(std::move(item)); + } + + return tokens; +} \ No newline at end of file diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..8c4280a --- /dev/null +++ b/src/utils.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include +#include + +#include "config.h" + + +extern std::optional optarg_to_format(); +extern int replace(std::string &s, const std::string &search, const std::string &replace, size_t pos=0); +extern void replace_all(std::string &s, const std::string &search, const std::string &rep); +extern void help(char *name); +extern void incorrect_format_usage(char *name); +extern std::vector split(char* haystack, const char delim);