├── .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 | 
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 |
--------------------------------------------------------------------------------