├── .editorconfig ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── batsignal.1.in ├── batsignal.service ├── battery.c ├── battery.h ├── main.c ├── main.h ├── notify.c ├── notify.h ├── options.c ├── options.h └── test ├── Dockerfile.arch ├── Dockerfile.debian-stable ├── Dockerfile.debian-testing ├── Dockerfile.fedora-latest └── Dockerfile.ubuntu-latest /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | 12 | # Tab indentation (no size specified) 13 | [Makefile] 14 | indent_style = tab 15 | 16 | [*.{c,h,cpp,hpp}] 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | batsignal 2 | batsignal.1 3 | *.o 4 | *.tar.gz 5 | *.supp 6 | .clang-format 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2018-2024 Corey Hinshaw 4 | Copyright (c) 2016-2017 Aaron Marcher 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 16 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .POSIX: 2 | 3 | TARGET = batsignal 4 | 5 | CC.$(CC)=$(CC) 6 | CC.=cc 7 | CC.c99=cc 8 | CC:=$(CC.$(CC)) 9 | 10 | RM = rm -f 11 | INSTALL = install 12 | SED = sed 13 | GREP = grep 14 | CUT = cut 15 | 16 | VERSION != $(GREP) VERSION main.h | $(CUT) -d \" -f2 17 | PROGNAME != $(GREP) PROGNAME main.h | $(CUT) -d \" -f2 18 | PROGUPPER != $(GREP) PROGUPPER main.h | $(CUT) -d \" -f2 19 | 20 | PREFIX = /usr/local 21 | 22 | MANPREFIX.$(PREFIX)=$(PREFIX)/share/man 23 | MANPREFIX./usr/local=/usr/local/man 24 | MANPREFIX.=/usr/share/man 25 | MANPREFIX=$(MANPREFIX.$(PREFIX)) 26 | 27 | INCLUDES != pkg-config --cflags libnotify 28 | CFLAGS_EXTRA = -pedantic -Wall -Wextra -Werror -Wno-unused-parameter -Os 29 | CFLAGS := $(CFLAGS_EXTRA) $(INCLUDES) $(CFLAGS) 30 | 31 | LIBS != pkg-config --libs libnotify 32 | LIBS := $(LIBS) -lm 33 | LDFLAGS_EXTRA = -s 34 | LDFLAGS := $(LDFLAGS_EXTRA) $(LDFLAGS) 35 | 36 | SRC = main.c options.c battery.c notify.c 37 | OBJ = $(SRC:.c=.o) 38 | HDR = $(SRC:.c=.h) 39 | 40 | .PHONY: all install install-service clean test compile-test 41 | 42 | all: $(TARGET) $(TARGET).1 43 | 44 | $(TARGET): $(OBJ) 45 | $(CC) -o $(TARGET) $(LDFLAGS) $(OBJ) $(LIBS) 46 | 47 | %.o: $(HDR) 48 | 49 | $(TARGET).1: $(TARGET).1.in main.h 50 | $(SED) "s/VERSION/$(VERSION)/g" < $(TARGET).1.in | $(SED) "s/PROGNAME/$(PROGNAME)/g" | $(SED) "s/PROGUPPER/$(PROGUPPER)/g" > $@ 51 | 52 | install: all 53 | @echo Installing in $(DESTDIR)$(PREFIX) 54 | $(INSTALL) -d $(DESTDIR)$(PREFIX)/bin 55 | $(INSTALL) -d $(DESTDIR)$(MANPREFIX)/man1 56 | $(INSTALL) -m 0755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/ 57 | $(INSTALL) -m 0644 $(TARGET).1 $(DESTDIR)$(MANPREFIX)/man1/ 58 | 59 | install-service: install 60 | $(INSTALL) -d $(DESTDIR)$(PREFIX)/lib/systemd/user 61 | $(INSTALL) -m 0644 $(TARGET).service $(DESTDIR)$(PREFIX)/lib/systemd/user/ 62 | 63 | uninstall: 64 | @echo Removing files from $(DESTDIR)$(PREFIX) 65 | $(RM) $(DESTDIR)$(PREFIX)/bin/$(TARGET) 66 | $(RM) $(DESTDIR)$(MANPREFIX)/man1/$(TARGET).1 67 | $(RM) $(DESTDIR)$(PREFIX)/lib/systemd/user/$(TARGET).service 68 | 69 | clean-all: clean clean-images 70 | 71 | clean: 72 | @echo Cleaning build files 73 | $(RM) $(TARGET) $(OBJ) $(TARGET).1 74 | 75 | clean-images: arch-clean debian-stable-clean debian-testing-clean ubuntu-latest-clean fedora-latest-clean 76 | 77 | %-clean: test/Dockerfile.% 78 | @echo Removing images for $* 79 | -docker container prune --force --filter="label=$(TARGET)-$*" 80 | -docker rmi -f $(TARGET)-$* 81 | 82 | test: compile-test 83 | 84 | compile-test: arch-test debian-stable-test debian-testing-test ubuntu-latest-test fedora-latest-test 85 | @echo Completed compile testing 86 | 87 | %-test: test/Dockerfile.% 88 | @echo Starting compile test for $* 89 | docker build --label=$(TARGET)-$* --tag=$(TARGET)-$* --file=$< . 90 | docker run -it $(TARGET)-$* 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | batsignal - Simple battery monitor 2 | ================================== 3 | batsignal is a lightweight battery daemon written in C that notifies the user 4 | about various battery states. It is intended for minimal window managers, but 5 | can be used in any environment that supports desktop notifications via 6 | libnotify. 7 | 8 | Features 9 | -------- 10 | Customizable messages via libnotify on three configurable battery levels. A 11 | fourth configurable battery level will execute an arbitrary system command. In 12 | order to use as few system resources as possible, fewer battery checks are 13 | performed while the battery is discharging and the level of charge is not near a 14 | warning level. 15 | 16 | By default, batsignal will attempt to detect the correct battery to monitor. 17 | 18 | Requirements 19 | ------------ 20 | Batsignal requires the following software to build: 21 | 22 | * C compiler 23 | * libnotify 24 | * make 25 | * pkg-config 26 | 27 | Installation 28 | ------------ 29 | Run the following command to build and install batsignal: 30 | 31 | $ make 32 | $ sudo make install 33 | 34 | Usage 35 | ----- 36 | See `man batsignal` for details. 37 | 38 | Configuration 39 | ------------- 40 | `batsignal` is configured using command options - see `man batsignal` for a 41 | complete list. these options can be passed manually from the terminal, added 42 | to the command invocation in shell profile scripts, or during window manager 43 | initialization. 44 | 45 | ### Systemd 46 | A systemd user service can be installed by running: 47 | 48 | $ make install-service 49 | 50 | And can be enabled and started with: 51 | 52 | $ sudo systemctl daemon-reload 53 | $ systemctl --user enable batsignal.service 54 | $ systemctl --user start batsignal.service 55 | 56 | The service unit starts `batsignal` with default options. To customize the 57 | options used by the service, create a drop-in file that overrides `ExecStart`. 58 | For example: 59 | 60 | $ mkdir -p ~/.config/systemd/user/batsignal.service.d 61 | $ printf '[Service]\nExecStart=\nExecStart=batsignal -c 10 -w 30 -f 97' > ~/.config/systemd/user/batsignal.service.d/options.conf 62 | 63 | Authors 64 | ------- 65 | batsignal is written by Corey Hinshaw. It was originally forked from juiced by 66 | Aaron Marcher. 67 | 68 | License and Copyright 69 | --------------------- 70 | Copyright (c) 2018-2024 Corey Hinshaw 71 | Copyright (c) 2016-2017 Aaron Marcher 72 | 73 | Permission to use, copy, modify, and/or distribute this software for any 74 | purpose with or without fee is hereby granted, provided that the above 75 | copyright notice and this permission notice appear in all copies. 76 | 77 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 78 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 79 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 80 | SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 81 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 82 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 83 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 84 | -------------------------------------------------------------------------------- /batsignal.1.in: -------------------------------------------------------------------------------- 1 | .TH PROGUPPER 1 PROGNAME\-VERSION 2 | .SH NAME 3 | PROGNAME \- battery monitor daemon 4 | .SH SYNOPSIS 5 | .B PROGNAME 6 | .RB [ OPTIONS ] 7 | .SH DESCRIPTION 8 | PROGNAME is a lightweight battery daemon written in C that notifies the user about battery states. 9 | It is intended for minimal window managers, but can be used in any environment that supports desktop notifications via libnotify. 10 | .SH OPTIONS 11 | .TP 12 | .B \-h 13 | Display help 14 | .TP 15 | .B \-v 16 | Display version information 17 | .TP 18 | .B \-b 19 | Run PROGNAME as background daemon 20 | .TP 21 | .B \-o 22 | Check battery once and exit 23 | .TP 24 | .B \-i 25 | Ignore missing battery errors 26 | .TP 27 | .B \-e 28 | Cause notifications to expire 29 | .TP 30 | .B \-N 31 | Disable desktop notifications 32 | .TP 33 | .B \-w LEVEL 34 | Battery warning LEVEL (default 15). 0 disables this level 35 | .TP 36 | .B \-c LEVEL 37 | Critical battery LEVEL (default 5). 0 disables this level 38 | .TP 39 | .B \-d LEVEL 40 | Battery danger LEVEL (default 2). 0 disables this level 41 | .TP 42 | .B \-f LEVEL 43 | Battery full LEVEL (default 0). 0 disables this level 44 | .TP 45 | .B \-p 46 | Show a message when the battery begins charging or discharging 47 | .TP 48 | .B \-W MESSAGE 49 | Show MESSAGE when battery is at warning level 50 | .TP 51 | .B \-C MESSAGE 52 | Show MESSAGE when battery is at critical level 53 | .TP 54 | .B \-D COMMAND 55 | Run COMMAND when battery is at danger level 56 | .TP 57 | .B \-F MESSAGE 58 | Show MESSAGE when battery is at full level 59 | .TP 60 | .B \-P MESSAGE 61 | Show MESSAGE when battery is charging, if -p option is set 62 | .TP 63 | .B \-U MESSAGE 64 | Show MESSAGE when battery is discharging, if -p option is set 65 | .TP 66 | .B \-M COMMAND 67 | Send each message using COMMAND 68 | .TP 69 | .B \-n NAME 70 | Battery device NAME - multiple batteries may be separated by commas (default BAT0) 71 | .TP 72 | .B \-m SECONDS 73 | Minimum number of SECONDS (default 60) to wait between battery checks. 74 | Settings SECONDS to 0 disables polling and waits for the USR1 signal before checking battery level. 75 | Prefixing SECONDS with a + (ex: -m +10) will force a check at SECONDS intervals, regardless of battery level. 76 | .TP 77 | .B \-a APP_NAME 78 | App name used in notifications (default: PROGNAME) 79 | .TP 80 | .B \-I ICON 81 | Display specified ICON in notifications 82 | .SH CONFIGURATION 83 | Options can be passed to PROGNAME as command arguments or placed in a configuration file. 84 | Options from the configuration file will be applied first and then may be overridden by command line as arguments. 85 | .P 86 | The configuration file should include option arguments as documented above, one argument per line. 87 | Lines beginning with # and empty lines are ignored. 88 | For example, to specify a warning level of 30 and run as a background daemon, the following configuration file could be used: 89 | .RS 90 | .P 91 | # Run in background 92 | .br 93 | -b 94 | .br 95 | # Use warning level of 30 96 | .br 97 | -w 98 | .br 99 | 30 100 | .RE 101 | .P 102 | The following paths are checked in sequence for a configuration file: 103 | .IP \[bu] 104 | .B PROGUPPER_CONFIG 105 | environment variable (if present) 106 | .IP \[bu] 107 | $HOME/.config/PROGNAME (or the config base specificed in XDG_CONFIG_HOME) 108 | .IP \[bu] 109 | $HOME/.PROGNAME 110 | .IP \[bu] 111 | /usr/local/etc/PROGNAME 112 | .IP \[bu] 113 | /etc/PROGNAME 114 | .SH ENVIRONMENT 115 | .TP 116 | .B PROGUPPER_CONFIG 117 | Sets the option configuration file path. 118 | .TP 119 | .B XDG_CONFIG_HOME 120 | The base path for the XDG config directory. Used in the option file search. 121 | .SH SIGNALS 122 | PROGNAME responds to the following signals: 123 | .TP 124 | .B SIGUSR1 125 | Sending the process SIGUSR1 will cause an immediate battery check to be performed. 126 | .SH NOTES 127 | In most cases, PROGNAME will perform fewer battery state checks while the battery is discharging and the level of charge is not near a warning level. 128 | This frequency is affected by the multiplier (-m) option and is never less than seconds. 129 | If the "full" level (-f) is set or charge/discharge messages are enabled (-p), PROGNAME will instead check the battery state every seconds regardless of level of charge. 130 | .P 131 | If the "full" level (-f) is set, the battery full notification will be triggered at the given level of charge or when the battery status changes to full, whichever occurs first. 132 | .P 133 | The message COMMAND passed with -M is a C printf-style format string. 134 | It can use two string placeholders (%s) that will be replaced by the message text and the current battery level. 135 | Be sure to test the command - invalid format strings may cause PROGNAME to crash. 136 | .br 137 | Ex: -M "wall 'Battery warning: %s - Level is %s'" 138 | .SH COPYRIGHT 139 | Copyright 2018-2024 Corey Hinshaw 140 | .br 141 | Copyright 2016-2017 Aaron Marcher 142 | .br 143 | License: ISC License 144 | -------------------------------------------------------------------------------- /batsignal.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Battery monitor daemon 3 | Documentation=man:batsignal(1) 4 | 5 | [Service] 6 | Type=simple 7 | ExecStart=batsignal 8 | Restart=on-failure 9 | RestartSec=1 10 | 11 | [Install] 12 | WantedBy=default.target 13 | -------------------------------------------------------------------------------- /battery.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-2024 Corey Hinshaw 3 | */ 4 | 5 | #define _DEFAULT_SOURCE 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "battery.h" 15 | 16 | static char *attr_path = NULL; 17 | 18 | static void set_attributes(char *battery_name, char **now_attribute, char **full_attribute) 19 | { 20 | sprintf(attr_path, POWER_SUPPLY_SUBSYSTEM "/%s/charge_now", battery_name); 21 | if (access(attr_path, F_OK) == 0) { 22 | *now_attribute = "charge_now"; 23 | *full_attribute = "charge_full"; 24 | } else { 25 | sprintf(attr_path, POWER_SUPPLY_SUBSYSTEM "/%s/energy_now", battery_name); 26 | if (access(attr_path, F_OK) == 0) { 27 | *now_attribute = "energy_now"; 28 | *full_attribute = "energy_full"; 29 | } else { 30 | *now_attribute = "capacity"; 31 | *full_attribute = NULL; 32 | } 33 | } 34 | } 35 | 36 | static bool is_type_battery(char *name) 37 | { 38 | FILE *file; 39 | char type[11] = ""; 40 | 41 | sprintf(attr_path, POWER_SUPPLY_SUBSYSTEM "/%s/type", name); 42 | file = fopen(attr_path, "r"); 43 | if (file != NULL) { 44 | if (fscanf(file, "%10s", type) == 0) { /* Continue... */ } 45 | fclose(file); 46 | } 47 | return strcmp(type, "Battery") == 0; 48 | } 49 | 50 | static bool has_capacity_field(char *name) 51 | { 52 | FILE *file; 53 | int capacity = -1; 54 | char *now_attribute; 55 | char *full_attribute; 56 | 57 | set_attributes(name, &now_attribute, &full_attribute); 58 | 59 | if (strcmp(now_attribute, "capacity") == 0) { 60 | sprintf(attr_path, POWER_SUPPLY_SUBSYSTEM "/%s/capacity", name); 61 | file = fopen(attr_path, "r"); 62 | if (file != NULL) { 63 | if (fscanf(file, "%d", &capacity) == 0) { /* Continue... */ } 64 | fclose(file); 65 | } 66 | } else { 67 | capacity = 1; 68 | } 69 | return capacity >= 0; 70 | } 71 | 72 | static bool is_battery(char *name) 73 | { 74 | return is_type_battery(name) && has_capacity_field(name); 75 | } 76 | 77 | int find_batteries(char ***battery_names) 78 | { 79 | unsigned int path_len = strlen(POWER_SUPPLY_SUBSYSTEM) + POWER_SUPPLY_ATTR_LENGTH; 80 | unsigned int entry_name_len = 5; 81 | int battery_count = 0; 82 | DIR *dir; 83 | struct dirent *entry; 84 | 85 | attr_path = realloc(attr_path, path_len + entry_name_len); 86 | 87 | dir = opendir(POWER_SUPPLY_SUBSYSTEM); 88 | if (dir) { 89 | while ((entry = readdir(dir)) != NULL) { 90 | if (strlen(entry->d_name) > entry_name_len) { 91 | entry_name_len = strlen(entry->d_name); 92 | attr_path = realloc(attr_path, path_len + entry_name_len); 93 | if (attr_path == NULL) 94 | err(EXIT_FAILURE, "Memory allocation failed"); 95 | } 96 | 97 | if (is_battery(entry->d_name)) { 98 | *battery_names = realloc(*battery_names, sizeof(char *) * (battery_count+1)); 99 | if (*battery_names == NULL) 100 | err(EXIT_FAILURE, "Memory allocation failed"); 101 | (*battery_names)[battery_count] = strdup(entry->d_name); 102 | if ((*battery_names)[battery_count] == NULL) 103 | err(EXIT_FAILURE, "Memory allocation failed"); 104 | battery_count++; 105 | } 106 | } 107 | closedir(dir); 108 | } 109 | 110 | return battery_count; 111 | } 112 | 113 | int validate_batteries(char **battery_names, int battery_count) 114 | { 115 | unsigned int path_len = strlen(POWER_SUPPLY_SUBSYSTEM) + POWER_SUPPLY_ATTR_LENGTH; 116 | unsigned int name_len = 5; 117 | int return_value = -1; 118 | 119 | attr_path = realloc(attr_path, path_len + name_len); 120 | 121 | for (int i = 0; i < battery_count; i++) { 122 | if (strlen(battery_names[i]) > name_len) { 123 | name_len = strlen(battery_names[i]); 124 | attr_path = realloc(attr_path, path_len + name_len); 125 | if (attr_path == NULL) 126 | err(EXIT_FAILURE, "Memory allocation failed"); 127 | } 128 | if (!is_battery(battery_names[i]) && return_value < 0) { 129 | return_value = i; 130 | } 131 | } 132 | return return_value; 133 | } 134 | 135 | void update_battery_state(BatteryState *battery, bool required) 136 | { 137 | char state[15]; 138 | char *now_attribute; 139 | char *full_attribute; 140 | unsigned int tmp_now; 141 | unsigned int tmp_full; 142 | FILE *file; 143 | 144 | battery->discharging = false; 145 | battery->full = true; 146 | battery->energy_now = 0; 147 | battery->energy_full = 0; 148 | set_attributes(battery->names[0], &now_attribute, &full_attribute); 149 | 150 | /* iterate through all batteries */ 151 | for (int i = 0; i < battery->count; i++) { 152 | sprintf(attr_path, POWER_SUPPLY_SUBSYSTEM "/%s/status", battery->names[i]); 153 | file = fopen(attr_path, "r"); 154 | if (file == NULL || fscanf(file, "%12s", state) == 0) { 155 | if (required) 156 | err(EXIT_FAILURE, "Could not read %s", attr_path); 157 | battery->discharging |= 0; 158 | if (file) 159 | fclose(file); 160 | continue; 161 | } 162 | fclose(file); 163 | 164 | battery->discharging |= strcmp(state, POWER_SUPPLY_DISCHARGING) == 0; 165 | battery->full &= strcmp(state, POWER_SUPPLY_FULL) == 0; 166 | 167 | sprintf(attr_path, POWER_SUPPLY_SUBSYSTEM "/%s/%s", battery->names[i], now_attribute); 168 | file = fopen(attr_path, "r"); 169 | if (file == NULL || fscanf(file, "%u", &tmp_now) == 0) { 170 | if (required) 171 | err(EXIT_FAILURE, "Could not read %s", attr_path); 172 | if (file) 173 | fclose(file); 174 | continue; 175 | } 176 | fclose(file); 177 | 178 | if (full_attribute != NULL) { 179 | sprintf(attr_path, POWER_SUPPLY_SUBSYSTEM "/%s/%s", battery->names[i], full_attribute); 180 | file = fopen(attr_path, "r"); 181 | if (file == NULL || fscanf(file, "%u", &tmp_full) == 0) { 182 | if (required) 183 | err(EXIT_FAILURE, "Could not read %s", attr_path); 184 | if (file) 185 | fclose(file); 186 | continue; 187 | } 188 | fclose(file); 189 | } else { 190 | tmp_full = 100; 191 | } 192 | 193 | battery->energy_now += tmp_now; 194 | battery->energy_full += tmp_full; 195 | } 196 | 197 | battery->level = round(100.0 * battery->energy_now / battery->energy_full); 198 | } 199 | -------------------------------------------------------------------------------- /battery.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-2024 Corey Hinshaw 3 | */ 4 | 5 | #ifndef BATTERY_H 6 | #define BATTERY_H 7 | 8 | #include 9 | 10 | /* battery states */ 11 | #define STATE_AC 0 12 | #define STATE_DISCHARGING 1 13 | #define STATE_WARNING 2 14 | #define STATE_CRITICAL 3 15 | #define STATE_DANGER 4 16 | #define STATE_FULL 5 17 | 18 | /* system paths */ 19 | #define POWER_SUPPLY_SUBSYSTEM "/sys/class/power_supply" 20 | 21 | /* Battery state strings */ 22 | #define POWER_SUPPLY_FULL "Full" 23 | #define POWER_SUPPLY_DISCHARGING "Discharging" 24 | 25 | #define POWER_SUPPLY_ATTR_LENGTH 15 26 | 27 | /* battery information */ 28 | typedef struct BatteryState { 29 | char **names; 30 | int count; 31 | bool discharging; 32 | bool full; 33 | char state; 34 | int level; 35 | int energy_full; 36 | int energy_now; 37 | } BatteryState; 38 | 39 | int find_batteries(char ***battery_names); 40 | int validate_batteries(char **battery_names, int battery_count); 41 | void update_battery_state(BatteryState *battery, bool required); 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-2024 Corey Hinshaw 3 | * Copyright (c) 2016-2017 Aaron Marcher 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 12 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | #define _DEFAULT_SOURCE 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include "battery.h" 27 | #include "main.h" 28 | #include "notify.h" 29 | #include "options.h" 30 | 31 | void print_version() 32 | { 33 | printf("%s %s\n", PROGNAME, VERSION); 34 | } 35 | 36 | void print_help() 37 | { 38 | printf("Usage: %s [OPTIONS]\n\ 39 | \n\ 40 | Sends battery level notifications.\n\ 41 | \n\ 42 | Options:\n\ 43 | -h print this help message\n\ 44 | -v print program version information\n\ 45 | -b run as background daemon\n\ 46 | -o check battery once and exit\n\ 47 | -i ignore missing battery errors\n\ 48 | -e cause notifications to expire\n\ 49 | -N disable desktop notifications\n\ 50 | -w LEVEL battery warning LEVEL\n\ 51 | (default: 15)\n\ 52 | -c LEVEL critical battery LEVEL\n\ 53 | (default: 5)\n\ 54 | -d LEVEL battery danger LEVEL\n\ 55 | (default: 2)\n\ 56 | -f LEVEL full battery LEVEL\n\ 57 | (default: disabled)\n\ 58 | -p show message when battery begins charging/discharging\n\ 59 | -W MESSAGE show MESSAGE when battery is at warning level\n\ 60 | -C MESSAGE show MESSAGE when battery is at critical level\n\ 61 | -D COMMAND run COMMAND when battery is at danger level\n\ 62 | -F MESSAGE show MESSAGE when battery is full\n\ 63 | -P MESSAGE battery charging MESSAGE\n\ 64 | -U MESSAGE battery discharging MESSAGE\n\ 65 | -M COMMAND send each message using COMMAND\n\ 66 | -n NAME use battery NAME - multiple batteries separated by commas\n\ 67 | (default: BAT0)\n\ 68 | -m SECONDS minimum number of SECONDS to wait between battery checks\n\ 69 | 0 SECONDS disables polling and waits for USR1 signal\n\ 70 | Prefixing with a + will always check at SECONDS interval\n\ 71 | (default: 60)\n\ 72 | -a NAME app NAME used in desktop notifications\n\ 73 | (default: %s)\n\ 74 | -I ICON display specified ICON in notifications\n\ 75 | ", PROGNAME, PROGNAME); 76 | } 77 | 78 | void cleanup() 79 | { 80 | if (notify_is_initted()) { 81 | notify_uninit(); 82 | } 83 | } 84 | 85 | void signal_handler() 86 | { 87 | exit(EXIT_SUCCESS); 88 | } 89 | 90 | int main(int argc, char *argv[]) 91 | { 92 | unsigned int duration; 93 | bool previous_discharging_status; 94 | sigset_t sigs; 95 | struct timespec timeout = { .tv_sec = 0 }; 96 | int bat_index; 97 | BatteryState battery; 98 | char *config_file = NULL; 99 | int conf_argc = 0; 100 | char **conf_argv; 101 | 102 | Config config = { 103 | .daemonize = false, 104 | .run_once = false, 105 | .battery_required = true, 106 | .show_notifications = true, 107 | .show_charging_msg = false, 108 | .help = false, 109 | .version = false, 110 | .battery_names = NULL, 111 | .battery_count = 0, 112 | .multiplier = 60, 113 | .fixed = false, 114 | .warning = 15, 115 | .critical = 5, 116 | .danger = 2, 117 | .full = 0, 118 | .warningmsg = "Battery is low", 119 | .criticalmsg = "Battery is critically low", 120 | .fullmsg = "Battery is full", 121 | .chargingmsg = "Battery is charging", 122 | .dischargingmsg = "Battery is discharging", 123 | .dangercmd = "", 124 | .msgcmd = "", 125 | .appname = PROGNAME, 126 | .icon = NULL, 127 | .notification_expires = NOTIFY_EXPIRES_NEVER 128 | }; 129 | 130 | sigemptyset(&sigs); 131 | sigaddset(&sigs, SIGUSR1); 132 | atexit(cleanup); 133 | signal(SIGTERM, signal_handler); 134 | signal(SIGINT, signal_handler); 135 | sigprocmask(SIG_BLOCK, &sigs, NULL); 136 | 137 | config_file = find_config_file(); 138 | if (config_file) { 139 | conf_argv = read_config_file(config_file, &conf_argc, NULL); 140 | parse_args(conf_argc, conf_argv, &config); 141 | } 142 | parse_args(argc, argv, &config); 143 | 144 | if (config.help) { 145 | print_help(); 146 | return EXIT_SUCCESS; 147 | } else if (config.version) { 148 | print_version(); 149 | return EXIT_SUCCESS; 150 | } 151 | 152 | validate_options(&config); 153 | if (config_file) 154 | printf("Using config file: %s\n", config_file); 155 | 156 | if (config.show_notifications) 157 | notification_init(config.appname, config.icon, config.notification_expires); 158 | set_message_command(config.msgcmd); 159 | 160 | if (config.battery_count > 0) { 161 | bat_index = validate_batteries(config.battery_names, config.battery_count); 162 | if (config.battery_required && bat_index >= 0) 163 | err(EXIT_FAILURE, "Battery %s not found", config.battery_names[bat_index]); 164 | } else { 165 | config.battery_count = find_batteries(&config.battery_names); 166 | } 167 | if (config.battery_count < 1) 168 | errx(EXIT_FAILURE, "No batteries found"); 169 | 170 | printf("Using batteries: %s", config.battery_names[0]); 171 | for (int i = 1; i < config.battery_count; i++) 172 | printf(", %s", config.battery_names[i]); 173 | printf("\n"); 174 | 175 | if (config.daemonize && daemon(1, 1) < 0) { 176 | err(EXIT_FAILURE, "Failed to daemonize"); 177 | } 178 | 179 | battery.names = config.battery_names; 180 | battery.count = config.battery_count; 181 | update_battery_state(&battery, config.battery_required); 182 | 183 | for(;;) { 184 | previous_discharging_status = battery.discharging; 185 | update_battery_state(&battery, config.battery_required); 186 | duration = config.multiplier; 187 | 188 | if (battery.discharging) { /* discharging */ 189 | if (config.danger && battery.level <= config.danger) { 190 | if (battery.state != STATE_DANGER) { 191 | battery.state = STATE_DANGER; 192 | if (config.dangercmd[0] != '\0') 193 | if (system(config.dangercmd) == -1) { /* Ignore command errors... */ } 194 | } 195 | 196 | } else if (config.critical && battery.level <= config.critical) { 197 | if (battery.state != STATE_CRITICAL) { 198 | battery.state = STATE_CRITICAL; 199 | notify(config.criticalmsg, NOTIFY_URGENCY_CRITICAL, battery); 200 | } 201 | 202 | } else if (config.warning && battery.level <= config.warning) { 203 | if (!config.fixed) 204 | duration = (battery.level - config.critical) * config.multiplier; 205 | 206 | if (battery.state != STATE_WARNING) { 207 | battery.state = STATE_WARNING; 208 | notify(config.warningmsg, NOTIFY_URGENCY_NORMAL, battery); 209 | } 210 | 211 | } else { 212 | if (config.show_charging_msg && battery.discharging != previous_discharging_status) { 213 | notify(config.dischargingmsg, NOTIFY_URGENCY_NORMAL, battery); 214 | } else if (battery.state == STATE_FULL) { 215 | close_notification(); 216 | } 217 | battery.state = STATE_DISCHARGING; 218 | if (!config.fixed) 219 | duration = (battery.level - config.warning) * config.multiplier; 220 | } 221 | 222 | } else { /* charging */ 223 | if ((config.full && battery.state != STATE_FULL) && (battery.level >= config.full || battery.full)) { 224 | battery.state = STATE_FULL; 225 | notify(config.fullmsg, NOTIFY_URGENCY_NORMAL, battery); 226 | 227 | } else if (config.show_charging_msg && battery.discharging != previous_discharging_status) { 228 | battery.state = STATE_AC; 229 | notify(config.chargingmsg, NOTIFY_URGENCY_NORMAL, battery); 230 | 231 | } else { 232 | battery.state = STATE_AC; 233 | close_notification(); 234 | } 235 | } 236 | 237 | if (config.multiplier == 0) { 238 | sigwaitinfo(&sigs, NULL); 239 | } else { 240 | timeout.tv_sec = duration; 241 | sigtimedwait(&sigs, NULL, &timeout); 242 | } 243 | 244 | if (config.run_once) break; 245 | } 246 | 247 | return EXIT_SUCCESS; 248 | } 249 | -------------------------------------------------------------------------------- /main.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-2024 Corey Hinshaw 3 | */ 4 | 5 | #ifndef MAIN_H 6 | #define MAIN_H 7 | 8 | #define VERSION "1.8.0" 9 | #define PROGNAME "batsignal" 10 | #define PROGUPPER "BATSIGNAL" 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /notify.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-2024 Corey Hinshaw 3 | */ 4 | 5 | #define _DEFAULT_SOURCE 6 | #include 7 | #include 8 | #include 9 | #include "battery.h" 10 | #include "notify.h" 11 | 12 | static NotifyNotification *notification = NULL; 13 | static char *notification_icon = NULL; 14 | 15 | static char *msgcmd = NULL; 16 | static char *msgcmdbuf = NULL; 17 | 18 | void notification_init(char* appname, char *icon, int expires) 19 | { 20 | notification_icon = icon; 21 | if (!notify_init(appname)) 22 | err(EXIT_FAILURE, "Failed to initialize notifications"); 23 | notification = notify_notification_new("", NULL, icon); 24 | notify_notification_set_timeout(notification, expires); 25 | } 26 | 27 | void set_message_command(char *command) 28 | { 29 | msgcmd = command; 30 | } 31 | 32 | void notify(char *msg, NotifyUrgency urgency, BatteryState battery) 33 | { 34 | char body[20]; 35 | char level[8]; 36 | size_t needed; 37 | 38 | if (msgcmd[0] != '\0') { 39 | snprintf(level, 8, "%d", battery.level); 40 | needed = snprintf(NULL, 0, msgcmd, msg, level); 41 | msgcmdbuf = realloc(msgcmdbuf, needed + 1); 42 | if (msgcmdbuf == NULL) 43 | err(EXIT_FAILURE, "Memory allocation failed"); 44 | sprintf(msgcmdbuf, msgcmd, msg, level); 45 | if (system(msgcmdbuf) == -1) { /* Ignore command errors... */ } 46 | } 47 | 48 | if (notification && msg[0] != '\0') { 49 | sprintf(body, "Battery level: %u%%", battery.level); 50 | notify_notification_update(notification, msg, body, notification_icon); 51 | notify_notification_set_urgency(notification, urgency); 52 | notify_notification_show(notification, NULL); 53 | } 54 | } 55 | 56 | void close_notification() 57 | { 58 | notify_notification_close(notification, NULL); 59 | } 60 | -------------------------------------------------------------------------------- /notify.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-2024 Corey Hinshaw 3 | */ 4 | 5 | #ifndef NOTIFY_H 6 | #define NOTIFY_H 7 | 8 | #include 9 | #include 10 | #include "battery.h" 11 | 12 | void notification_init(char* appname, char *icon, int expires); 13 | void set_message_command(char *command); 14 | void notify(char *msg, NotifyUrgency urgency, BatteryState battery); 15 | void close_notification(); 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /options.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-2024 Corey Hinshaw 3 | */ 4 | 5 | #define _DEFAULT_SOURCE 6 | #include "options.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "main.h" 15 | 16 | static int split(char *in, char delim, char ***out) 17 | { 18 | int count = 1; 19 | 20 | if (in[0] == '\0') 21 | return 0; 22 | 23 | char *p = in; 24 | while (*p != '\0') { 25 | if (*p == delim) 26 | count++; 27 | p++; 28 | } 29 | 30 | *out = (char **)realloc(*out, sizeof(char **) * (count)); 31 | if (*out == NULL) 32 | err(EXIT_FAILURE, "Memory allocation failed"); 33 | 34 | (*out)[0] = strtok(in, &delim); 35 | for (int i = 1; i < count; i++) { 36 | char *tok = strtok(NULL, &delim); 37 | if (tok) 38 | (*out)[i] = tok; 39 | else { 40 | count--; 41 | i--; 42 | } 43 | } 44 | 45 | return count; 46 | } 47 | 48 | char* find_config_file() 49 | { 50 | char *config_file = NULL; 51 | char *config_paths[5]; 52 | char *home = getenv("HOME"); 53 | char *config_home = getenv("XDG_CONFIG_HOME"); 54 | 55 | config_paths[0] = getenv(PROGUPPER "_CONFIG"); 56 | if (config_home == NULL || config_home[0] == '\0') { 57 | config_paths[1] = malloc(strlen(home) + strlen("/.config/" PROGNAME) + 1); 58 | if (config_paths[1] == NULL) 59 | err(EXIT_FAILURE, "Memory allocation failed"); 60 | strcpy(config_paths[1], home); 61 | strcat(config_paths[1], "/.config/" PROGNAME); 62 | } else { 63 | config_paths[1] = malloc(strlen(config_home) + strlen("/" PROGNAME) + 1); 64 | if (config_paths[1] == NULL) 65 | err(EXIT_FAILURE, "Memory allocation failed"); 66 | strcpy(config_paths[1], config_home); 67 | strcat(config_paths[1], "/" PROGNAME); 68 | } 69 | config_paths[2] = malloc(strlen(home) + strlen("/." PROGNAME) + 1); 70 | if (config_paths[2] == NULL) 71 | err(EXIT_FAILURE, "Memory allocation failed"); 72 | strcpy(config_paths[2], home); 73 | strcat(config_paths[2], "/." PROGNAME); 74 | config_paths[3] = "/usr/local/etc/" PROGNAME; 75 | config_paths[4] = "/etc/" PROGNAME; 76 | 77 | for (int i = 0; i < 5; i++) { 78 | if (config_paths[i] && access(config_paths[i], F_OK) == 0) { 79 | config_file = strdup(config_paths[i]); 80 | if (config_file == NULL) 81 | err(EXIT_FAILURE, "Memory allocation failed"); 82 | } 83 | } 84 | 85 | free(config_paths[1]); 86 | free(config_paths[2]); 87 | return config_file; 88 | } 89 | 90 | char** read_config_file(char *path, int *argc, char *argv0) 91 | { 92 | char **argv; 93 | FILE *file; 94 | size_t numlines_allocated = 128; 95 | char **ptr; 96 | char *end; 97 | ssize_t numchars; 98 | size_t maxbytes = 0; 99 | size_t buffer_increment = 512 * sizeof(char*); 100 | 101 | file = fopen(path, "r"); 102 | if (file == NULL) { 103 | err(EXIT_FAILURE, "Could not read %s", path); 104 | } 105 | 106 | argv = malloc(numlines_allocated); 107 | argv[0] = argv0; 108 | *argc = 1; 109 | ptr = argv; 110 | ptr++; 111 | 112 | while((numchars = getline(ptr, &maxbytes, file)) != -1) { 113 | maxbytes = 0; 114 | end = *ptr + strlen(*ptr) - 1; 115 | while(end >= *ptr && ((unsigned char)*end == '\n' || (unsigned char)*end == '\r')) 116 | end--; 117 | end[1] = '\0'; 118 | if (*ptr[0] == '\0' || *ptr[0] == '#') 119 | continue; 120 | 121 | ptr++; (*argc)++; 122 | if((size_t)*argc > (numlines_allocated/sizeof(char *))) { 123 | argv = realloc(argv, numlines_allocated + buffer_increment); 124 | if (argv == NULL) 125 | err(EXIT_FAILURE, "Memory allocation failed"); 126 | numlines_allocated += buffer_increment; 127 | ptr = argv + *argc; 128 | } 129 | } 130 | 131 | *(ptr) = 0; 132 | argv = realloc(argv, sizeof(char*) * *argc+1); 133 | if (argv == NULL) 134 | err(EXIT_FAILURE, "Memory allocation failed"); 135 | 136 | if (file) 137 | fclose(file); 138 | return argv; 139 | } 140 | 141 | void parse_args(int argc, char *argv[], Config *config) 142 | { 143 | signed int c; 144 | optind = 1; 145 | 146 | while ((c = getopt(argc, argv, ":hvboiew:c:d:f:pW:C:D:F:P:U:M:Nn:m:a:I:")) != -1) { 147 | switch (c) { 148 | case 'h': 149 | config->help = true; 150 | break; 151 | case 'v': 152 | config->version = true; 153 | break; 154 | case 'b': 155 | config->daemonize = true; 156 | break; 157 | case 'o': 158 | config->run_once = true; 159 | break; 160 | case 'i': 161 | config->battery_required = false; 162 | break; 163 | case 'w': 164 | config->warning = strtoul(optarg, NULL, 10); 165 | break; 166 | case 'c': 167 | config->critical = strtoul(optarg, NULL, 10); 168 | break; 169 | case 'd': 170 | config->danger = strtoul(optarg, NULL, 10); 171 | break; 172 | case 'f': 173 | config->full = strtoul(optarg, NULL, 10); 174 | config->fixed = true; 175 | break; 176 | case 'p': 177 | config->show_charging_msg = 1; 178 | config->fixed = true; 179 | break; 180 | case 'W': 181 | config->warningmsg = optarg; 182 | break; 183 | case 'C': 184 | config->criticalmsg = optarg; 185 | break; 186 | case 'D': 187 | config->dangercmd = optarg; 188 | break; 189 | case 'F': 190 | config->fullmsg = optarg; 191 | break; 192 | case 'P': 193 | config->chargingmsg = optarg; 194 | break; 195 | case 'U': 196 | config->dischargingmsg = optarg; 197 | break; 198 | case 'M': 199 | config->msgcmd = optarg; 200 | break; 201 | case 'N': 202 | config->show_notifications = false; 203 | break; 204 | case 'n': 205 | config->battery_count = split(optarg, ',', &config->battery_names); 206 | break; 207 | case 'm': 208 | if (optarg[0] == '+') { 209 | config->fixed = true; 210 | config->multiplier = strtoul(optarg + 1, NULL, 10); 211 | } else { 212 | config->multiplier = strtoul(optarg, NULL, 10); 213 | } 214 | break; 215 | case 'a': 216 | config->appname = optarg; 217 | break; 218 | case 'I': 219 | config->icon = optarg; 220 | break; 221 | case 'e': 222 | config->notification_expires = NOTIFY_EXPIRES_DEFAULT; 223 | break; 224 | case '?': 225 | errx(EXIT_FAILURE, "Unknown option `-%c'.", optopt); 226 | case ':': 227 | errx(EXIT_FAILURE, "Option -%c requires an argument.", optopt); 228 | } 229 | } 230 | } 231 | 232 | void validate_options(Config *config) 233 | { 234 | int lowlvl = config->danger; 235 | char *rangemsg = "Option -%c must be between 0 and %i."; 236 | 237 | /* Sanity check numberic values */ 238 | if (config->warning > 100 || config->warning < 0) errx(EXIT_FAILURE, rangemsg, 'w', 100); 239 | if (config->critical > 100 || config->critical < 0) errx(EXIT_FAILURE, rangemsg, 'c', 100); 240 | if (config->danger > 100 || config->danger < 0) errx(EXIT_FAILURE, rangemsg, 'd', 100); 241 | if (config->full > 100 || config->full < 0) errx(EXIT_FAILURE, rangemsg, 'f', 100); 242 | if (config->multiplier < 0 || config->multiplier > 3600) errx(EXIT_FAILURE, rangemsg, 'm', 3600); 243 | 244 | /* Enssure levels are correctly ordered */ 245 | if (config->warning && config->warning <= config->critical) 246 | errx(EXIT_FAILURE, "Warning level must be greater than critical."); 247 | if (config->critical && config->critical <= config->danger) 248 | errx(EXIT_FAILURE, "Critical level must be greater than danger."); 249 | 250 | /* Find highest warning level */ 251 | if (config->warning || config->critical) 252 | lowlvl = config->warning ? config->warning : config->critical; 253 | 254 | /* Ensure the full level is higher than the warning levels */ 255 | if (config->full && config->full <= lowlvl) 256 | errx(EXIT_FAILURE, "Option -f must be greater than %i.", lowlvl); 257 | } 258 | -------------------------------------------------------------------------------- /options.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-2024 Corey Hinshaw 3 | */ 4 | 5 | #ifndef OPTIONS_H 6 | #define OPTIONS_H 7 | 8 | #include 9 | #include 10 | 11 | typedef struct Config { 12 | /* program operation options */ 13 | bool daemonize; 14 | bool run_once; 15 | bool battery_required; 16 | bool show_notifications; 17 | bool show_charging_msg; 18 | bool help; 19 | bool version; 20 | 21 | /* Battery configuration */ 22 | char **battery_names; 23 | int battery_count; 24 | 25 | /* check frequency multiplier (seconds) */ 26 | int multiplier; 27 | bool fixed; 28 | 29 | /* battery warning levels */ 30 | int warning; 31 | int critical; 32 | int danger; 33 | int full; 34 | 35 | /* messages for battery levels */ 36 | char *warningmsg; 37 | char *criticalmsg; 38 | char *fullmsg; 39 | char *chargingmsg; 40 | char *dischargingmsg; 41 | 42 | /* run this system command if battery reaches danger level */ 43 | char *dangercmd; 44 | 45 | /* run this system command to display a message */ 46 | char *msgcmd; 47 | 48 | /* app name for notification */ 49 | char *appname; 50 | 51 | /* specify the icon used in notifications */ 52 | char *icon; 53 | 54 | /* specify when the notification should expire */ 55 | int notification_expires; 56 | } Config; 57 | 58 | char* find_config_file(); 59 | char** read_config_file(char *path, int *argc, char *argv0); 60 | void parse_args(int argc, char *argv[], Config *config); 61 | void validate_options(Config *config); 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /test/Dockerfile.arch: -------------------------------------------------------------------------------- 1 | FROM archlinux:latest 2 | RUN pacman -Syu --noconfirm \ 3 | gcc \ 4 | libnotify \ 5 | make \ 6 | pkgconf \ 7 | && pacman -Scc --noconfirm 8 | COPY . /build 9 | RUN cd /build && make clean install 10 | CMD ["batsignal", "-v"] 11 | -------------------------------------------------------------------------------- /test/Dockerfile.debian-stable: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | RUN apt-get update && apt-get install -y \ 3 | gcc \ 4 | libnotify-dev \ 5 | make \ 6 | pkg-config \ 7 | && rm -rf /var/lib/apt/lists/* 8 | COPY . /build 9 | RUN cd /build && make clean install 10 | CMD ["batsignal", "-v"] 11 | -------------------------------------------------------------------------------- /test/Dockerfile.debian-testing: -------------------------------------------------------------------------------- 1 | FROM debian:testing 2 | RUN apt-get update && apt-get install -y \ 3 | gcc \ 4 | libnotify-dev \ 5 | make \ 6 | pkg-config \ 7 | && rm -rf /var/lib/apt/lists/* 8 | COPY . /build 9 | RUN cd /build && make clean install 10 | CMD ["batsignal", "-v"] 11 | -------------------------------------------------------------------------------- /test/Dockerfile.fedora-latest: -------------------------------------------------------------------------------- 1 | FROM fedora:latest 2 | RUN dnf -y install \ 3 | gcc \ 4 | libnotify \ 5 | libnotify-devel \ 6 | make \ 7 | pkg-config \ 8 | && dnf clean all 9 | COPY . /build 10 | RUN cd /build && make clean install 11 | CMD ["batsignal", "-v"] 12 | -------------------------------------------------------------------------------- /test/Dockerfile.ubuntu-latest: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | RUN apt-get update && \ 3 | DEBIAN_FRONTEND=noninteractive TZ=America/New_York \ 4 | apt-get install -y \ 5 | gcc \ 6 | libnotify-dev \ 7 | make \ 8 | pkg-config \ 9 | && rm -rf /var/lib/apt/lists/* 10 | COPY . /build 11 | RUN cd /build && make clean install 12 | CMD ["batsignal", "-v"] 13 | --------------------------------------------------------------------------------