682a8af20e3e5c1b192c75bfc559e23953d01ab5
[khatus.git] / bin / khatus_loop
1 #! /bin/bash
2
3 set -e
4
5 produce_disk_space() {
6 disk_space_device="$1"
7 df --output=pcent "$disk_space_device" | awk 'NR == 2 {print $1}'
8 }
9
10 produce_net_addr_io() {
11 ip -s addr \
12 | awk \
13 -v prefixes_of_interfaces_to_show="$PREFIXES_OF_INTERFACES_TO_SHOW" \
14 '
15 BEGIN {
16 bytes_per_unit = 1024 * 1024
17 }
18
19 /^[0-9]+:/ {
20 sub(":$", "", $1)
21 sub(":$", "", $2)
22 sequence = $1
23 interface = $2
24 interfaces[sequence] = interface
25 }
26
27 /^ +inet [0-9]/ {
28 sub("/[0-9]+", "", $2)
29 addr = $2
30 addrs[interface] = addr
31 }
32
33 /^ +RX: / {transfer_direction = "r"}
34 /^ +TX: / {transfer_direction = "w"}
35
36 /^ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ *$/ {
37 io[interface, transfer_direction] = $1;
38 }
39
40 END {
41 for (seq=1; seq<=sequence; seq++) {
42 interface = interfaces[seq]
43 label = substr(interface, 1, 1)
44 if (addrs[interface]) {
45 curr_read = io[interface, "r"]
46 curr_write = io[interface, "w"]
47 print(interface, addrs[interface], curr_write, curr_read)
48 } else {
49 print(interface)
50 }
51 }
52 }'
53 }
54
55 produce_net_wifi_status() {
56 nmcli \
57 -f ACTIVE,SSID,SIGNAL \
58 -t \
59 d wifi \
60 | awk \
61 -F ':' \
62 '
63 BEGIN {wifi_status = "--"}
64 $1 == "yes" {wifi_status = $2 ":" $3 "%"}
65 END {print wifi_status}
66 '
67 }
68
69 produce_bluetooth_power() {
70 echo -e 'show \n quit' \
71 | bluetoothctl \
72 | awk '
73 /^Controller / {
74 controller = $2;
75 controllers[++ctrl_count] = controller;
76 }
77 /^\t[A-Z][A-Za-z]+:/ {
78 key = $1;
79 sub(":$", "", key);
80 val = $2;
81 for (i=3; i<=NF; i++) {
82 val = val " " $i};
83 data[controller, key] = val;
84 }
85 END {
86 # Using the 1st seen controller. Should we select specific instead?
87 power_status = data[controllers[1], "Powered"];
88 if (ctrl_count > 0) {
89 if (power_status == "no") {
90 power_status = "off"
91 } else if (power_status == "yes") {
92 power_status = "on"
93 } else {
94 printf("Unexpected bluetooth power status: %s\n", power_status)\
95 > "/dev/stderr";
96 power_status = "ERROR"
97 }
98 } else {
99 power_status = "off" # TODO: Perhaps use differentiated marker?
100 }
101 printf("%s\n", power_status);
102 }'
103 }
104
105 produce_screen_brightness() {
106 screen_brightness_device_path="$1"
107 echo "\
108 $(cat $screen_brightness_device_path/max_brightness) \
109 $(cat $screen_brightness_device_path/brightness)\
110 "
111 }
112
113 produce_volume() {
114 pactl list sinks \
115 | awk '
116 /^\tMute:/ {
117 printf("%s,", $0);
118 }
119 /^\tVolume:/ {
120 for (i=2; i<=NF; i++) printf(" %s", $i);
121 }' \
122 | awk -v RS=',' '
123 /^[ \t]*Mute:/ {mute = $2}
124 /^[ \t]*front-left:/ {left = $4}
125 /^[ \t]*front-right:/ {right = $4}
126 END {
127 if (mute == "yes") {
128 print("x")
129 } else {
130 print("%s %s\n", left, right)
131 }
132 }
133 '
134 }
135
136 produce_mpd_state() {
137 echo 'status' \
138 | nc 127.0.0.1 6600 \
139 | awk '
140 {
141 status[$1] = $2
142 }
143
144 /^time: +[0-9]+:[0-9]+$/ {
145 split($2, time, ":")
146 seconds_current = time[1]
147 seconds_total = time[2]
148
149 hours = int(seconds_current / 60 / 60);
150 secs_beyond_hours = seconds_current - (hours * 60 * 60);
151 mins = int(secs_beyond_hours / 60);
152 secs = secs_beyond_hours - (mins * 60);
153 if (hours > 0) {
154 current_time = sprintf("%d:%.2d:%.2d", hours, mins, secs)
155 } else {
156 current_time = sprintf("%.2d:%.2d", mins, secs)
157 }
158
159 if (seconds_total > 0) {
160 time_percentage = (seconds_current / seconds_total) * 100
161 current_percentage = sprintf("%d%%", time_percentage)
162 } else {
163 current_percentage = "~"
164 }
165 }
166
167 END {
168 state = status["state:"]
169
170 if (state == "play") {
171 symbol = "▶"
172 } else if (state == "pause") {
173 symbol = "❚❚"
174 } else if (state == "stop") {
175 symbol = "⬛"
176 } else {
177 symbol = "--"
178 }
179
180 printf(\
181 "%s %s %s\n",
182 status["state:"], current_time, current_percentage\
183 )
184 }
185 '
186 }
187
188 produce_mpd_song() {
189 echo 'currentsong' \
190 | nc 127.0.0.1 6600 \
191 | awk '
192 /^OK/ {
193 next
194 }
195
196 {
197 key = $1
198 sub("^" key " +", "")
199 val = $0
200 data[key] = val
201 }
202
203 END {
204 name = data["Name:"]
205 title = data["Title:"]
206 file = data["file:"]
207
208 if (name) {
209 out = name
210 } else if (title) {
211 out = title
212 } else if (file) {
213 last = split(file, parts, "/")
214 out = parts[last]
215 } else {
216 out = ""
217 }
218 print out
219 }
220 '
221 }
222
223 produce_weather() {
224 weather_station_id="$1"
225 metar -d "$weather_station_id" 2>&1 \
226 | awk '
227 /METAR pattern not found in NOAA data/ {
228 failures++
229 }
230
231 /^Temperature/ {
232 celsius = $3;
233 fahrenheit = (celsius * (9 / 5)) + 32;
234 temperature = fahrenheit
235 }
236
237 END {
238 if (failures > 0) {
239 temperature = "--"
240 }
241 print temperature "°F"
242 }'
243 }
244
245 produce_datetime() {
246 date +'%a %b %d %H:%M:%S'
247 }
248
249 consume() {
250 pipe="$1"
251 debug="$2"
252 prefixes_of_net_interfaces_to_show="$3"
253 tail -f "$pipe" \
254 | stdbuf -o L awk \
255 -v opt_debug="$debug" \
256 -v opt_mpd_song_max_chars=10 \
257 -v opt_prefixes_of_net_interfaces_to_show="$prefixes_of_net_interfaces_to_show" \
258 '
259 /^in:DISK_SPACE/\
260 {
261 split_msg_parts()
262 db["disk_space_used"] = msg_body
263 }
264
265 /^in:NET_ADDR_IO/\
266 {
267 split_msg_parts()
268 set_net_addr_io()
269 }
270
271 /^in:NET_WIFI_STATUS/\
272 {
273 split_msg_parts()
274 db["net_wifi_status"] = msg_body
275 }
276
277 /^in:BLUETOOTH_POWER/\
278 {
279 split_msg_parts()
280 db["bluetooth_power"] = msg_body
281 }
282
283 /^in:SCREEN_BRIGHTNESS/\
284 {
285 split_msg_parts()
286 set_screen_brightness()
287 }
288
289 /^in:VOLUME/\
290 {
291 split_msg_parts()
292 db["volume"] = msg_body
293 }
294
295 /^in:MPD_STATE/\
296 {
297 split_msg_parts()
298 db["mpd_state"] = $1
299 db["mpd_curr_song_time"] = $2
300 db["mpd_curr_song_percent"] = $3
301 }
302
303 /^in:MPD_SONG/\
304 {
305 split_msg_parts()
306 db["mpd_curr_song_name"] = msg_body
307 }
308
309 /^in:WEATHER/\
310 {
311 split_msg_parts()
312 db["weather_temperature"] = msg_body
313 }
314
315 /^in:DATE_TIME/\
316 {
317 split_msg_parts()
318 db["datetime"] = msg_body
319 }
320
321 /^out:BAR/\
322 {
323 split_msg_parts()
324 print make_bar()
325 }
326
327
328 function set_net_addr_io( \
329 interface, address, io_curr_w, io_curr_r, io_prev_w, io_prev_r\
330 ) {
331 interface = $1
332 address = $2
333 io_curr_w = $3
334 io_curr_r = $4
335 if (interface) {
336 if (address && io_curr_w && io_curr_r) {
337 # recalculate
338 io_prev_w = net_io_curr_w[interface]
339 io_prev_r = net_io_curr_r[interface]
340
341 net_addr[interface] = address
342 net_io_curr_w[interface] = io_curr_w
343 net_io_curr_r[interface] = io_curr_r
344 net_io_diff_w[interface] = io_curr_w - io_prev_w
345 net_io_diff_r[interface] = io_curr_r - io_prev_r
346 } else {
347 # clear
348 net_addr[interface] = ""
349 net_io_curr_w[interface] = 0
350 net_io_curr_r[interface] = 0
351 net_io_diff_w[interface] = 0
352 net_io_diff_r[interface] = 0
353 }
354 }
355 }
356
357 function set_screen_brightness( max, cur) {
358 max = $1
359 cur = $2
360 db["screen_brightness"] = (cur / max) * 100
361 }
362
363 function split_msg_parts() {
364 msg_head = $1
365 sub("^" msg_head " +", "")
366 msg_body = $0
367 debug(msg_head, msg_body)
368 }
369
370 function make_bar( position, bar, sep, i, j) {
371 position[++i] = sprintf("D=[%s]", db["disk_space_used"])
372 position[++i] = make_status_net()
373 position[++i] = sprintf("B=%s", db["bluetooth_power"])
374 position[++i] = sprintf("*%d%%", db["screen_brightness"])
375 position[++i] = sprintf("(%s)", db["volume"])
376 position[++i] = make_status_mpd()
377 position[++i] = db["weather_temperature"]
378 position[++i] = db["datetime"]
379 bar = ""
380 sep = ""
381 for (j = 1; j <= i; j++) {
382 bar = bar sep position[j]
383 sep = " "
384 }
385 return bar
386 }
387
388 function make_status_net( \
389 out,
390 number_of_interfaces_to_show,
391 n,
392 array_of_prefixes_of_interfaces_to_show,
393 prefix,
394 interface,
395 label,
396 count_printed,
397 sep,
398 io_stat,
399 dw, dr,
400 bytes_per_unit\
401 ) {
402 out = ""
403 number_of_interfaces_to_show = \
404 split(\
405 opt_prefixes_of_net_interfaces_to_show,\
406 array_of_prefixes_of_interfaces_to_show,\
407 ","\
408 )
409 for (n = 1; n <= number_of_interfaces_to_show; n++) {
410 prefix = array_of_prefixes_of_interfaces_to_show[n]
411 for (interface in net_addr) {
412 if (interface ~ ("^" prefix)) {
413 label = substr(interface, 1, 1)
414 if (net_addr[interface]) {
415 bytes_per_mb = 1024 * 1024 # TODO: option
416 dw = net_io_diff_w[interface] / bytes_per_mb
417 dr = net_io_diff_r[interface] / bytes_per_mb
418 io_stat = sprintf("%0.3f▲ %0.3f▼", dw, dr)
419 } else {
420 io_stat = "--"
421 }
422 if (interface ~ "^w") {
423 label = label ":" db["net_wifi_status"]
424 }
425 if (++count_printed > 1) {
426 sep = " "
427 } else {
428 sep = ""
429 }
430 out = out sep label ":" io_stat
431 }
432 }
433 }
434 return sprintf("N[%s]", out)
435 }
436
437 function make_status_mpd( state, status) {
438 state = db["mpd_state"]
439
440 if (state == "play") {
441 status = make_status_mpd_state_known("▶")
442 } else if (state == "pause") {
443 status = make_status_mpd_state_known("❚❚")
444 } else if (state == "stop") {
445 status = make_status_mpd_state_known("⬛")
446 } else {
447 status = make_status_mpd_state_unknown("--")
448 }
449
450 return sprintf("[%s]", status)
451 }
452
453 function make_status_mpd_state_known(symbol) {
454 return sprintf(\
455 "%s %s %s %s",
456 symbol,
457 db["mpd_curr_song_time"],
458 db["mpd_curr_song_percent"],
459 substr(db["mpd_curr_song_name"], 1, opt_mpd_song_max_chars)\
460 )
461 }
462
463 function make_status_mpd_state_unknown(symbol) {
464 return sprintf("%s", symbol)
465 }
466
467 function debug(location, msg) {
468 if (opt_debug) {
469 print_error(location, msg)
470 }
471 }
472
473 function print_error(location, msg) {
474 print(location " ==> " msg) > "/dev/stderr"
475 }
476 '
477 }
478
479 produce_bar_req() {
480 echo ''
481 }
482
483 spawn() {
484 cmd="$1"
485 pipe="$2"
486 msg_head="$3"
487 interval="$4"
488 while true; do
489 $cmd | while read line; do
490 echo "${msg_head} $line" > "$pipe"
491 done
492 sleep "$interval"
493 done &
494 }
495
496 main() {
497 # Defaults
498 debug=0
499 dir_data="$HOME/.khatus"
500 weather_station_id='KJFK'
501 screen_brightness_device_name='acpi_video0'
502 prefixes_of_net_interfaces_to_show='w' # comma-separated
503 disk_space_device='/'
504
505 # User-overrides
506 long_options=''
507 long_options+='debug'
508 long_options+=',data-dir:'
509 long_options+=',weather-station:'
510 long_options+=',screen-device:'
511 long_options+=',prefixes_of_net_interfaces_to_show:'
512 long_options+=',disk_space_device:'
513 OPTS=$(
514 getopt \
515 -o 'd' \
516 -l $long_options \
517 -- "$@"
518 )
519 eval set -- "$OPTS"
520 while true
521 do
522 case "$1" in
523 -d|--debug)
524 debug=1
525 shift
526 ;;
527 --data-dir)
528 dir_data="$2"
529 shift 2
530 ;;
531 --weather-station)
532 weather_station_id="$2"
533 shift 2
534 ;;
535 --screen-device)
536 screen_brightness_device_name="$2"
537 shift 2
538 ;;
539 --prefixes_of_net_interfaces_to_show)
540 prefixes_of_net_interfaces_to_show="$2"
541 shift 2
542 ;;
543 --disk_space_device)
544 disk_space_device="$2"
545 shift 2
546 ;;
547 --)
548 shift
549 break
550 ;;
551 esac
552 done
553
554 pipe="$dir_data/khatus_data_pipe"
555 screen_brightness_device_path='/sys/class/backlight'
556 screen_brightness_device_path+="/$screen_brightness_device_name"
557
558 ( echo "Khatus starting with the following parameters:"
559 ( echo " debug|= $debug"
560 echo " dir_data|= $dir_data"
561 echo " pipe|= $pipe"
562 echo " screen_brightness_device_name|= $screen_brightness_device_name"
563 echo " screen_brightness_device_path|= $screen_brightness_device_path"
564 echo " weather_station_id|= $weather_station_id"
565 echo " prefixes_of_net_interfaces_to_show|= $prefixes_of_net_interfaces_to_show"
566 echo " disk_space_device|= $disk_space_device"
567 ) | column -ts\|
568 echo ''
569 ) >&2
570
571 mkdir -p "$dir_data"
572 rm -f "$pipe"
573 mkfifo "$pipe"
574
575 cmd_produce_screen_brightness='produce_screen_brightness'
576 cmd_produce_screen_brightness+=" $screen_brightness_device_path"
577
578 cmd_produce_weather="produce_weather $weather_station_id"
579
580 cmd_produce_disk_space="produce_disk_space $disk_space_device"
581
582 # TODO: Redirect each worker's stderr to a dedicated log file
583 spawn produce_datetime "$pipe" 'in:DATE_TIME' 1
584 spawn "$cmd_produce_screen_brightness" "$pipe" 'in:SCREEN_BRIGHTNESS' 1
585 spawn "$cmd_produce_weather" "$pipe" 'in:WEATHER' $(( 30 * 60 ))
586 spawn produce_mpd_state "$pipe" 'in:MPD_STATE' 1
587 spawn produce_mpd_song "$pipe" 'in:MPD_SONG' 1
588 spawn produce_volume "$pipe" 'in:VOLUME' 1
589 spawn produce_bluetooth_power "$pipe" 'in:BLUETOOTH_POWER' 5
590 spawn produce_net_wifi_status "$pipe" 'in:NET_WIFI_STATUS' 5
591 spawn produce_net_addr_io "$pipe" 'in:NET_ADDR_IO' 1
592 spawn "$cmd_produce_disk_space" "$pipe" 'in:DISK_SPACE' 1
593 spawn produce_bar_req "$pipe" 'out:BAR' 1
594
595 consume \
596 "$pipe" \
597 "$debug" \
598 "$prefixes_of_net_interfaces_to_show"
599 }
600
601 main $@
This page took 0.126241 seconds and 3 git commands to generate.