├── .gitignore ├── LICENSE ├── Makefile ├── README.md └── sway-musli.c /.gitignore: -------------------------------------------------------------------------------- 1 | sway-musli 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sebastian Carlos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #### Start of standard makefile configuration. #### 2 | 3 | SHELL := /usr/bin/env bash 4 | LN_S := ln -sf 5 | 6 | INSTALL := install 7 | INSTALL_PROGRAM := $(INSTALL) 8 | 9 | # Root of the installation 10 | prefix := /usr/local 11 | 12 | # Root of the executables 13 | exec_prefix := $(prefix) 14 | 15 | # Executables 16 | bindir := $(exec_prefix)/bin 17 | 18 | # Set space as the recipe prefix, instead of tab 19 | # Note: It also works with multiple spaces before the recipe text 20 | empty := 21 | space := $(empty) $(empty) 22 | .RECIPEPREFIX := $(space) $(space) 23 | 24 | # Enable delete on error, which is disabled by default for legacy reasons 25 | .DELETE_ON_ERROR: 26 | 27 | #### End of standard makefile configuration. #### 28 | 29 | # Project specific absolute path 30 | srcdir := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) 31 | 32 | green := \\e[32m 33 | blue := \\e[34m 34 | bold := \\e[1m 35 | reset := \\e[0m 36 | 37 | CFLAGS ?= -O3 38 | 39 | # default to /usr/bin/gcc 40 | CC = /usr/bin/gcc 41 | 42 | .PHONY: all 43 | all: sway-musli 44 | 45 | .PHONY: install 46 | install: installdirs 47 | @echo -e $(blue)Installing ...$(reset) 48 | @$(INSTALL_PROGRAM) sway-musli $(DESTDIR)$(bindir)/sway-musli 49 | @echo -e ' 'Installing $(green)sway-musli$(reset) in $(green)$(DESTDIR)$(bindir)/$(reset)$(bold)sway-musli$(reset) 50 | @echo -e $(blue)Installing$(reset) $(green)DONE$(reset) 51 | 52 | .PHONY: installdirs 53 | installdirs: 54 | @echo -e $(blue)Creating directories ...$(reset) 55 | @mkdir -p $(DESTDIR)$(bindir) 56 | @echo -e ' 'Creating directory $(green)$(DESTDIR)$(bindir)$(reset) 57 | @echo -e $(blue)Creating directories$(reset) $(green)DONE$(reset)\\n 58 | 59 | .PHONY: uninstall 60 | uninstall: 61 | @echo -e $(blue)Uninstalling ...$(reset) 62 | @rm -f $(DESTDIR)$(bindir)/sway-musli 63 | @echo -e ' 'Deleting file $(green)sway-musli$(reset) in $(green)$(DESTDIR)$(bindir)/$(reset)$(bold)sway-musli$(reset) 64 | @echo -e $(green)Uninstalling DONE$(reset) 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sway-MÜSLI: Sway – Minimal Ültrafast Status Line 2 |

3 | 4 |

5 | 6 | ## Example 7 | ![sway-musli](https://github.com/sebastiancarlos/sway-musli/assets/88276600/b1c82f5e-b2b7-4176-ae95-da5d6ed42d04) 8 | 9 | `MyWiFi | Colemak | 65% - Charging | Fri 2023-12-01 17:01:13` 10 | 11 | ## Features 12 | - Written in C for **ültra speed**. 13 | - No dependencies. Uses sockets for communication with Sway and Linux 14 | subsystems. 15 | - Takes **1ms** to render on my machine. 16 | - Great performance even when running in **60 FPS möde**! 17 | - Supports both Sway and i3. 18 | - Easy to adapt to any status bar which supports text input. 19 | - Minimal features: 20 | - Shows wifi connection. 21 | - Shows keyboard layout. 22 | - Shows battery status. 23 | - Shows date and time (even seconds!). 24 | 25 | ## Installation 26 | 1. Clone the repo and run: 27 | ```bash 28 | make 29 | make install # if you want to have the executable in your path 30 | ``` 31 | 32 | 2. Add to your Sway/i3 config file: 33 | ``` 34 | bar { 35 | ... 36 | status_command sway-musli 37 | ... 38 | } 39 | ``` 40 | 41 | ## Usage 42 | ``` 43 | Usage: sway-musli [-1|--once] [-f|--fps ] 44 | - Print a stream of status lines to be used with swaybar. 45 | - If passed -1 or --once, print once and exit. 46 | - If passed -f or --fps, print at most times per second. 47 | - Default is 30 FPS. 48 | - Example sway config: 49 | ... 50 | bar { 51 | status_command sway-musli 52 | } 53 | ... 54 | # Note: Make sure sway-musli is in your PATH. 55 | ``` 56 | 57 | ## FPS? 58 | 59 | That's right. We are in high-performance territory now. 60 | 61 | By default, `sway-musli` runs at **30 FPS**, but you can change it with the 62 | `--fps` option. 63 | 64 | - **1 FPS** is completely acceptable, but you might want more if you add some 65 | sort of dynamic content, or if you want to see keyboard layout changes reflected 66 | immediately. 67 | - At around **60 FPS**, your processor might feel a slight tickle. If your bar 68 | is hidden by default, you might as well run it at 60 FPS because Sway/i3 won't 69 | run it while the bar is hidden. 70 | 71 | ## Notes 72 | - Assumes your network device is `wlan0`. But you can change this in the source 73 | file. 74 | - You can take this as a sample code to help you build your personal 75 | **ültrafast** Sway status line. 76 | - See also, [i3status](https://manned.org/i3status.1), 77 | [i3status-rust](https://github.com/greshake/i3status-rust), and 78 | [i3blocks](https://github.com/vivien/i3blocks) 79 | - I ate müsli while coding this 🥣 80 | - Contributions welcome! 81 | 82 | ## License 83 | MIT 84 | 85 | ## Contributing 86 | We welcome contributions of all kinds. If you have a suggestion or fix, please feel free to open an issue or pull request. 87 | -------------------------------------------------------------------------------- /sway-musli.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define IW_INTERFACE "wlan0" 13 | #define MAX_BUF 100 14 | #define OUTPUT_BUF_SIZE 100 15 | 16 | // status-line.c 17 | // - Print a line containing the WIFI SSID, keyboard layout, battery capacity, 18 | // battery status, and time. 19 | // - Made for Sway. 20 | // - Depends on swaymsg (for keyboard layout) and iwctl (SSID). 21 | // - Example: 22 | // MyWiFi | Qwerty | 80% - Charging | Sun 2021-08-22 12:00:00 23 | 24 | void read_file(const char *filename, char *buffer, size_t buffer_size) { 25 | FILE *fp = fopen(filename, "r"); 26 | if (fp == NULL) { 27 | printf("Cannot open file %s\n", filename); 28 | exit(1); 29 | } 30 | fgets(buffer, buffer_size - 1, fp); 31 | buffer[strcspn(buffer, "\n")] = 0; // remove newline 32 | fclose(fp); 33 | } 34 | 35 | char* get_time_string() { 36 | static char time_str[25]; // "aaa yyyy-mm-dd hh:mm:ss" 37 | time_t rawtime; 38 | struct tm *timeinfo; 39 | 40 | time(&rawtime); 41 | timeinfo = localtime(&rawtime); 42 | 43 | strftime(time_str, sizeof(time_str), "%a %Y-%m-%d %H:%M:%S", timeinfo); 44 | return time_str; 45 | } 46 | 47 | // adapted from https://stackoverflow.com/a/19733653/21567639 48 | void extract_wifi_ssid(char *buffer, size_t buffer_size) { 49 | static int sockfd = -1; 50 | char * id; 51 | id = (char *)malloc(sizeof(char)*IW_ESSID_MAX_SIZE+1); 52 | 53 | struct iwreq wreq; 54 | memset(&wreq, 0, sizeof(struct iwreq)); 55 | wreq.u.essid.length = IW_ESSID_MAX_SIZE+1; 56 | 57 | sprintf(wreq.ifr_name, IW_INTERFACE); 58 | 59 | // initialize socket if it hasn't been initialized yet 60 | if (sockfd == -1) { 61 | if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { 62 | exit(1); 63 | } 64 | } 65 | 66 | wreq.u.essid.pointer = id; 67 | if (ioctl(sockfd,SIOCGIWESSID, &wreq) == -1) { 68 | fprintf(stderr, "Get ESSID ioctl failed \n"); 69 | exit(2); 70 | } 71 | 72 | strncpy(buffer, (char *)wreq.u.essid.pointer, buffer_size); 73 | buffer[buffer_size - 1] = 0; 74 | 75 | // if buffer is empty, set "Not Connected" instead. 76 | if (strlen(buffer) == 0) { 77 | strncpy(buffer, "Not Connected", buffer_size); 78 | buffer[buffer_size - 1] = 0; 79 | } 80 | 81 | free(id); 82 | } 83 | 84 | void send_sway_command(int sockfd, uint32_t command_type, const char *command) { 85 | const char *magic = "i3-ipc"; // Sway IPC magic string 86 | uint32_t magic_length = strlen(magic); 87 | uint32_t command_size = strlen(command); 88 | size_t message_size = magic_length + sizeof(command_size) + sizeof(command_type) + command_size; 89 | 90 | // Allocate buffer for the entire message 91 | char *message = malloc(message_size); 92 | if (message == NULL) { 93 | perror("malloc failed"); 94 | exit(1); 95 | } 96 | 97 | // Copy the magic string, command size, command type, and command into the buffer 98 | char *buffer_ptr = message; 99 | memcpy(buffer_ptr, magic, magic_length); 100 | buffer_ptr += magic_length; 101 | memcpy(buffer_ptr, &command_size, sizeof(command_size)); 102 | buffer_ptr += sizeof(command_size); 103 | memcpy(buffer_ptr, &command_type, sizeof(command_type)); 104 | buffer_ptr += sizeof(command_type); 105 | memcpy(buffer_ptr, command, command_size); 106 | 107 | if (write(sockfd, message, message_size) != message_size) { 108 | perror("write failed"); 109 | exit(1); 110 | } 111 | 112 | // Free the allocated buffer 113 | free(message); 114 | } 115 | 116 | // A simple function to extract the keyboard layout from the swaymsg command output. 117 | void extract_keyboard_layout(char *buffer, size_t buffer_size) { 118 | static int sockfd = -1; 119 | struct sockaddr_un addr; 120 | static const char *socket_path; 121 | const char *get_inputs_command = "get_inputs"; 122 | char read_buffer[1024]; 123 | int found = 0; 124 | 125 | // get socket path on first run 126 | if (socket_path == NULL) { 127 | socket_path = getenv("SWAYSOCK"); 128 | 129 | // If not found, try I3SOCK, else bail out 130 | if (socket_path == NULL) { 131 | socket_path = getenv("I3SOCK"); 132 | if (socket_path == NULL) { 133 | fprintf(stderr, "SWAYSOCK or I3SOCK not set\n"); 134 | exit(1); 135 | } 136 | } 137 | } 138 | 139 | if (sockfd == -1) { 140 | sockfd = socket(AF_UNIX, SOCK_STREAM, 0); 141 | if (sockfd == -1) { 142 | perror("socket"); 143 | exit(1); 144 | } 145 | 146 | memset(&addr, 0, sizeof(addr)); 147 | addr.sun_family = AF_UNIX; 148 | strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); 149 | 150 | if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { 151 | perror("connect"); 152 | close(sockfd); 153 | exit(1); 154 | } 155 | } 156 | 157 | // Send "get_inputs" command to Sway 158 | const uint32_t get_inputs_command_type = 100; // IPC command type for "get_inputs" 159 | send_sway_command(sockfd, get_inputs_command_type, get_inputs_command); 160 | 161 | // Read Sway's response header 162 | size_t prefix_size = strlen("i3-ipc") + sizeof(uint32_t) + sizeof(uint32_t); 163 | ssize_t num_read = read(sockfd, read_buffer, prefix_size); 164 | if (num_read == -1) { 165 | perror("read"); 166 | close(sockfd); 167 | exit(1); 168 | } 169 | 170 | // Read Sway's response 171 | num_read = read(sockfd, read_buffer, sizeof(read_buffer)); 172 | if (num_read == -1) { 173 | perror("read"); 174 | close(sockfd); 175 | exit(1); 176 | } 177 | 178 | char *start = strstr(read_buffer, "xkb_active_layout_name"); 179 | if (start) { 180 | char *start_quote = strchr(start, '('); 181 | char *end_quote = strchr(start_quote, ')'); 182 | if (start_quote && end_quote && start_quote < end_quote) { 183 | size_t len = end_quote - start_quote - 1; 184 | if (len < buffer_size) { 185 | strncpy(buffer, start_quote + 1, len); 186 | buffer[len] = 0; // Null-terminate buffer. 187 | 188 | // if value is "US", change to "Qwerty" 189 | if (strcmp(buffer, "US") == 0) { 190 | strncpy(buffer, "Qwerty", buffer_size); 191 | buffer[buffer_size - 1] = 0; 192 | } 193 | 194 | found = 1; 195 | } 196 | } 197 | } 198 | 199 | // flush rest of response 200 | while (num_read == sizeof(read_buffer)) { 201 | num_read = read(sockfd, read_buffer, sizeof(read_buffer)); 202 | } 203 | 204 | if (!found) { 205 | strncpy(buffer, "Unknown", buffer_size); 206 | buffer[buffer_size - 1] = 0; 207 | } 208 | } 209 | 210 | void print() { 211 | char wifi[MAX_BUF]; 212 | char keyboard[MAX_BUF]; 213 | char batcap[MAX_BUF]; 214 | char batstat[MAX_BUF]; 215 | static char last_message[MAX_BUF]; 216 | char message[MAX_BUF]; 217 | 218 | extract_wifi_ssid(wifi, sizeof(wifi)); 219 | extract_keyboard_layout(keyboard, sizeof(keyboard)); 220 | read_file("/sys/class/power_supply/BAT0/capacity", batcap, sizeof(batcap)); 221 | read_file("/sys/class/power_supply/BAT0/status", batstat, sizeof(batstat)); 222 | 223 | // only print if it changed since last time 224 | sprintf(message, "%s | %s | %s%% - %s | %s", wifi, keyboard, batcap, batstat, get_time_string()); 225 | if (strcmp(message, last_message) == 0) { 226 | return; 227 | } 228 | strncpy(last_message, message, sizeof(last_message)); 229 | last_message[sizeof(last_message) - 1] = 0; 230 | printf("%s\n", message); 231 | 232 | fflush(stdout); 233 | } 234 | 235 | void usage() { 236 | printf("Usage: sway-musli [-1|--once] [-f|--fps ]\n"); 237 | printf(" - Print a stream of status lines to be used with swaybar.\n"); 238 | printf(" - If passed -1 or --once, print once and exit.\n"); 239 | printf(" - If passed -f or --fps, print at most times per second.\n"); 240 | printf(" - Default is 30.\n"); 241 | printf(" - Example sway config:\n"); 242 | printf(" ...\n"); 243 | printf(" bar {\n"); 244 | printf(" status_command sway-musli\n"); 245 | printf(" }\n"); 246 | printf(" ...\n"); 247 | printf(" # Note: Make sure sway-musli is in your PATH.\n"); 248 | } 249 | 250 | int main(int argc, char *argv[]) { 251 | // print usage on -h or --help 252 | if (argc == 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) { 253 | usage(); 254 | return 0; 255 | } 256 | 257 | // if passed argument -1 or --once, print once and exit 258 | if (argc == 2 && (strcmp(argv[1], "-1") == 0 || strcmp(argv[1], "--once") == 0)) { 259 | print(); 260 | return 0; 261 | } 262 | 263 | // parse fps 264 | int fps = 30; 265 | if (argc == 3 && (strcmp(argv[1], "-f") == 0 || strcmp(argv[1], "--fps") == 0)) { 266 | fps = atoi(argv[2]); 267 | argc = 1; 268 | } 269 | 270 | // on any other argument, print usage and exit 271 | if (argc != 1) { 272 | usage(); 273 | return 1; 274 | } 275 | 276 | while (1) { 277 | print(); 278 | // run at provided FPS 279 | usleep(1000000 / fps); 280 | } 281 | } 282 | --------------------------------------------------------------------------------