chore: refactor polybar
74da441c
19 file(s) · +747 −183
| 1 | + | ; Palette. Included at the top of config.ini, so ${colors.*} is available to |
|
| 2 | + | ; every module file. |
|
| 3 | + | ||
| 4 | + | [colors] |
|
| 5 | + | background = #121113 |
|
| 6 | + | background-alt = #333333 |
|
| 7 | + | foreground = #c1c1c1 |
|
| 8 | + | primary = #fbcb97 |
|
| 9 | + | secondary = #5f8787 |
|
| 10 | + | alert = #e78a53 |
|
| 11 | + | disabled = #888888 |
|
| 12 | + | ||
| 13 | + | ; The bar window itself is invisible -- only the islands paint anything. |
|
| 14 | + | ; Needs a compositor (xfwm4's is on) or this renders black. |
|
| 15 | + | transparent = #00000000 |
|
| 16 | + | ||
| 17 | + | ; vim:ft=dosini |
| 16 | 16 | ; |
|
| 17 | 17 | ;========================================================== |
|
| 18 | 18 | ||
| 19 | - | [colors] |
|
| 20 | - | background = #121113 |
|
| 21 | - | background-alt = #333333 |
|
| 22 | - | foreground = #c1c1c1 |
|
| 23 | - | primary = #fbcb97 |
|
| 24 | - | secondary = #5f8787 |
|
| 25 | - | alert = #e78a53 |
|
| 26 | - | disabled = #888888 |
|
| 19 | + | ; One transparent, full-width bar. The visible blocks are "islands": runs of |
|
| 20 | + | ; modules that paint their own background between a pair of rounded caps. |
|
| 21 | + | ; See modules/caps.ini for how that works and what a module owes an island. |
|
| 22 | + | ; |
|
| 23 | + | ; colors.ini the palette |
|
| 24 | + | ; fonts.ini fonts, including the oversized one the caps are drawn with |
|
| 25 | + | ; layout.ini which modules go in which island |
|
| 26 | + | ; modules/ one file per module; a file here is not enabled until it |
|
| 27 | + | ; is named in layout.ini |
|
| 28 | + | ; scripts/ backing scripts for custom/script modules |
|
| 29 | + | ; |
|
| 30 | + | ; Relative includes resolve next to this file *as installed* -- polybar does |
|
| 31 | + | ; not follow the symlink first -- so every one of them needs its own symlink |
|
| 32 | + | ; in ~/.config/polybar. |
|
| 27 | 33 | ||
| 28 | - | ; Two independent bars with a real gap between them: each is its own window, |
|
| 29 | - | ; so the middle of the screen is actually empty and clicks land on whatever |
|
| 30 | - | ; is underneath. Shared settings live in [bar/base], which is never launched |
|
| 31 | - | ; directly -- launch.sh starts "left" and "right". |
|
| 34 | + | include-file = colors.ini |
|
| 35 | + | include-directory = modules |
|
| 32 | 36 | ||
| 33 | - | [bar/base] |
|
| 37 | + | [bar/main] |
|
| 38 | + | ; 24pt = 32px at 96dpi. fonts.ini pins the cap glyph to this height, so the |
|
| 39 | + | ; two move together. |
|
| 34 | 40 | height = 24pt |
|
| 35 | - | radius = 8 |
|
| 41 | + | ; Inset 32px from both screen edges, on any monitor width. |
|
| 42 | + | width = 100%:-64px |
|
| 43 | + | offset-x = 32px |
|
| 36 | 44 | offset-y = 6pt |
|
| 37 | 45 | ||
| 38 | - | ; dpi = 96 |
|
| 39 | - | ||
| 40 | - | background = ${colors.background} |
|
| 46 | + | ; The islands are rounded, the bar is not: it is invisible. |
|
| 47 | + | radius = 0 |
|
| 48 | + | background = ${colors.transparent} |
|
| 41 | 49 | foreground = ${colors.foreground} |
|
| 42 | 50 | ||
| 43 | 51 | line-size = 3pt |
|
| 44 | 52 | ||
| 45 | - | ; Islands have visible edges now, so pad both sides evenly. A bare number |
|
| 46 | - | ; here is character cells (10.40px at this font/size), not pixels -- use an |
|
| 47 | - | ; explicit "px" suffix if you want pixel-exact. Raising this needs a matching |
|
| 48 | - | ; bump to each bar's width, since a bar cannot grow to fit its content. |
|
| 49 | - | padding = 2 |
|
| 50 | - | module-margin = 1 |
|
| 51 | - | ||
| 52 | - | separator = | |
|
| 53 | - | separator-foreground = ${colors.disabled} |
|
| 54 | - | ||
| 55 | - | font-0 = BerkeleyMono Nerd Font:size=13;2 |
|
| 53 | + | ; No bar padding, no module-margin, no separator, on purpose: space between |
|
| 54 | + | ; two modules is a see-through gap in the island. Spacing inside an island |
|
| 55 | + | ; comes from each module's own format-padding, spacing between islands from |
|
| 56 | + | ; [module/gap]. |
|
| 57 | + | padding = 0 |
|
| 58 | + | module-margin = 0 |
|
| 56 | 59 | ||
| 57 | 60 | cursor-click = pointer |
|
| 58 | 61 | cursor-scroll = ns-resize |
|
| 59 | 62 | ||
| 60 | 63 | enable-ipc = true |
|
| 61 | 64 | ||
| 62 | - | ; Widths are measured, not guessed: this font advances 10.40px per cell at |
|
| 63 | - | ; size 13 / 96dpi, and the rendered content is 110px (left) and 379px (right). |
|
| 64 | - | ; Polybar cannot fit a bar to its content, so each width is content + padding |
|
| 65 | - | ; + a little slack. Every extra workspace costs ~31px on the left. |
|
| 66 | - | [bar/left] |
|
| 67 | - | inherit = bar/base |
|
| 68 | - | width = 150px |
|
| 69 | - | offset-x = 32px |
|
| 70 | - | ||
| 71 | - | modules-left = xworkspaces |
|
| 72 | - | ||
| 73 | - | ; Slack here covers percentages reaching 3 digits (~42px) and a tray icon |
|
| 74 | - | ; or two (~40px each). offset-x is measured from the right screen edge: the |
|
| 75 | - | ; 452px is this bar's own width (420) plus the 32px margin, so the island |
|
| 76 | - | ; stays put on any monitor width. |
|
| 77 | - | [bar/right] |
|
| 78 | - | inherit = bar/base |
|
| 79 | - | width = 420px |
|
| 80 | - | offset-x = 100%:-452px |
|
| 81 | - | ||
| 82 | - | ; Only one bar may own the tray; this is it. |
|
| 83 | - | modules-right = filesystem pulseaudio memory cpu systray date |
|
| 84 | - | ||
| 85 | - | ; wm-restack = generic |
|
| 86 | - | ; wm-restack = bspwm |
|
| 87 | - | ; wm-restack = i3 |
|
| 88 | - | ||
| 89 | - | ; override-redirect = true |
|
| 90 | - | ||
| 91 | - | ; This module is not active by default (to enable it, add it to one of the |
|
| 92 | - | ; modules-* list above). |
|
| 93 | - | ; Please note that only a single tray can exist at any time. If you launch |
|
| 94 | - | ; multiple bars with this module, only a single one will show it, the others |
|
| 95 | - | ; will produce a warning. Which bar gets the module is timing dependent and can |
|
| 96 | - | ; be quite random. |
|
| 97 | - | ; For more information, see the documentation page for this module: |
|
| 98 | - | ; https://polybar.readthedocs.io/en/stable/user/modules/tray.html |
|
| 99 | - | [module/systray] |
|
| 100 | - | type = internal/tray |
|
| 101 | - | ||
| 102 | - | format-margin = 8pt |
|
| 103 | - | tray-spacing = 16pt |
|
| 104 | - | ||
| 105 | - | [module/xworkspaces] |
|
| 106 | - | type = internal/xworkspaces |
|
| 107 | - | ||
| 108 | - | label-active = %name% |
|
| 109 | - | label-active-background = ${colors.background-alt} |
|
| 110 | - | label-active-underline= ${colors.primary} |
|
| 111 | - | label-active-padding = 1 |
|
| 112 | - | ||
| 113 | - | label-occupied = %name% |
|
| 114 | - | label-occupied-padding = 1 |
|
| 115 | - | ||
| 116 | - | label-urgent = %name% |
|
| 117 | - | label-urgent-background = ${colors.alert} |
|
| 118 | - | label-urgent-padding = 1 |
|
| 119 | - | ||
| 120 | - | label-empty = %name% |
|
| 121 | - | label-empty-foreground = ${colors.disabled} |
|
| 122 | - | label-empty-padding = 1 |
|
| 123 | - | ||
| 124 | - | [module/xwindow] |
|
| 125 | - | type = internal/xwindow |
|
| 126 | - | label = %title:0:60:...% |
|
| 127 | - | ||
| 128 | - | [module/filesystem] |
|
| 129 | - | type = internal/fs |
|
| 130 | - | interval = 25 |
|
| 131 | - | ||
| 132 | - | mount-0 = / |
|
| 133 | - | ||
| 134 | - | label-mounted = %{F#fbcb97}%{F-} %percentage_used%% |
|
| 135 | - | ||
| 136 | - | label-unmounted = %mountpoint% not mounted |
|
| 137 | - | label-unmounted-foreground = ${colors.disabled} |
|
| 138 | - | ||
| 139 | - | [module/pulseaudio] |
|
| 140 | - | type = internal/pulseaudio |
|
| 141 | - | ||
| 142 | - | format-volume-prefix = " " |
|
| 143 | - | format-volume-prefix-foreground = ${colors.primary} |
|
| 144 | - | format-volume = <label-volume> |
|
| 145 | - | ||
| 146 | - | label-volume = %percentage%% |
|
| 147 | - | ||
| 148 | - | label-muted = muted |
|
| 149 | - | label-muted-foreground = ${colors.disabled} |
|
| 150 | - | ||
| 151 | - | [module/xkeyboard] |
|
| 152 | - | type = internal/xkeyboard |
|
| 153 | - | blacklist-0 = num lock |
|
| 154 | - | ||
| 155 | - | label-layout = %layout% |
|
| 156 | - | label-layout-foreground = ${colors.primary} |
|
| 157 | - | ||
| 158 | - | label-indicator-padding = 2 |
|
| 159 | - | label-indicator-margin = 1 |
|
| 160 | - | label-indicator-foreground = ${colors.background} |
|
| 161 | - | label-indicator-background = ${colors.secondary} |
|
| 162 | - | ||
| 163 | - | [module/memory] |
|
| 164 | - | type = internal/memory |
|
| 165 | - | interval = 2 |
|
| 166 | - | format-prefix = " " |
|
| 167 | - | format-prefix-foreground = ${colors.primary} |
|
| 168 | - | label = %percentage_used:2%% |
|
| 169 | - | ||
| 170 | - | [module/cpu] |
|
| 171 | - | type = internal/cpu |
|
| 172 | - | interval = 2 |
|
| 173 | - | format-prefix = " " |
|
| 174 | - | format-prefix-foreground = ${colors.primary} |
|
| 175 | - | label = %percentage:2%% |
|
| 176 | - | ||
| 177 | - | [network-base] |
|
| 178 | - | type = internal/network |
|
| 179 | - | interval = 5 |
|
| 180 | - | format-connected = <label-connected> |
|
| 181 | - | format-disconnected = <label-disconnected> |
|
| 182 | - | label-disconnected = %{F#888888} %ifname%%{F-} |
|
| 183 | - | ||
| 184 | - | [module/wlan] |
|
| 185 | - | inherit = network-base |
|
| 186 | - | interface-type = wireless |
|
| 187 | - | label-connected = %{F#fbcb97}%{F-} %essid% |
|
| 188 | - | ||
| 189 | - | [module/eth] |
|
| 190 | - | inherit = network-base |
|
| 191 | - | interface-type = wired |
|
| 192 | - | label-connected = %{F#fbcb97}%{F-} %local_ip% |
|
| 193 | - | ||
| 194 | - | [module/date] |
|
| 195 | - | type = internal/date |
|
| 196 | - | interval = 1 |
|
| 197 | - | ||
| 198 | - | date = %H:%M |
|
| 199 | - | date-alt = %Y-%m-%d %H:%M:%S |
|
| 200 | - | ||
| 201 | - | format = <label> |
|
| 202 | - | format-prefix = " " |
|
| 203 | - | format-prefix-foreground = ${colors.primary} |
|
| 204 | - | ||
| 205 | - | label = %date% |
|
| 65 | + | include-file = fonts.ini |
|
| 66 | + | include-file = layout.ini |
|
| 206 | 67 | ||
| 207 | 68 | [settings] |
|
| 208 | 69 | screenchange-reload = true |
| 1 | + | ; Included inside [bar/main], so these are bar keys. |
|
| 2 | + | ||
| 3 | + | ; %{T1} -- everything except the island caps. |
|
| 4 | + | font-0 = BerkeleyMono Nerd Font:size=13;2 |
|
| 5 | + | ||
| 6 | + | ; %{T2} -- the island cap glyphs in modules/caps.ini, and nothing else. This |
|
| 7 | + | ; is the corner-radius knob: the glyph is a half circle 1.213em tall, so at |
|
| 8 | + | ; 30pt (40px em) it stands 48px against a 32px bar and only the flat middle |
|
| 9 | + | ; shows, leaving a ~6px rounded corner. Raise the size for a squarer corner, |
|
| 10 | + | ; lower it towards 20pt to go back to a full pill. The ;7 centres the clipped |
|
| 11 | + | ; glyph on the bar; re-check it after changing either number. |
|
| 12 | + | font-1 = BerkeleyMono Nerd Font:size=30;7 |
|
| 13 | + | ||
| 14 | + | ; vim:ft=dosini |
| 1 | 1 | #!/bin/sh |
|
| 2 | - | # Start the polybar bars defined in ./config.ini (installed at |
|
| 3 | - | # ~/.config/polybar/config.ini), replacing any bars already running. |
|
| 2 | + | # Start the polybar bar defined in ./config.ini (installed at |
|
| 3 | + | # ~/.config/polybar/config.ini), replacing any bar already running. |
|
| 4 | 4 | # |
|
| 5 | - | # The layout is two islands -- "left" and "right" -- as separate bar windows |
|
| 6 | - | # with a real gap between them, so add any new bar to BARS below. |
|
| 5 | + | # There is one bar window, spanning the screen and fully transparent; the |
|
| 6 | + | # visible islands are built inside it out of modules (see modules/caps.ini). |
|
| 7 | 7 | # |
|
| 8 | 8 | # Meant to be run as the XFCE session client that used to be xfce4-panel — |
|
| 9 | 9 | # see ../xfce/polybar-panel.sh, which does that swap. |
|
| 19 | 19 | i=$((i + 1)) |
|
| 20 | 20 | done |
|
| 21 | 21 | ||
| 22 | - | BARS="left right" |
|
| 23 | - | ||
| 24 | - | for bar in $BARS; do |
|
| 25 | - | polybar "$bar" & |
|
| 26 | - | done |
|
| 22 | + | polybar main & |
|
| 27 | 23 | ||
| 28 | - | # Stay alive as the session client for as long as the bars are up. polybar |
|
| 29 | - | # re-execs in place on `polybar-msg cmd restart`, so these PIDs survive it. |
|
| 24 | + | # Stay alive as the session client for as long as the bar is up. polybar |
|
| 25 | + | # re-execs in place on `polybar-msg cmd restart`, so this PID survives it. |
|
| 30 | 26 | wait |
|
| 1 | + | ; Which modules make up which island. Included inside [bar/main]. |
|
| 2 | + | ; |
|
| 3 | + | ; Every run of modules wrapped in cap-left ... cap-right renders as one |
|
| 4 | + | ; rounded island. To split a side into two islands, close and reopen with a |
|
| 5 | + | ; gap between them: |
|
| 6 | + | ; |
|
| 7 | + | ; modules-right = cap-left cpu memory cap-right gap cap-left date cap-right |
|
| 8 | + | ; |
|
| 9 | + | ; Only one bar may own the tray, and this is the only bar. |
|
| 10 | + | ||
| 11 | + | modules-left = xworkspaces |
|
| 12 | + | modules-right = filesystem memory cpu systray pulseaudio weather date |
|
| 13 | + | ||
| 14 | + | ; vim:ft=dosini |
| 1 | + | ; The island machinery, adopted from Murzchnvok/polybar-collection. |
|
| 2 | + | ; |
|
| 3 | + | ; [bar/main] is one full-width, fully transparent window. An island is just a |
|
| 4 | + | ; run of modules that paint their own background, opened and closed by these |
|
| 5 | + | ; two text modules: a half circle drawn in the island colour at font-1 |
|
| 6 | + | ; (%{T2}). The result is a rounded block only as wide as what is inside it -- |
|
| 7 | + | ; no bar widths to measure and no second bar window to keep in sync. |
|
| 8 | + | ; |
|
| 9 | + | ; The corner radius is font-1's *size*, and the trick is that the glyph is far |
|
| 10 | + | ; taller than the bar: at 30pt it is 48px tall, the bar keeps the middle 32px, |
|
| 11 | + | ; and a shallow 6px corner is all that shows. Bigger size, flatter corner -- |
|
| 12 | + | ; the inset at the bar's top and bottom edge works out to about |
|
| 13 | + | ; |
|
| 14 | + | ; 0.636 * em * (1 - sqrt(1 - (16 / (0.6065 * em))^2)) em = size * 4/3 |
|
| 15 | + | ; |
|
| 16 | + | ; The %{O-8} is what keeps the cap from being fat: the glyph's advance is |
|
| 17 | + | ; 0.6em (24px at 30pt), far more than the corner needs, so the cursor is |
|
| 18 | + | ; pulled back 8px to leave a 16px cap. Which side of the glyph the pullback |
|
| 19 | + | ; goes on matters. cap-left trims after, because its ink runs to the right |
|
| 20 | + | ; into the next module's background, which is painted afterwards and covers |
|
| 21 | + | ; it. cap-right trims before, because its ink runs left over the *previous* |
|
| 22 | + | ; module -- so keep that pullback under that module's format-padding (10px) |
|
| 23 | + | ; or the cap will paint over the last of its text. |
|
| 24 | + | ; |
|
| 25 | + | ; The contract for anything that goes in an island: set a background and some |
|
| 26 | + | ; padding on every format the module can show, e.g. |
|
| 27 | + | ; |
|
| 28 | + | ; format-background = ${colors.background} |
|
| 29 | + | ; format-padding = 1 |
|
| 30 | + | ; |
|
| 31 | + | ; A format without a background leaves a see-through hole in the island. Note |
|
| 32 | + | ; that it is format-<name>-background for modules with named formats |
|
| 33 | + | ; (mounted, volume, connected, ...) -- there is no fallback to a bare |
|
| 34 | + | ; format-background. |
|
| 35 | + | ||
| 36 | + | [module/cap-left] |
|
| 37 | + | type = custom/text |
|
| 38 | + | label = %{T2}%{T-}%{O-8} |
|
| 39 | + | label-foreground = ${colors.background} |
|
| 40 | + | ||
| 41 | + | [module/cap-right] |
|
| 42 | + | type = custom/text |
|
| 43 | + | label = %{O-8}%{T2}%{T-} |
|
| 44 | + | label-foreground = ${colors.background} |
|
| 45 | + | ||
| 46 | + | ; A transparent spacer, for a real gap between two islands on the same side. |
|
| 47 | + | ; One font-1 space, so it is a cap's worth of nothing: 24px. |
|
| 48 | + | [module/gap] |
|
| 49 | + | type = custom/text |
|
| 50 | + | label = %{T2} %{T-} |
|
| 51 | + | label-foreground = ${colors.transparent} |
|
| 52 | + | ||
| 53 | + | ; vim:ft=dosini |
| 1 | + | [module/cpu] |
|
| 2 | + | type = internal/cpu |
|
| 3 | + | interval = 2 |
|
| 4 | + | ||
| 5 | + | format = <label> |
|
| 6 | + | format-background = ${colors.background} |
|
| 7 | + | format-padding = 1 |
|
| 8 | + | format-prefix = " " |
|
| 9 | + | format-prefix-foreground = ${colors.primary} |
|
| 10 | + | ||
| 11 | + | label = %percentage:2%% |
|
| 12 | + | ||
| 13 | + | ; vim:ft=dosini |
| 1 | + | [module/date] |
|
| 2 | + | type = internal/date |
|
| 3 | + | interval = 1 |
|
| 4 | + | ||
| 5 | + | date = %H:%M |
|
| 6 | + | date-alt = %Y-%m-%d %H:%M:%S |
|
| 7 | + | ||
| 8 | + | format = <label> |
|
| 9 | + | format-background = ${colors.background} |
|
| 10 | + | format-padding = 1 |
|
| 11 | + | format-prefix = " " |
|
| 12 | + | format-prefix-foreground = ${colors.primary} |
|
| 13 | + | ||
| 14 | + | label = %date% |
|
| 15 | + | ||
| 16 | + | ; vim:ft=dosini |
| 1 | + | [module/filesystem] |
|
| 2 | + | type = internal/fs |
|
| 3 | + | interval = 25 |
|
| 4 | + | ||
| 5 | + | mount-0 = / |
|
| 6 | + | ||
| 7 | + | format-mounted = <label-mounted> |
|
| 8 | + | format-mounted-background = ${colors.background} |
|
| 9 | + | format-mounted-padding = 1 |
|
| 10 | + | label-mounted = %{F#fbcb97}%{F-} %used% |
|
| 11 | + | ||
| 12 | + | format-unmounted = <label-unmounted> |
|
| 13 | + | format-unmounted-background = ${colors.background} |
|
| 14 | + | format-unmounted-padding = 1 |
|
| 15 | + | label-unmounted = %mountpoint% not mounted |
|
| 16 | + | label-unmounted-foreground = ${colors.disabled} |
|
| 17 | + | ||
| 18 | + | ; vim:ft=dosini |
| 1 | + | [module/memory] |
|
| 2 | + | type = internal/memory |
|
| 3 | + | interval = 2 |
|
| 4 | + | ||
| 5 | + | format = <label> |
|
| 6 | + | format-background = ${colors.background} |
|
| 7 | + | format-padding = 1 |
|
| 8 | + | format-prefix = " " |
|
| 9 | + | format-prefix-foreground = ${colors.primary} |
|
| 10 | + | ||
| 11 | + | label = %used% |
|
| 12 | + | ||
| 13 | + | ; vim:ft=dosini |
| 1 | + | ; Not in layout.ini -- defined and ready, add wlan or eth to an island run. |
|
| 2 | + | [network-base] |
|
| 3 | + | type = internal/network |
|
| 4 | + | interval = 5 |
|
| 5 | + | ||
| 6 | + | format-connected = <label-connected> |
|
| 7 | + | format-connected-background = ${colors.background} |
|
| 8 | + | format-connected-padding = 1 |
|
| 9 | + | ||
| 10 | + | format-disconnected = <label-disconnected> |
|
| 11 | + | format-disconnected-background = ${colors.background} |
|
| 12 | + | format-disconnected-padding = 1 |
|
| 13 | + | label-disconnected = %{F#888888} %ifname%%{F-} |
|
| 14 | + | ||
| 15 | + | [module/wlan] |
|
| 16 | + | inherit = network-base |
|
| 17 | + | interface-type = wireless |
|
| 18 | + | label-connected = %{F#fbcb97}%{F-} %essid% |
|
| 19 | + | ||
| 20 | + | [module/eth] |
|
| 21 | + | inherit = network-base |
|
| 22 | + | interface-type = wired |
|
| 23 | + | label-connected = %{F#fbcb97}%{F-} %local_ip% |
|
| 24 | + | ||
| 25 | + | ; vim:ft=dosini |
| 1 | + | [module/pulseaudio] |
|
| 2 | + | type = internal/pulseaudio |
|
| 3 | + | ||
| 4 | + | format-volume = <label-volume> |
|
| 5 | + | format-volume-background = ${colors.background} |
|
| 6 | + | format-volume-padding = 1 |
|
| 7 | + | format-volume-prefix = " " |
|
| 8 | + | format-volume-prefix-foreground = ${colors.primary} |
|
| 9 | + | label-volume = %percentage%% |
|
| 10 | + | ||
| 11 | + | format-muted = <label-muted> |
|
| 12 | + | format-muted-background = ${colors.background} |
|
| 13 | + | format-muted-padding = 1 |
|
| 14 | + | label-muted = muted |
|
| 15 | + | label-muted-foreground = ${colors.disabled} |
|
| 16 | + | ||
| 17 | + | ; vim:ft=dosini |
| 1 | + | ; Only one tray may exist per session, so this module lives on exactly one |
|
| 2 | + | ; bar. Padding rather than margin: a margin would be a hole in the island. |
|
| 3 | + | [module/systray] |
|
| 4 | + | type = internal/tray |
|
| 5 | + | ||
| 6 | + | format = <tray> |
|
| 7 | + | format-background = ${colors.background} |
|
| 8 | + | format-padding = 1 |
|
| 9 | + | ||
| 10 | + | tray-background = ${colors.background} |
|
| 11 | + | tray-spacing = 8pt |
|
| 12 | + | ||
| 13 | + | ; vim:ft=dosini |
| 1 | + | ; Weather from the US National Weather Service, via ../scripts/weather. |
|
| 2 | + | ; Nothing here needs an API key -- api.weather.gov is open -- but it is US-only, |
|
| 3 | + | ; and the script wants NWS_UA pointed at a real contact address. |
|
| 4 | + | ||
| 5 | + | [module/weather] |
|
| 6 | + | type = custom/script |
|
| 7 | + | ||
| 8 | + | ; scripts/ is symlinked into ~/.config/polybar the same way rofi/scripts is. |
|
| 9 | + | exec = ~/.config/polybar/scripts/weather |
|
| 10 | + | click-left = ~/.config/polybar/scripts/weather detail |
|
| 11 | + | ||
| 12 | + | ; The script serves a cached line until NWS_INTERVAL (15 minutes) is up, so |
|
| 13 | + | ; polling this often costs a file read, not a request. It only sets how soon a |
|
| 14 | + | ; refreshed line reaches the bar. |
|
| 15 | + | interval = 300 |
|
| 16 | + | ||
| 17 | + | ; The palette stays in colors.ini; the script prints plain text unless told |
|
| 18 | + | ; otherwise, so pass the two colours it uses in from here. |
|
| 19 | + | env-NWS_ICON_COLOR = ${colors.primary} |
|
| 20 | + | env-NWS_ALERT_COLOR = ${colors.alert} |
|
| 21 | + | ||
| 22 | + | ; Pin the location to skip the geolocation lookup, and put a real address in |
|
| 23 | + | ; NWS_UA -- NWS asks for one it can contact about a misbehaving client. |
|
| 24 | + | ; env-NWS_LAT = 36.1627 |
|
| 25 | + | ; env-NWS_LON = -86.7816 |
|
| 26 | + | ; env-NWS_UA = (dotfiles polybar, you@example.com) |
|
| 27 | + | ||
| 28 | + | format = <label> |
|
| 29 | + | format-background = ${colors.background} |
|
| 30 | + | format-padding = 1 |
|
| 31 | + | ||
| 32 | + | label = %output% |
|
| 33 | + | ||
| 34 | + | ; vim:ft=dosini |
| 1 | + | ; Not in layout.ini -- defined and ready, add xkeyboard to an island run. |
|
| 2 | + | [module/xkeyboard] |
|
| 3 | + | type = internal/xkeyboard |
|
| 4 | + | blacklist-0 = num lock |
|
| 5 | + | ||
| 6 | + | format = <label-layout><label-indicator> |
|
| 7 | + | format-background = ${colors.background} |
|
| 8 | + | format-padding = 1 |
|
| 9 | + | ||
| 10 | + | label-layout = %layout% |
|
| 11 | + | label-layout-foreground = ${colors.primary} |
|
| 12 | + | ||
| 13 | + | label-indicator-padding = 2 |
|
| 14 | + | label-indicator-margin = 1 |
|
| 15 | + | label-indicator-foreground = ${colors.background} |
|
| 16 | + | label-indicator-background = ${colors.secondary} |
|
| 17 | + | ||
| 18 | + | ; vim:ft=dosini |
| 1 | + | ; Not in layout.ini -- defined and ready, add xwindow to an island run. It |
|
| 2 | + | ; grows and shrinks with the window title, and so does its island. |
|
| 3 | + | [module/xwindow] |
|
| 4 | + | type = internal/xwindow |
|
| 5 | + | ||
| 6 | + | format = <label> |
|
| 7 | + | format-background = ${colors.background} |
|
| 8 | + | format-padding = 1 |
|
| 9 | + | ||
| 10 | + | label = %title:0:60:...% |
|
| 11 | + | ||
| 12 | + | ; vim:ft=dosini |
| 1 | + | [module/xworkspaces] |
|
| 2 | + | type = internal/xworkspaces |
|
| 3 | + | ||
| 4 | + | format = <label-state> |
|
| 5 | + | format-background = ${colors.background} |
|
| 6 | + | ||
| 7 | + | ; Padding by prefix/suffix rather than format-padding, and a background on |
|
| 8 | + | ; every state, both for the same reason: a label that carries its own |
|
| 9 | + | ; background (label-active does) drops polybar back to the *bar's* colour for |
|
| 10 | + | ; everything drawn after it -- and the bar is transparent, so anything left |
|
| 11 | + | ; unpainted is a hole straight through the island. |
|
| 12 | + | format-padding = 0 |
|
| 13 | + | format-prefix = " " |
|
| 14 | + | format-prefix-background = ${colors.background} |
|
| 15 | + | format-suffix = " " |
|
| 16 | + | format-suffix-background = ${colors.background} |
|
| 17 | + | ||
| 18 | + | label-active = %name% |
|
| 19 | + | label-active-background = ${colors.background-alt} |
|
| 20 | + | label-active-underline = ${colors.primary} |
|
| 21 | + | label-active-padding = 1 |
|
| 22 | + | ||
| 23 | + | label-occupied = %name% |
|
| 24 | + | label-occupied-background = ${colors.background} |
|
| 25 | + | label-occupied-padding = 1 |
|
| 26 | + | ||
| 27 | + | label-urgent = %name% |
|
| 28 | + | label-urgent-background = ${colors.alert} |
|
| 29 | + | label-urgent-padding = 1 |
|
| 30 | + | ||
| 31 | + | label-empty = %name% |
|
| 32 | + | label-empty-background = ${colors.background} |
|
| 33 | + | label-empty-foreground = ${colors.disabled} |
|
| 34 | + | label-empty-padding = 1 |
|
| 35 | + | ||
| 36 | + | ; vim:ft=dosini |
| 1 | + | #!/bin/sh |
|
| 2 | + | # Requires: curl, jq, a Nerd Font (the glyphs are nf-md-*), and dunst or any |
|
| 3 | + | # notification daemon for the `detail` action. |
|
| 4 | + | # Weather for the bar, straight from the US National Weather Service. It began |
|
| 5 | + | # as polybar-scripts' openweathermap-fullfeatured, but against api.weather.gov, |
|
| 6 | + | # so there is no API key to keep out of the repo. The trade is that NWS only |
|
| 7 | + | # covers the US and its territories: /points returns 404 for anywhere else and |
|
| 8 | + | # the module goes blank. |
|
| 9 | + | # |
|
| 10 | + | # The bar gets current conditions and nothing else -- an icon and a temperature, |
|
| 11 | + | # the same shape as every other module here. The forecast is a click away. |
|
| 12 | + | # |
|
| 13 | + | # weather one line for the bar (the default) |
|
| 14 | + | # weather detail a notification with the full forecast and any alerts |
|
| 15 | + | # |
|
| 16 | + | # NWS asks callers for a contactable User-Agent and for restraint, and this |
|
| 17 | + | # honours both. The grid lookup is cached for a day because a grid square does |
|
| 18 | + | # not move, and the rendered line for NWS_INTERVAL seconds, so the polybar |
|
| 19 | + | # restarts that screenchange-reload makes routine cost no requests at all. A |
|
| 20 | + | # failed fetch falls back to the last good line rather than blanking the bar. |
|
| 21 | + | set -u |
|
| 22 | + | ||
| 23 | + | # Blank asks the network where we are; set both to pin a location and skip |
|
| 24 | + | # the geolocation request entirely. |
|
| 25 | + | LAT=${NWS_LAT:-} |
|
| 26 | + | LON=${NWS_LON:-} |
|
| 27 | + | # us = degrees F, si = degrees C. Passed straight through to NWS for the |
|
| 28 | + | # forecast; observations always arrive in Celsius and are converted here. |
|
| 29 | + | UNITS=${NWS_UNITS:-us} |
|
| 30 | + | # Seconds a rendered line is reused before refetching. The module may poll |
|
| 31 | + | # faster than this -- it just gets the cache back until this expires. |
|
| 32 | + | INTERVAL=${NWS_INTERVAL:-900} |
|
| 33 | + | # NWS wants a User-Agent it can contact about a misbehaving client; an email |
|
| 34 | + | # address or a URL is what they ask for. Put yours here. |
|
| 35 | + | UA=${NWS_UA:-"polybar-nws (https://github.com/polybar/polybar-scripts)"} |
|
| 36 | + | # Polybar colour markup, left empty so the script prints plain text anywhere |
|
| 37 | + | # else. modules/weather.ini fills both in from colors.ini, which is where the |
|
| 38 | + | # palette belongs -- nothing in here should need editing to match the bar. |
|
| 39 | + | ICON_COLOR=${NWS_ICON_COLOR:-} |
|
| 40 | + | ALERT_COLOR=${NWS_ALERT_COLOR:-} |
|
| 41 | + | # 1 names the active alert on the bar as well as flagging it. Off by default: |
|
| 42 | + | # "Severe Thunderstorm Warning" is wider than everything else put together, |
|
| 43 | + | # and it is one click away in `detail` either way. |
|
| 44 | + | SHOW_ALERT_LABEL=${NWS_SHOW_ALERT_LABEL:-0} |
|
| 45 | + | ||
| 46 | + | SYMBOL=° |
|
| 47 | + | API=https://api.weather.gov |
|
| 48 | + | CACHE=${XDG_CACHE_HOME:-$HOME/.cache}/polybar-nws |
|
| 49 | + | GRID=$CACHE/grid |
|
| 50 | + | # Icon, temperature and alert event, tab separated -- deliberately not the |
|
| 51 | + | # finished bar string. The colours arrive from the module, so a bare run from a |
|
| 52 | + | # shell has none; caching a painted line would let whichever ran last decide |
|
| 53 | + | # how the bar looked for the next quarter hour, and would freeze the palette |
|
| 54 | + | # in until the cache expired. |
|
| 55 | + | LINE=$CACHE/line |
|
| 56 | + | # How long the /points and station lookup is trusted. A day, so a laptop that |
|
| 57 | + | # travelled overnight re-locates on its own. |
|
| 58 | + | GRID_TTL=86400 |
|
| 59 | + | # vol uses 7303; a separate id so a volume popup and a forecast popup do not |
|
| 60 | + | # replace each other. |
|
| 61 | + | NOTIFY_ID=7304 |
|
| 62 | + | ||
| 63 | + | # NWS condition token -> glyph. $1 is day or night, $2 the token lifted out of |
|
| 64 | + | # the icon URL; the full token list is at https://api.weather.gov/icons. |
|
| 65 | + | # |
|
| 66 | + | # These are nf-md (Material Design) glyphs, the family every other module here |
|
| 67 | + | # draws from. nf-weather has a finer-grained set, but it is a visibly lighter |
|
| 68 | + | # weight and read as pasted-in next to the solid icons around it. |
|
| 69 | + | # |
|
| 70 | + | # Every glyph below was checked at 13pt against the bar's own font. Material |
|
| 71 | + | # only splits day from night for clear and partly cloudy skies; nothing else |
|
| 72 | + | # needs it, since rain looks like rain whatever the hour. |
|
| 73 | + | get_icon() { |
|
| 74 | + | case $2 in |
|
| 75 | + | skc) d=''; n='';; |
|
| 76 | + | few|sct) d=''; n='';; |
|
| 77 | + | bkn|ovc) d=''; n='';; |
|
| 78 | + | wind_skc|wind_few|wind_sct|wind_bkn|wind_ovc) |
|
| 79 | + | d=''; n='';; |
|
| 80 | + | rain) d=''; n='';; |
|
| 81 | + | rain_showers|rain_showers_hi) d=''; n='';; |
|
| 82 | + | tsra|tsra_sct|tsra_hi) d=''; n='';; |
|
| 83 | + | snow) d=''; n='';; |
|
| 84 | + | blizzard) d=''; n='';; |
|
| 85 | + | sleet|fzra|rain_snow|rain_sleet|snow_sleet|rain_fzra|snow_fzra) |
|
| 86 | + | d=''; n='';; |
|
| 87 | + | fog) d=''; n='';; |
|
| 88 | + | haze|dust) d=''; n='';; |
|
| 89 | + | smoke) d=''; n='';; |
|
| 90 | + | hot) d=''; n='';; |
|
| 91 | + | cold) d=''; n='';; |
|
| 92 | + | tornado) d=''; n='';; |
|
| 93 | + | hurricane|tropical_storm) d=''; n='';; |
|
| 94 | + | *) d=''; n='';; |
|
| 95 | + | esac |
|
| 96 | + | ||
| 97 | + | if [ "$1" = night ]; then printf '%s' "$n"; else printf '%s' "$d"; fi |
|
| 98 | + | } |
|
| 99 | + | ||
| 100 | + | # The condition lives in the icon URL's path, not in a field of its own: |
|
| 101 | + | # .../icons/land/day/tsra_hi,40/tsra,60?size=medium |
|
| 102 | + | # Prints "day tsra_hi" -- the leading period wins, and both the ",40" chance |
|
| 103 | + | # suffix and any second period are dropped. |
|
| 104 | + | icon_parts() { |
|
| 105 | + | printf '%s' "${1%%\?*}" | awk -F/ ' |
|
| 106 | + | { |
|
| 107 | + | for (i = 1; i < NF; i++) |
|
| 108 | + | if ($i == "day" || $i == "night") { |
|
| 109 | + | sub(/,.*/, "", $(i + 1)) |
|
| 110 | + | print $i, $(i + 1) |
|
| 111 | + | found = 1 |
|
| 112 | + | exit |
|
| 113 | + | } |
|
| 114 | + | } |
|
| 115 | + | END { if (!found) print "day unknown" }' |
|
| 116 | + | } |
|
| 117 | + | ||
| 118 | + | # curl with the headers NWS expects, plus a retry: gridpoint requests return |
|
| 119 | + | # a 500 often enough that one attempt is not a fair test. |
|
| 120 | + | fetch() { |
|
| 121 | + | curl -sfL --compressed --max-time 8 --retry 2 --retry-delay 1 \ |
|
| 122 | + | -H "User-Agent: $UA" -H "Accept: application/geo+json" "$1" |
|
| 123 | + | } |
|
| 124 | + | ||
| 125 | + | # $2 wrapped in polybar's colour markup, or left alone when $1 is empty. |
|
| 126 | + | paint() { |
|
| 127 | + | if [ -n "$1" ]; then printf '%%{F%s}%s%%{F-}' "$1" "$2"; else printf '%s' "$2"; fi |
|
| 128 | + | } |
|
| 129 | + | ||
| 130 | + | # The bar line, built from the three cached fields: glyph, temperature, and the |
|
| 131 | + | # active alert event (empty when there is none). Painting happens here, at the |
|
| 132 | + | # last possible moment, so the module's colours apply to a cached reading too. |
|
| 133 | + | # |
|
| 134 | + | # The trailing space goes *inside* the colour run. polybar clips each %{F...} |
|
| 135 | + | # segment at the advance width of the text in it, and these glyphs draw wider |
|
| 136 | + | # than the single cell they advance -- put the space after the %{F-} and the |
|
| 137 | + | # overflow is cut off, leaving a half-rendered blob. It is the same reason |
|
| 138 | + | # every other module here writes format-prefix = "<glyph> " rather than |
|
| 139 | + | # hanging the space off the label. |
|
| 140 | + | format_line() { |
|
| 141 | + | line="$(paint "$ICON_COLOR" "$1 ")$2$SYMBOL" |
|
| 142 | + | if [ -n "$3" ]; then |
|
| 143 | + | if [ "$SHOW_ALERT_LABEL" = 1 ]; then flag=" $3"; else flag=''; fi |
|
| 144 | + | line="$(paint "$ALERT_COLOR" "$flag ")$line" |
|
| 145 | + | fi |
|
| 146 | + | printf '%s\n' "$line" |
|
| 147 | + | } |
|
| 148 | + | ||
| 149 | + | # One field out of a JSON blob on stdin's place, empty rather than "null". |
|
| 150 | + | j() { printf '%s' "$1" | jq -r "$2 // empty" 2>/dev/null; } |
|
| 151 | + | ||
| 152 | + | # Seconds since $1 was last written; effectively infinite if it is not there. |
|
| 153 | + | age() { |
|
| 154 | + | if [ -f "$1" ]; then |
|
| 155 | + | echo $(( $(date +%s) - $(stat -c %Y "$1") )) |
|
| 156 | + | else |
|
| 157 | + | echo 999999999 |
|
| 158 | + | fi |
|
| 159 | + | } |
|
| 160 | + | ||
| 161 | + | # Observations are always Celsius no matter what ?units= asked for, so the |
|
| 162 | + | # conversion happens here rather than being left to the API. |
|
| 163 | + | c_to_display() { |
|
| 164 | + | [ -n "${1:-}" ] || return 1 |
|
| 165 | + | if [ "$UNITS" = us ]; then |
|
| 166 | + | awk -v c="$1" 'BEGIN { printf "%.0f", c * 9 / 5 + 32 }' |
|
| 167 | + | else |
|
| 168 | + | awk -v c="$1" 'BEGIN { printf "%.0f", c }' |
|
| 169 | + | fi |
|
| 170 | + | } |
|
| 171 | + | ||
| 172 | + | kmh_to_display() { |
|
| 173 | + | [ -n "${1:-}" ] || return 1 |
|
| 174 | + | if [ "$UNITS" = us ]; then |
|
| 175 | + | awk -v k="$1" 'BEGIN { printf "%.0f mph", k * 0.621371 }' |
|
| 176 | + | else |
|
| 177 | + | awk -v k="$1" 'BEGIN { printf "%.0f km/h", k }' |
|
| 178 | + | fi |
|
| 179 | + | } |
|
| 180 | + | ||
| 181 | + | # Whether $obs is recent enough to show as "now". Stations that report hourly |
|
| 182 | + | # routinely skip a cycle, so the cutoff is two hours; past that the forecast is |
|
| 183 | + | # the better answer. busybox date cannot parse an ISO stamp with an offset, and |
|
| 184 | + | # jq's fromdateiso8601 insists on a "Z" -- NWS only ever sends "+00:00", so that |
|
| 185 | + | # is the one offset translated, and anything else counts as fresh rather than |
|
| 186 | + | # throwing away a good observation over a timestamp this cannot read. |
|
| 187 | + | fresh_obs() { |
|
| 188 | + | [ -n "$obs" ] || return 1 |
|
| 189 | + | ts=$(j "$obs" '.properties.timestamp | sub("\\+00:00$"; "Z") | fromdateiso8601') |
|
| 190 | + | [ -n "$ts" ] || return 0 |
|
| 191 | + | [ $(( $(date +%s) - ts )) -lt 7200 ] |
|
| 192 | + | } |
|
| 193 | + | ||
| 194 | + | # Resolve coordinates to the three URLs everything else needs, and cache them. |
|
| 195 | + | # NWS models the country as a grid of 2.5km squares; /points is the only way |
|
| 196 | + | # to learn which square a latitude and longitude fall in, and the answer is |
|
| 197 | + | # stable, so this runs once a day at most. |
|
| 198 | + | resolve_grid() { |
|
| 199 | + | if [ -n "$LAT" ] && [ -n "$LON" ]; then |
|
| 200 | + | loc="$LAT,$LON" |
|
| 201 | + | else |
|
| 202 | + | # Two providers because both are free endpoints that go away without |
|
| 203 | + | # warning; either one is accurate enough to pick a 2.5km square. |
|
| 204 | + | loc=$(curl -sf --max-time 6 https://ipinfo.io/json | jq -r '.loc // empty') |
|
| 205 | + | [ -n "$loc" ] || loc=$(curl -sf --max-time 6 'http://ip-api.com/json/?fields=lat,lon' | |
|
| 206 | + | jq -r 'if .lat then "\(.lat),\(.lon)" else empty end') |
|
| 207 | + | [ -n "$loc" ] || return 1 |
|
| 208 | + | fi |
|
| 209 | + | ||
| 210 | + | # NWS caps coordinate precision at four decimals: /points answers anything |
|
| 211 | + | # longer with a 301 to the truncated form, and /alerts just returns a 500. |
|
| 212 | + | # Round here so neither can surprise a hand-set NWS_LAT later on. |
|
| 213 | + | loc=$(printf '%s' "$loc" | awk -F, '{ printf "%.4f,%.4f", $1, $2 }') |
|
| 214 | + | ||
| 215 | + | points=$(fetch "$API/points/$loc") || return 1 |
|
| 216 | + | hourly=$(j "$points" '.properties.forecastHourly') |
|
| 217 | + | daily=$(j "$points" '.properties.forecast') |
|
| 218 | + | stations=$(j "$points" '.properties.observationStations') |
|
| 219 | + | place=$(j "$points" '.properties.relativeLocation.properties | |
|
| 220 | + | if .city then "\(.city), \(.state)" else empty end') |
|
| 221 | + | [ -n "$hourly" ] && [ -n "$stations" ] || return 1 |
|
| 222 | + | ||
| 223 | + | # The station list is not sorted by distance despite looking like it is -- |
|
| 224 | + | # for Denver it leads with an airport 24km up the highway -- so pick the |
|
| 225 | + | # nearest one properly. Flat-earth distance with a cosine correction on the |
|
| 226 | + | # longitude is plenty over the handful of kilometres in play here. |
|
| 227 | + | station=$(fetch "$stations" | jq -r --argjson lat "${loc%,*}" --argjson lon "${loc#*,}" ' |
|
| 228 | + | [ .features[] |
|
| 229 | + | | { id: .properties.stationIdentifier, |
|
| 230 | + | dx: ((.geometry.coordinates[0] - $lon) * (($lat * 3.14159265 / 180) | cos)), |
|
| 231 | + | dy: (.geometry.coordinates[1] - $lat) } ] |
|
| 232 | + | | min_by(.dx * .dx + .dy * .dy) |
|
| 233 | + | | .id // empty') |
|
| 234 | + | [ -n "$station" ] || return 1 |
|
| 235 | + | ||
| 236 | + | mkdir -p "$CACHE" |
|
| 237 | + | printf '%s\n%s\n%s\n%s\n%s\n' "$hourly" "$daily" "$station" "$loc" "$place" > "$GRID" |
|
| 238 | + | } |
|
| 239 | + | ||
| 240 | + | # Populates grid_hourly, grid_daily, grid_station, grid_point and grid_place. |
|
| 241 | + | load_grid() { |
|
| 242 | + | if [ "$(age "$GRID")" -gt "$GRID_TTL" ]; then |
|
| 243 | + | resolve_grid || [ -f "$GRID" ] || return 1 |
|
| 244 | + | fi |
|
| 245 | + | grid_hourly=$(sed -n 1p "$GRID") |
|
| 246 | + | grid_daily=$(sed -n 2p "$GRID") |
|
| 247 | + | grid_station=$(sed -n 3p "$GRID") |
|
| 248 | + | grid_point=$(sed -n 4p "$GRID") |
|
| 249 | + | grid_place=$(sed -n 5p "$GRID") |
|
| 250 | + | [ -n "$grid_hourly" ] |
|
| 251 | + | } |
|
| 252 | + | ||
| 253 | + | # NWS hands back active alerts in no particular order, so severity has to be |
|
| 254 | + | # sorted on rather than assumed. Shared by the bar, which wants only the worst |
|
| 255 | + | # one, and by detail, which lists them all. The severity has to be bound before |
|
| 256 | + | # the ranking array is piped in, or the index lookup reads .severity off the |
|
| 257 | + | # array instead of off the alert. |
|
| 258 | + | ALERT_ORDER='[.features[].properties] |
|
| 259 | + | | sort_by((.severity // "Unknown") as $s |
|
| 260 | + | | (["Extreme", "Severe", "Moderate", "Minor"] | index($s)) // 9)' |
|
| 261 | + | ||
| 262 | + | # The most serious active alert for our point, or nothing. |
|
| 263 | + | active_alert() { |
|
| 264 | + | fetch "$API/alerts/active?point=$grid_point" | jq -r "$ALERT_ORDER | .[0].event // empty" |
|
| 265 | + | } |
|
| 266 | + | ||
| 267 | + | render() { |
|
| 268 | + | load_grid || return 1 |
|
| 269 | + | ||
| 270 | + | temp= |
|
| 271 | + | icon_url= |
|
| 272 | + | obs=$(fetch "$API/stations/$grid_station/observations/latest") |
|
| 273 | + | ||
| 274 | + | # The observation is a real measurement and is what "now" ought to mean. |
|
| 275 | + | if fresh_obs; then |
|
| 276 | + | temp=$(c_to_display "$(j "$obs" '.properties.temperature.value')") || temp= |
|
| 277 | + | icon_url=$(j "$obs" '.properties.icon') |
|
| 278 | + | fi |
|
| 279 | + | ||
| 280 | + | # Small stations drop out for hours at a time, and a four-hour-old reading |
|
| 281 | + | # is worse than none. Period 1 of the hourly forecast covers the current |
|
| 282 | + | # hour and stands in when that happens -- which is why this request sits |
|
| 283 | + | # down here rather than up beside the observation: in the ordinary case the |
|
| 284 | + | # station answers and it never runs at all. |
|
| 285 | + | if [ -z "$temp" ] || [ -z "$icon_url" ]; then |
|
| 286 | + | hourly=$(fetch "$grid_hourly?units=$UNITS") |
|
| 287 | + | [ -n "$temp" ] || temp=$(j "$hourly" '.properties.periods[0].temperature') |
|
| 288 | + | [ -n "$icon_url" ] || icon_url=$(j "$hourly" '.properties.periods[0].icon') |
|
| 289 | + | fi |
|
| 290 | + | ||
| 291 | + | [ -n "$temp" ] || return 1 |
|
| 292 | + | # NWS sends whole numbers today; this is here so a stray decimal cannot |
|
| 293 | + | # turn up inside the bar label. |
|
| 294 | + | temp=${temp%%.*} |
|
| 295 | + | ||
| 296 | + | # Unquoted on purpose: icon_parts prints the two arguments get_icon takes. |
|
| 297 | + | icon=$(get_icon $(icon_parts "$icon_url")) |
|
| 298 | + | alert=$(active_alert) |
|
| 299 | + | ||
| 300 | + | mkdir -p "$CACHE" |
|
| 301 | + | printf '%s\t%s\t%s\n' "$icon" "$temp" "$alert" > "$LINE" |
|
| 302 | + | format_line "$icon" "$temp" "$alert" |
|
| 303 | + | } |
|
| 304 | + | ||
| 305 | + | # The cached reading, repainted with whatever colours this run was given. |
|
| 306 | + | # Fails on a missing file, and on the painted single-field line older versions |
|
| 307 | + | # of this script wrote, so either one just falls through to a fresh fetch. |
|
| 308 | + | show_cached() { |
|
| 309 | + | [ -f "$LINE" ] || return 1 |
|
| 310 | + | IFS=$(printf '\t') read -r icon temp alert < "$LINE" || return 1 |
|
| 311 | + | [ -n "$temp" ] || return 1 |
|
| 312 | + | format_line "$icon" "$temp" "$alert" |
|
| 313 | + | } |
|
| 314 | + | ||
| 315 | + | # The bar line, from cache when it is still fresh. A failed fetch prints the |
|
| 316 | + | # last good line however old it is: a stale temperature is more use on a bar |
|
| 317 | + | # than a module that vanishes whenever the wifi hiccups. |
|
| 318 | + | bar() { |
|
| 319 | + | if [ "$(age "$LINE")" -lt "$INTERVAL" ] && show_cached; then |
|
| 320 | + | return 0 |
|
| 321 | + | fi |
|
| 322 | + | render && return 0 |
|
| 323 | + | ||
| 324 | + | # Nothing new and nothing cached is either the first run without a network |
|
| 325 | + | # or a location NWS does not cover. Say so on stderr, where polybar's log |
|
| 326 | + | # will keep it, and exit clean so polybar does not treat it as a crash. |
|
| 327 | + | show_cached || |
|
| 328 | + | echo "${0##*/}: no forecast (offline, or outside NWS coverage)" >&2 |
|
| 329 | + | return 0 |
|
| 330 | + | } |
|
| 331 | + | ||
| 332 | + | # Everything that does not fit on the bar: the alerts in full, what the nearest |
|
| 333 | + | # station is actually reading, and the NWS forecaster's own prose for the |
|
| 334 | + | # current period. |
|
| 335 | + | detail() { |
|
| 336 | + | load_grid || { notify "Weather" "No NWS grid for this location"; return 1; } |
|
| 337 | + | ||
| 338 | + | obs=$(fetch "$API/stations/$grid_station/observations/latest") |
|
| 339 | + | daily=$(fetch "$grid_daily?units=$UNITS") |
|
| 340 | + | ||
| 341 | + | body="" |
|
| 342 | + | ||
| 343 | + | alerts=$(fetch "$API/alerts/active?point=$grid_point" | |
|
| 344 | + | jq -r "$ALERT_ORDER"' | .[] | "\(.event) - \(.headline // "")"') |
|
| 345 | + | [ -n "$alerts" ] && body="$alerts |
|
| 346 | + | ||
| 347 | + | " |
|
| 348 | + | ||
| 349 | + | if fresh_obs; then |
|
| 350 | + | temp=$(c_to_display "$(j "$obs" '.properties.temperature.value')") || temp="" |
|
| 351 | + | hum=$(j "$obs" '.properties.relativeHumidity.value') |
|
| 352 | + | wind=$(kmh_to_display "$(j "$obs" '.properties.windSpeed.value')") || wind="" |
|
| 353 | + | # Whichever of the two "feels like" numbers is in season; NWS leaves |
|
| 354 | + | # the other null rather than sending both. |
|
| 355 | + | feels=$(c_to_display "$(j "$obs" '.properties.heatIndex.value')") || |
|
| 356 | + | feels=$(c_to_display "$(j "$obs" '.properties.windChill.value')") || feels="" |
|
| 357 | + | ||
| 358 | + | line="$(j "$obs" '.properties.textDescription') $temp$SYMBOL" |
|
| 359 | + | [ -n "$feels" ] && line="$line (feels $feels$SYMBOL)" |
|
| 360 | + | [ -n "$hum" ] && line="$line, $(awk -v h="$hum" 'BEGIN { printf "%.0f", h }')% humidity" |
|
| 361 | + | [ -n "$wind" ] && line="$line, wind $wind" |
|
| 362 | + | else |
|
| 363 | + | line="No recent reading from $grid_station" |
|
| 364 | + | fi |
|
| 365 | + | body="$body$line |
|
| 366 | + | ||
| 367 | + | $(j "$daily" '.properties.periods[0] | "\(.name): \(.detailedForecast)"')" |
|
| 368 | + | ||
| 369 | + | notify "${grid_place:-Weather} ($grid_station)" "$body" |
|
| 370 | + | } |
|
| 371 | + | ||
| 372 | + | notify() { |
|
| 373 | + | dunstify -r "$NOTIFY_ID" -t 15000 "$1" "$2" 2>/dev/null || |
|
| 374 | + | notify-send "$1" "$2" |
|
| 375 | + | } |
|
| 376 | + | ||
| 377 | + | case "${1:-bar}" in |
|
| 378 | + | bar) bar ;; |
|
| 379 | + | detail) detail ;; |
|
| 380 | + | *) echo "usage: ${0##*/} [bar|detail]" >&2; exit 2 ;; |
|
| 381 | + | esac |
| 19 | 19 | CHANNEL=xfce4-session |
|
| 20 | 20 | BACKUP="${XDG_DATA_HOME:-$HOME/.local/share}/polybar-panel.backup" |
|
| 21 | 21 | ||
| 22 | + | # xfwm4's compositor shadows a dock as a rectangle the size of its window, |
|
| 23 | + | # ignoring what the window actually paints. polybar's bar spans the screen and |
|
| 24 | + | # is transparent everywhere except the islands, so that shadow shows up as a |
|
| 25 | + | # band hanging across the empty middle. Turning it off is the only knob for it; |
|
| 26 | + | # true is xfwm4's default, which --remove puts back. |
|
| 27 | + | dock_shadow() { |
|
| 28 | + | command -v xfconf-query >/dev/null 2>&1 || return 0 |
|
| 29 | + | xfconf-query -c xfwm4 -p /general/show_dock_shadow -s "$1" 2>/dev/null || |
|
| 30 | + | xfconf-query -c xfwm4 -p /general/show_dock_shadow -n -t bool -s "$1" |
|
| 31 | + | } |
|
| 32 | + | ||
| 22 | 33 | # Where the polybar launcher lives. The repo layout is the default; point |
|
| 23 | 34 | # POLYBAR_LAUNCH somewhere else to use an installed copy instead. |
|
| 24 | 35 | LAUNCH=${POLYBAR_LAUNCH:-$(CDPATH= cd -- "$(dirname -- "$0")/../polybar" 2>/dev/null && pwd)/launch.sh} |
|
| 63 | 74 | echo "restored $prop -> $cmd" |
|
| 64 | 75 | done <"$BACKUP" |
|
| 65 | 76 | rm -f "$BACKUP" |
|
| 77 | + | dock_shadow true |
|
| 66 | 78 | polybar-msg cmd quit >/dev/null 2>&1 || pkill -u "$(id -u)" -x polybar |
|
| 67 | 79 | [ -n "${DISPLAY:-}" ] && command -v xfce4-panel >/dev/null 2>&1 && |
|
| 68 | 80 | (xfce4-panel >/dev/null 2>&1 &) |
|
| 80 | 92 | done |
|
| 81 | 93 | clients | grep -q "$LAUNCH" && found=1 |
|
| 82 | 94 | [ "$found" = 1 ] || { echo "polybar-panel: no xfce4-panel session client found" >&2; exit 1; } |
|
| 95 | + | dock_shadow false |
|
| 83 | 96 | if [ -n "${DISPLAY:-}" ]; then |
|
| 84 | 97 | pgrep -u "$(id -u)" -x xfce4-panel >/dev/null 2>&1 && |
|
| 85 | 98 | xfce4-panel --quit >/dev/null 2>&1 |
|