Compare commits

..

479 commits

Author SHA1 Message Date
Daniel Eklöf
43e1944607
Merge branch 'mpris-fixes' 2025-03-20 09:00:03 +01:00
haruInDisguise
ca0f565237 module_mpris: Refactoring + Fixed mutex usage
- Addressed inconsistens variable naming and removed redundant code.
- Mutex locks are now used correctly.
- The context struct now references the modules config, making config
  access less awkward
2025-03-18 13:56:08 +01:00
haruInDisguise
dfa0970b75 module_mpris: Fixed multi threading issues regarding 'identity_list'
This addresses changes requested by @dnkl
2025-03-10 13:36:25 +01:00
haruInDisguise
dcbb0f88ae module_mpris: Cleanup
Fixed inconsistent variable naming/debug logging
2025-03-10 11:34:29 +01:00
haruInDisguise
6a97b364a0 module_mpris: Fixed inconsistent string validation checks
This addresses changes requested by @mathstuf
2025-03-10 11:13:17 +01:00
Ben Boeckel
87c74d54b7
meson: guard the full-conf-good test by the plugins it expects 2025-03-10 07:57:47 +01:00
haruInDisguise
0bcde5c453 module_mpris: Fixed memory leak
The identity list now uses tlllist and is deallocated properly
2025-03-10 01:36:26 +01:00
Ben Boeckel
56467d0ba3 meson: require 0.60.0
This introduces dependencies with multiple names which is used for the
`sdbus_library` dependency of the `mpris` module.
2025-03-10 01:24:55 +01:00
Ben Boeckel
7e76d53c0a modules/mpris: fix dependency search
Move dependency discovery to be with other module dependency searches.
Also only define the `sdbus` dependency if the module is enabled.

The `mpris` module was always being compiled because the `sdbus`
dependency always existed. Instead, check for the external
dependency's status and create the `sdbus` bridge dependency only when
necessary.
2025-03-10 01:24:54 +01:00
haruInDisguise
dcf936fd9b module_mpris: Fixed 'use after free' 2025-03-10 00:32:14 +01:00
haruInDisguise
e423776000 module_mpris: Added 'query-timeout' option
This enables us to configure the communication timeout
with the dbus daemon.
2025-03-09 23:02:59 +01:00
Daniel Eklöf
e68ed8d843
module: mpris: mark debug-only variables with attribute unused
Fixes release builds
2025-03-05 08:41:04 +01:00
haruInDisguise
c27de56bea
Added 'MPRIS' module
This commit adds the ability to display status information for MPRIS
compatible music players.

Closes #53
2025-03-05 08:37:02 +01:00
Nicholas Sudsgaard
b5450c3918
modules/i3: Add unused attribute to focused
This variable is only used in assert() which expands to nothing in
release builds. Adding the unused attribute prevents the compiler from
throwing an error (-Wunused-but-set-variable) when building.
2025-03-05 08:25:17 +01:00
Nicholas Sudsgaard
5a515eae99
bar/wayland: Add unused attribute to count
This variable is only used in LOG_DBG which expands to nothing by
default. Adding the unused attribute prevents the compiler from throwing
an error (-Wunused-but-set-variable) when building.
2025-03-05 08:25:17 +01:00
Daniel Eklöf
b486088f77
examples: codespell: re-using -> reusing 2025-03-05 08:21:54 +01:00
Daniel Eklöf
a242d3d569
ci: sr.ht: skip codespell (this is done in woodpecker) 2025-03-05 08:21:03 +01:00
Daniel Eklöf
2eb1cda733
changelog: pipewire: spacing attributes 2025-03-05 08:19:29 +01:00
Ralph Torres
21f374d2eb module/pipewire: add spacing config 2025-02-24 04:59:37 +00:00
Daniel Eklöf
fc24ea225d
changelog: fix ref (again) for #405 - the issue number is #404 2025-01-01 13:57:32 +01:00
Daniel Eklöf
e1f7c0292f
changelog: fix ref for #405 2025-01-01 13:52:52 +01:00
vova
d746d12f6a
add niri-workspaces and niri-language modules 2025-01-01 13:51:25 +01:00
Ben Boeckel
61d082c802
typos: fix some typos 2025-01-01 13:49:32 +01:00
Ben Boeckel
57711f0dbe mpd: support the single flag
This flag indicates that `mpd` will automatically stop after the
current song is played.
2024-12-28 21:04:54 +01:00
Alexey Yerin
b15714b38a pipewire: Improve handling of node switching
When switching to a node that has a missing property, yambar didn't
reset its internal state to the default value, causing outdated
information to be displayed.
2024-11-27 00:03:12 +03:00
Daniel Eklöf
3e0083c9f2
module/removables: no need to open+fdopen, just do fopen() 2024-10-23 09:40:06 +02:00
Daniel Eklöf
a367895dc6
Open sockets, files etc with FD_CLOEXEC 2024-10-23 09:36:59 +02:00
Mathias Stærk
650d1f13f9
docs: fix typo in example 2024-10-08 15:42:03 +02:00
Daniel Eklöf
e1b6a78f22
doc: particles: remove trailing spaces 2024-10-02 08:11:02 +02:00
Daniel Eklöf
20d48a753b
particle/map: code style 2024-10-02 08:10:53 +02:00
Daniel Eklöf
37ecc251a4
changelog: line-wrap 2024-10-02 08:10:30 +02:00
bagnaram
4826a52306
string like operation 2024-09-16 10:39:12 -05:00
Daniel Eklöf
0f47cbb889
changelog: i3/sway: output tag 2024-09-07 08:35:58 +02:00
Daniel Eklöf
c3f7fe013d
doc: i3/sway: add 'output' to tag list 2024-09-07 08:35:39 +02:00
Daniel Eklöf
b81e41c3c4
module/i3: add 'output' tag
This allows bars to render workspaces differently, depending on which
output the workspace is on:

    - map:
      default: ...
      conditions:
        output == DP-1:
          ...
2024-09-05 11:56:10 +02:00
Daniel Eklöf
060586dbbe
tag: remove the :b formatter
Superseded by /N. Removing since a) it's no longer needed, and b) its
name is not consistent with the other kb/mb/gb formatters.
2024-09-05 08:23:47 +02:00
Daniel Eklöf
c80bae7604
changelog: /N tag formatter 2024-09-05 08:20:59 +02:00
Zhong Jianxin
311c481bfe
tag: add '/N' formatter 2024-09-05 08:19:28 +02:00
Zhong Jianxin
9498d7e445
tag: combine FMT_*BYTE into one FMT_DIVIDE 2024-09-05 08:19:28 +02:00
Daniel Eklöf
2d651d1c0e
changelog: bar position in multi-monitor setups, with location=bottom 2024-09-05 08:16:25 +02:00
fraktal
1b2dee55ef fix bar Y position in case of multi-monitor setups with mixed resolutions 2024-09-04 15:33:25 +02:00
Daniel Eklöf
700bf5b28c
tag: add 'b' formatter
Divides the tag's decimal value by 8.

Closes #392
2024-08-20 14:34:45 +02:00
Daniel Eklöf
f8ba887dcd
readme: repology: use four columns 2024-08-20 09:11:17 +02:00
Daniel Eklöf
887e770202
doc: network: update example
Only display Ethernet and WLAN devices (not loopback, bridges etc).
2024-08-20 07:40:09 +02:00
Daniel Eklöf
54902f46ab
module: network: hardcode type to "wlan" when we see NL80211_CMD_NEW_INTERFACE
Wlan interfaces apparently report themselves as ARPHRD_ETHER in their
ifinfomsg struct (despite there being a ARPHRD_IEEE80211 type...).

"Fix" by hardcoding the type to "wlan" when we receive a
NL80211_CMD_NEW_INTERFACE message.
2024-08-20 07:40:08 +02:00
Daniel Eklöf
699c563051
module: network: add 'kind' tag
The tag maps to the IFLA_INFO_KIND (part of the IFLA_LINKINFO)
netlink attribute.

This attribute is only available on virtual interfaces. Examples of
valid values are:

* bond
* bridge
* gre
* tun
* veth
2024-08-20 07:40:08 +02:00
Daniel Eklöf
a5ae61b5df
module: network: add 'type` tag
This tag maps to the ifinfomsg->ifi_type member, which is set to one
of the ARPHRD_xyz values, defined in linux/if_arp.h.

There's a *ton* of them, and we can't possibly add a string mapping
for _all_ of them, so for now, set to one of:

* loopback
* ether
* wlan
* ARPHRD_NNN, where N is a number
2024-08-20 07:39:59 +02:00
Daniel Eklöf
568eb1140f
modules/mpd: fix reconnect when we're not using inotify
When we're not able to use inotify, we rely on polling. However, we
never detected poll() timeouts, which meant we never re-attempted to
reconnect to MPD.

Maybe #394
2024-08-20 07:34:46 +02:00
Daniel Eklöf
3e0a65f185
meson: fix misdetection of memfd_create() 2024-08-07 17:27:26 +02:00
Daniel Eklöf
1a323c6d21
log: respect the NO_COLOR environment variable
http://no-color.org/
2024-07-18 08:31:46 +02:00
Daniel Eklöf
739dc30323
changelog: env var substitution: fix issue reference
Reference the issue, not the PR.
2024-06-09 10:12:32 +02:00
Daniel Eklöf
8422e7e0b1
doc: yambar(5): remove trailing whitespace 2024-06-09 10:12:23 +02:00
Daniel Eklöf
9cc5e0f7a7
module/network: plug memory leak
Free the 'ifaces' list, not just its contents.
2024-06-09 10:08:38 +02:00
Daniel Eklöf
3431d5fc75
yml: replace_env_variables(): const:ify function variables 2024-06-09 10:05:21 +02:00
Tomas Slusny
20659d3350 Add support for environment variable references
The format is
key: ${env_variable}

Closes #96

Signed-off-by: Tomas Slusny <slusnucky@gmail.com>
2024-06-09 01:11:34 +02:00
Daniel Eklöf
0bea49b75e
module/river: return empty particle list when river is not running
Closes #384
2024-05-20 09:33:45 +02:00
Daniel Eklöf
70efd7d15c
doc: yambar-particles: document the hard-coded spacing of short-form lists
Closes #385
2024-05-20 09:21:29 +02:00
Daniel Eklöf
b8a93a2673
changelog: i3/sway crash fix for output being turned on/off 2024-05-20 07:45:49 +02:00
QuincePie
a467f56677
i3: Handle FALLBACK output for workspaces.
sway moves the workspace to fallback_output when there is no output. For example: when all the screens are off. This commit adds an ignore for the fallback output.
2024-05-20 07:44:50 +02:00
Delgan
b3313cefc6 Fix remaining typos in the codebase (and update CI checks) 2024-05-02 16:28:51 +00:00
betazoid
00234696fe
Add examples/river-minimal.yml 2024-04-30 08:57:36 +02:00
Birger Schacht
3a7455913f
fix: typo
Probaly -> Probably
2024-04-30 08:56:18 +02:00
Sertonix
547cef5afb network: fix missing break in switch statement
This can cause the first character of the string to be read as an iface state.

Fixes https://codeberg.org/dnkl/yambar/issues/377
2024-04-22 16:23:16 +02:00
Daniel Eklöf
6f3952819f
changelog: add new 'unreleased' section 2024-04-17 10:42:56 +02:00
Daniel Eklöf
f148c68736
Merge branch 'releases/1.11' 2024-04-17 10:42:35 +02:00
Daniel Eklöf
a2d30b96fb
meson/pkgbuild: bump version to 1.11.0 2024-04-17 10:30:54 +02:00
Daniel Eklöf
cae07a36ff
changelog: prepare for 1.11.0 2024-04-17 10:30:29 +02:00
Daniel Eklöf
3136310ade
ci: install openssl explicitly, to fix missing SSL module in Python 2024-04-17 09:53:00 +02:00
Daniel Eklöf
be01eeb1de
ci: set 'event' filters on all 'when'-statements 2024-04-17 09:52:46 +02:00
Daniel Eklöf
13f46a314a
examples: conf: laptop: repair network modules
Closes #374
2024-04-11 15:47:50 +02:00
Daniel Eklöf
3c572c70c9
wayland: silence compiler warning
... by ensuring 'layer' is always initialized, to _something_.
2024-04-09 16:34:53 +02:00
Delgan
b85ba99980 Apply "clang-format" preferences globally 2024-04-07 10:05:10 +02:00
Daniel Eklöf
d841aeeecd
config: layer: add 'overlay' and 'background'
The layer option (Wayland only) now accepts 'overlay' and
'background'.

Closes #372
2024-04-06 15:39:19 +02:00
Daniel Eklöf
28a18ad91e
log: fix syslog not respecting the configured log level 2024-04-05 16:11:37 +02:00
Delgan
da19c09122 Add missing "dynlist" dependency to network module 2024-04-01 08:53:50 +00:00
Delgan
58c397d154 Fix CI failing due to outdated test config file 2024-04-01 08:46:50 +00:00
Delgan
53dec73ed2
Fix miscalculation of list width in presence of empty particles 2024-04-01 09:10:58 +02:00
Sertonix
c44c66c83f
network: use dynlist instead of fixed name
Closes #271
Closes #265
Closes #71
2024-04-01 08:34:18 +02:00
Daniel Eklöf
4e07b63cef
plugin: workaround gcc bug that triggers a compilation error
GCC thinks str2type() returns NULL when it doesn't.

Closes #350
2024-03-18 16:47:37 +01:00
Daniel Eklöf
c19a31d925
ci: install and run codespell in/from a venv 2024-03-18 16:45:41 +01:00
Daniel Eklöf
4066326614
ci: sync with woodpecker 2.x changes 2024-03-18 16:40:25 +01:00
Daniel Eklöf
424f22ab84
ci: rename .woodpecker.yml -> .woodpecker.yaml 2024-03-18 16:39:43 +01:00
Haden Collins
7a5cf26fb5
Add changelog entry 2024-03-16 10:00:24 -05:00
Haden Collins
89ae7bd743
Handle reload workspace events from sway correctly
Closes #361
2024-03-14 16:14:53 -05:00
Daniel Eklöf
f2d25c8341
script: fix buffer resize bug
If the amount of data coming in is more than we can hold in our
buffer, we resized the buffer by doubling its size. However, there
were two(!) issues here:

* If this was the first resize, the buffer size was set to 1024. This
  may not be enough (i.e. there may be more than 1024 bytes to process).
* In all other cases, the buffer size was doubled. However, there is
  still no guarantee the buffer is large enough.

Fix by looping until the buffer *is* large enough.
2024-02-05 12:52:40 +01:00
Delgan
aeeef4f236
Fix "mem" values updated while it should not
Closes #352
2024-02-05 12:49:26 +01:00
Delgan
195ac5d1cd
Fix incorrect empty/title state of i3 workspaces 2024-02-05 12:45:02 +01:00
Yiyu Zhou
9d90848291 style: remove trailing spaces 2024-01-13 00:24:58 -05:00
Daniel Eklöf
bbbf2b601e
main: S_ISFIFO() matches both pipes and FIFOs 2024-01-04 16:35:05 +01:00
Daniel Eklöf
cb2561a72c
changelog: move 'reading config from pipe' from changed to added 2024-01-04 16:33:59 +01:00
steovd
8b1fa13686 main: allow reading alternative config from pipe 2024-01-04 15:13:49 +01:00
Daniel Eklöf
bdc4fbe8e7
doc: pipewire: describe the 'content' config option 2024-01-04 13:57:17 +01:00
Delgan
e1f78a16ab
Add new "quality" tag to "network" module 2024-01-04 13:44:54 +01:00
rdbo
26bf62a899 fixed meson setup directory on readme
Signed-off-by: rdbo <rdbo@noreply.codeberg.org>
2024-01-04 08:21:05 +00:00
Daniel Eklöf
cdee55afed
bar: wayland: update bar size + refresh in output_done()
This ensures the bar's size (width) is updated when the screen
resolution (and/or scale) is changed.

Note that we already handled scale changes. This logic has been moved
from output_scale() to output_done().

Closes #330
2024-01-03 15:39:04 +01:00
Daniel Eklöf
9365580539
module: battery: style 2024-01-03 15:30:03 +01:00
Daniel Eklöf
3a3a711b69
changelog: battery: smoothing + scaling 2024-01-03 15:29:06 +01:00
Daniel Eklöf
4d46f25854
doc: battery: document defaults for battery-scale and smoothing-secs 2024-01-03 15:28:32 +01:00
Jordan Isaacs
a943def94e
battery scale and discharge smoothing 2024-01-03 15:22:12 +01:00
Sertonix
e54e8635e0
main: change default log level to warning 2024-01-03 15:20:31 +01:00
kotyk
176ed4a6f3
particles/string: rewrite truncation code, show three dots only for max>3 2024-01-03 15:17:51 +01:00
Daniel Eklöf
d5823bcc4c
changelog: fixed: crash when hidden by an opaque window 2024-01-03 15:15:20 +01:00
Väinö Mäkelä
1283160e17
bar/wayland: Reset last_mapped_monitor on enter
If the surface enters an output, there's no need for
last_mapped_monitor, and it must be reset to fulfill the asserts in
other parts of the code.

This makes yambar no longer crash when it is hidden by an opaque window
multiple times on a compositor using wlroots' scene tree.
2024-01-03 15:14:05 +01:00
Leonardo Hernández Hernández
60671da2ca
lowercase DWL (dwl is the preferred form) 2024-01-03 15:11:09 +01:00
oob
14550440dd
Minor documentation update 2024-01-03 14:34:24 +01:00
Daniel Eklöf
89e74139f5
bar: wayland: shm: try with MFD_NOEXEC_SEAL first, then without
MFD_NOEXEC_SEAL is only supported on kernels 6.3 and later.

If we were compiled on linux >= 6.3, but run on linux < 6.3, we'd exit
with an error, due to memfd_create() failing with EINVAL.

This patch fixes the problem by first trying to call
memfd_create() *with* MFD_NOEXEC_SEAL, and if that fails with EINVAL,
we try again without it.
2023-10-13 16:34:02 +02:00
Daniel Eklöf
cbd3bebb04
bar/wayland: create memfd with MFD_NOEXEC_SEAL 2023-10-08 11:12:15 +02:00
Daniel Eklöf
7fbc1f2c44
bar/wayland: seal memfd 2023-10-08 11:12:08 +02:00
Daniel Eklöf
9a111a52f5
ci: 'pipeline' -> 'steps' 2023-08-18 16:49:18 +02:00
Daniel Eklöf
78f7b60e13
particle/map: non-greedy matching of quotes
Flex regexps are greedy.

This means '"foo" || "bar"' will return 'foo" || "bar', which is
obviously wrong.

Use "start conditions" to implement non-greedy matching.

Closes #302
2023-07-24 17:13:19 +02:00
Daniel Eklöf
9f5f35a8ac
Merge branch 'tag-fmt-is-maybe-uninitialized-warning'
Closes #311
2023-07-14 13:05:17 +02:00
Daniel Eklöf
42cef9373e
changelog: "‘fmt’ may be used uninitialized" compiler warning 2023-07-14 12:54:23 +02:00
Daniel Eklöf
e1fc3a0e29
tag: explicitly initialize ‘fmt’
Fixes the following compiler warning/error:

  In file included from /usr/include/stdio.h:906,
                   from ../tag.c:6:
  In function ‘snprintf’,
      inlined from ‘tags_expand_template’ at ../tag.c:708:13:
  /usr/include/bits/stdio2.h:54:10: error: ‘fmt’ may be used uninitialized [-Werror=maybe-uninitialized]
     54 |   return __builtin___snprintf_chk (__s, __n, __USE_FORTIFY_LEVEL - 1,
        |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     55 |                                    __glibc_objsize (__s), __fmt,
        |                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     56 |                                    __va_arg_pack ());
        |                                    ~~~~~~~~~~~~~~~~~
  ../tag.c: In function ‘tags_expand_template’:
  ../tag.c:677:25: note: ‘fmt’ was declared here
    677 |             const char *fmt;
        |                         ^~~
  cc1: all warnings being treated as errors

Closes #311
2023-07-14 12:52:19 +02:00
Daniel Eklöf
5db61745a4
changelog: add new ‘unreleased’ section 2023-07-14 09:04:32 +02:00
Daniel Eklöf
4ba193ad0f
Merge branch 'releases/1.10' 2023-07-14 09:04:18 +02:00
Daniel Eklöf
c4e094de3e
meson+pkgbuild: bump version to 1.10.0 2023-07-14 09:03:02 +02:00
Daniel Eklöf
1b23e72770
changelog: prepare for 1.10.0 2023-07-14 09:02:32 +02:00
Daniel Eklöf
5ac7d51e8a
changelog: minor formatting changes 2023-07-14 08:34:16 +02:00
Daniel Eklöf
f923261fec
config: don’t ignore asprintf() return value 2023-07-11 12:40:14 +02:00
Daniel Eklöf
8e4d7f04e4
module/script: path: expand ‘~’ to the user’s $HOME directory
Closes #307
2023-07-11 12:38:44 +02:00
Daniel Eklöf
d6e7710a7e
particle: on-click: tilde expansion
We now do tilde expansion of the *first* argument in on-click
handlers.

That is:

  ~/bin/foobar.sh ~/arg1

is expanded to

  $HOME/bin/foobar.sh ~/arg1

(meaning, the handler will most likely *not* do what you’d expect)

Related to #307
2023-07-11 10:31:40 +02:00
Daniel Eklöf
f948b9f8f9
module/battery: regression: allow poll-interval == 0
The regression was introduced after 1.9.0, hence no changelog entry.
2023-07-09 11:18:33 +02:00
Daniel Eklöf
6220a07aaf
module/battery: debug log when updating due to udev notification 2023-07-09 11:18:33 +02:00
Daniel Eklöf
a342e036ad
module/battery: don’t reset poll timeout on irrelevant udev notifications
We may receive udev notifications for the power-supply subsystem, that
aren’t for us.

Before this patch, this would result in a new poll() call, with
timeout being set to m->poll_interval again. That is, the poll timeout
was being reset.

In theory, this means we could end up in a situation where the battery
status is never updated.

This patch fixes it by tracking how much time is left of the poll
interval. The interval is reset either when the timeout has occurred,
or when we receive an udev notification that _is_ for us.

Hopefully closes #305
2023-07-09 11:18:30 +02:00
David Bimmler
b694fc1583 battery: also show time_to_full
The kernel also provides time_to_full, also in seconds, so let's use
that too. Both time_to_empty and time_to_full have 0 as their default
value, hence only consider them for the estimate if they are
positive (instead of non-negative).

Signed-off-by: David Bimmler <git@d4ve.email>
2023-07-07 17:32:41 +02:00
David Bimmler
08f5f444eb battery: correct time_to_empty calculation
The kernel docs state that time_to_empty contains the estimation of
remaining time in seconds. Hence, calculate estimate minutes and hours
from that in a more correct way.

Signed-off-by: David Bimmler <git@d4ve.email>
2023-07-07 17:32:41 +02:00
Daniel Eklöf
d236b9c1b9
particle/map: make eval_map_condition() more readable 2023-07-04 11:36:35 +02:00
Daniel Eklöf
7c5ea4fed6
particle/map: make local functions ‘static’ 2023-07-04 11:35:47 +02:00
Daniel Eklöf
9218ef234c
pkgbuild: add changelog 2023-06-13 17:07:22 +02:00
Daniel Eklöf
971361b046
yaml: keep original value when anchor and target node both defines the same key
When merging an anchor into a target yaml node, and both the target
node and the anchor defines the same key(s), keep the value from the
target node.

Closes #286
2023-04-09 09:27:31 +02:00
tiosgz
5e3859f218 module/network: allow poll-interval == 0
Apparently the possibility to disable it was missed when the interval
was migrated to use milliseconds.
2023-04-08 20:06:07 +00:00
Yutaro Ohno
963b9d47ee modules/dwl: handle the appid status
dwl added an "appid" field as output status [1]. We currently don't
handle this field, and thus output warnings that say "UNKNOWN action".

Handle the "appid" field correctly and expose a value of this field to
users. Also, suppress the warnings.

Link: 7f9a212476 [1]
2023-03-26 16:08:55 +09:00
Armin Fisslthaler
daeb59e021 i3: add native sorting to changelog 2023-03-24 19:00:21 +01:00
Armin Fisslthaler
3ec6fa1bc7 i3: update man page to include native sorting 2023-03-24 18:59:49 +01:00
Armin Fisslthaler
f21db9caca i3: add "native" sway/i3 sort mode
This adds a sort mode for workspaces which corresponds to the default
behavior in sway/i3.
2023-03-24 17:48:26 +01:00
Oleg Hahm
8ccd79ad08 modules/network: do not use IPv6 link-local
Probably you don't want to see your IPv6 link-local address but rather a
global one.
2023-03-08 18:41:20 +01:00
Daniel Eklöf
d1776714ed
Merge branch 'pipewire-roundf'
Closes #262
2023-01-21 15:50:22 +01:00
Leonardo Hernández Hernández
5da51210de
module/dwl: allow specify the name of tags 2023-01-16 19:53:21 -06:00
Ogromny
7773a17d57 CHANGELOG.md: add issue #262 2023-01-16 10:29:16 +01:00
Ogromny
bdbcc0100a modules/pipewire: change type of volume from uint8 to uint16 2023-01-16 10:29:16 +01:00
Ogromny
10fde4dd0a modules/pipewire: use roundf instead of ceilf for more accuracy 2023-01-16 10:29:16 +01:00
Daniel Eklöf
134ae847dc
module/river: add support for ‘layout’ events 2023-01-12 18:15:16 +01:00
Daniel Eklöf
f75168796a
module/pipewire: handle failures when trying to connect to pipewire
* Replace asserts() with error logs
* Handle not being connected in content()
* Return from run() with an error
2023-01-02 14:08:21 +01:00
Daniel Eklöf
d09d88b60b
ci: drop gitlab CI
We’re no longer mirroring to gitlab.
2023-01-02 13:52:39 +01:00
Daniel Eklöf
0f3894bf63
tag: handle width formatter on ints when no other formatting options are used
For example: in {cpu:3}, the ‘3’ were ignored, assuming ‘cpu’ was an
int tag.
2023-01-02 12:19:17 +01:00
Daniel Eklöf
38a1d0b57c
doc: tags: add a couple of formatting examples 2023-01-02 11:51:43 +01:00
Daniel Eklöf
11bef7dd08
doc: tags: re-arrange columns in ‘formatting’ table
Put the ‘description’ column last. Since the last column is expanded
to fill the screen, and the tags’ descriptions can be fairly long, it
makes sense to put the description column last.
2023-01-02 11:39:19 +01:00
Leonardo Gibrowski Faé
146759bd96
implement field width tag format option
This implements the possibility of specifying padding for numeric tags.
Both space and zero padding is supported.
2023-01-01 16:22:44 -03:00
Daniel Eklöf
73ccafdade
module/i3: fix regression in handling of persistent workspaces
bbd2394601 added support for ‘rename’
and ‘move’ events. In order to do that, it changed how workspace
events are matched against our internal workspace list: from string
comparing the workspace name, to checking i3/sway’s workspace ID
parameter.

This introduced a regression in our handling of persistent
workspaces. A persistent workspace is one that isn’t removed from the
bar when it’s deleted (empty, and switched away from) by i3/sway.

This concept doesn’t exist in i3/sway, but is something we’ve
added. Put simple, the way we do this is be keeping the workspace in
our list, even when i3/sway tells us it has been deleted.

However, at this point the workspace doesn’t have an ID anymore. And
the same is true at startup; when we initialize the persistent
workspaces, we only have their names. Not their IDs (since the
workspaces don’t actually exist yet).

To this the regression, we need to:

a) fallback to looking up workspaces by name. That is, if we fail to
  find one with a matching ID, try again using the workspace name. For
  _this_ to match, we also required the matched workspace to be a
  persistent workspace, with an ID < 0 (which essentially means the
  workspace doesn’t exist yet).

b) reset the ID to -1 when a persistent workspace is "deleted".

Closes #253
2022-12-28 15:21:46 +01:00
Leonardo Hernández Hernández
02d281df58
module/dwl: correctly handle the title
Uupdate the title when process a new batch of info instead of call
strtok_r and looping until `string` is NULL.

This fixes an issue where only the last part of the title was kept.
2022-12-28 15:19:11 +01:00
Daniel Eklöf
e4edbd26c6
modules: change min poll interval from 500ms to 250ms 2022-12-27 13:20:31 +01:00
Daniel Eklöf
310c07b03d
module/battery: using a static buffer in readline_from_fd() isn’t thread safe 2022-12-27 13:17:06 +01:00
Daniel Eklöf
2283647fc7
Merge branch 'poll-interval-milliseconds'
Closes #244
2022-12-23 11:06:14 +01:00
Daniel Eklöf
d26d3953f1
changelog: batter/network/script: poll-interval: seconds -> milliseconds 2022-12-22 12:09:43 +01:00
Daniel Eklöf
c4f820e486
module/script: poll-interval: convert value from ‘seconds’ to ‘milliseconds’ 2022-12-22 12:06:25 +01:00
Daniel Eklöf
500b051fe4
module/network: poll-interval: convert value from ‘seconds’ to ‘milliseconds’ 2022-12-22 12:06:15 +01:00
Daniel Eklöf
8fbbce10a5
module/battery: poll-interval: convert value from ‘seconds’ to ‘milliseconds’ 2022-12-22 11:59:02 +01:00
Daniel Eklöf
ac8e45c331
module/mem: cleanup poll-interval
* man page: spell out ‘milliseconds’
* use a ‘static const’ variable for min_poll_interval, instead of a
  macro
2022-12-22 11:47:44 +01:00
Daniel Eklöf
a18296a179
module/disk-io: cleanup poll-interval
* man page: spell out ‘milliseconds’
* use a ‘static const’ variable for min_poll_interval, instead of a
  macro
2022-12-22 11:47:44 +01:00
Daniel Eklöf
1f25978eb4
module/cpu: cleanup poll-interval
* man page: spell out ‘milliseconds’
* use a ‘static const’ variable for min_poll_interval, instead of a
  macro
2022-12-22 11:46:00 +01:00
Stanislav Ochotnický
c0e0702a6c Update Font Awesome 5 to 6
This makes it less likely that new users will get confused and
accidentally use fallback fonts.
2022-12-21 17:06:25 +01:00
Horus
23d47656e4 Merge pr 'doc: Reinclude yambar-modules man page' (#245) into master
Reviewed-on: https://codeberg.org/dnkl/yambar/pulls/245
2022-12-19 18:01:03 +00:00
Ben Brown
4318765030 doc: Reinclude yambar-modules man page 2022-12-19 17:56:25 +00:00
Daniel Eklöf
cb8acf261a
module/mem: rename ‘interval’ to ‘poll-interval’ 2022-12-18 16:51:41 +01:00
Daniel Eklöf
63c9c90a61
module/disk-io: rename ‘interval’ to ‘poll-interval’ 2022-12-18 10:38:56 +01:00
Daniel Eklöf
252a7a1580
doc: cpu: ‘interval’ has been renamed to ‘poll-interval’
Closes #241
2022-12-18 10:37:22 +01:00
Daniel Eklöf
710cede371
module/pipewire: disable debug logging 2022-12-17 18:26:31 +01:00
Daniel Eklöf
ede6a541e1
modules: meson: add missing ‘m’ (math) dependency
The following modules used functions from the ‘m’ (math)
library (e.g. round()), but didn’t explicitly link against it. This
caused build failures when no other plugin that _did_ link against ‘m’
was enabled.

* cpu
* mem
* pulse
* pipewire
* foreign-toplevel

Closes #239
2022-12-17 18:26:25 +01:00
Daniel Eklöf
a9ce81b376
changelog: add new ‘unreleased’ section 2022-12-17 10:32:36 +01:00
Daniel Eklöf
a599e7264f
Merge branch 'releases/1.9' 2022-12-17 10:32:14 +01:00
Daniel Eklöf
1353d635c2
meson/pkgbuild: bump version to 1.9.0 2022-12-17 10:29:11 +01:00
Daniel Eklöf
ef7b4ce9b3
changelog: prepare for 1.9.0 2022-12-17 10:28:41 +01:00
Daniel Eklöf
06bf127332
doc: expand last column to fill screen in all tables 2022-12-14 12:18:47 +01:00
Daniel Eklöf
b195bc4dcb
module/cpu: make ‘content’ particle a template
Before this patch, the cpu module instantiated a single particle (the
‘content’ particle), with one tag ("cpu") representing the total CPU
usage, and then one tag (cpuN) for each core.

This makes it cumbersome to configure, since you need to explicitly
reference each cpuN tag to get per-core usage.

This patch rewrites this, so that ‘content’ is now a template. It’s
instantiated once to represent the total CPU usage, and then once for
each core.

Each instance has a "cpu" tag, representing the CPU usage of that
core (or total usage). It also has an "id" tag. The ID is 0..n for
actual cores, and -1 for total usage.

This means you can do something like this in your config:

- cpu:
    content:
      map:
        conditions:
          id < 0: {string: {text: "Total: {cpu}%"}}
          id >= 0: {string: {text: "Core #{id}: {cpu}%"}}

Closes #207
2022-12-14 12:06:00 +01:00
Daniel Eklöf
2e0e1a402f
bar: also log module name when logging a failed module 2022-12-14 12:05:34 +01:00
Daniel Eklöf
3ca274759a
module: const:ify ‘module’ argument to module->description() 2022-12-14 12:05:17 +01:00
Daniel Eklöf
6794193791
Merge branch 'plugin-compile-time-optional' 2022-12-14 11:03:16 +01:00
Daniel Eklöf
95b1f5f261
modules: meson: regression: it’s ‘libudev’, not ‘udev’ 2022-12-14 11:01:05 +01:00
Daniel Eklöf
b22614ecc3
ci (woodpecker): add pipewire-dev to x86 builds
Before this, it was only added in the x64 builds
2022-12-14 10:54:26 +01:00
Daniel Eklöf
c4cc1b7a36
changelog: all modules are now compile-time optional 2022-12-14 10:30:20 +01:00
Daniel Eklöf
a53e48a2c1
doc: meson: only install man pages for modules we actually build 2022-12-14 10:29:08 +01:00
Daniel Eklöf
a5e79d14b7
pkgbuild: add pipewire dependency 2022-12-14 10:18:18 +01:00
Daniel Eklöf
690bd630a2
plugin: use auto-generated defines for enabled plugins 2022-12-14 10:16:54 +01:00
Daniel Eklöf
9ef6d73663
meson: make ‘foreign-toplevel’ plugin compile time optional 2022-12-14 10:08:48 +01:00
Daniel Eklöf
56b0047004
meson: make ‘river’ plugin compile time optional 2022-12-14 10:05:23 +01:00
Daniel Eklöf
1a81255579
meson: make ‘xwindow’ plugin compile time optional 2022-12-14 10:02:13 +01:00
Daniel Eklöf
a14d38b0cb
meson: make ‘xkb’ plugin compile time optional 2022-12-14 09:58:45 +01:00
Daniel Eklöf
b6450446a8
meson: make ‘sway-xkb’ plugin compile time optional 2022-12-14 09:53:24 +01:00
Daniel Eklöf
0cf0d64970
meson: pipewire-specific ‘json’ dependency 2022-12-14 09:50:22 +01:00
Daniel Eklöf
ec9ed66b6b
meson: make ‘script’ plugin compile time optional 2022-12-14 09:48:50 +01:00
Daniel Eklöf
eb26f64ea7
meson: make ‘removables’ plugin compile time optional 2022-12-14 09:46:08 +01:00
Daniel Eklöf
b901ac50ee
meson: make ‘network’ plugin compile time optional 2022-12-14 09:43:14 +01:00
Daniel Eklöf
8d5e8b5f20
meson: make ‘label’ plugin compile time optional 2022-12-14 09:41:44 +01:00
Daniel Eklöf
f54f583be1
meson: make ‘i3’ plugin compile time optional 2022-12-14 09:39:47 +01:00
Daniel Eklöf
85d55905f9
meson: make ‘mem’ plugin compile time optional 2022-12-14 09:36:48 +01:00
Daniel Eklöf
659b282445
meson: make ‘disk-io’ plugin compile time optional 2022-12-14 09:36:45 +01:00
Daniel Eklöf
b23365ccac
meson: make ‘cpu’ plugin compile time optional 2022-12-14 09:32:04 +01:00
Daniel Eklöf
25e123fbe6
meson: make ‘clock’ plugin compile time optional 2022-12-14 09:32:04 +01:00
Daniel Eklöf
aeef3eca0e
meson: make ‘battery’ plugin compile time optional 2022-12-14 09:32:04 +01:00
Daniel Eklöf
881359183f
meson: make ‘backlight’ plugin compile time optional 2022-12-13 16:47:48 +01:00
Daniel Eklöf
4c1398f1a5
meson: make ‘alsa’ plugin compile time optional 2022-12-13 16:41:50 +01:00
Daniel Eklöf
f8f0d7ae99
meson_options: sort plugin options 2022-12-13 16:36:55 +01:00
Daniel Eklöf
49576a26bf
readme: add ‘dwl’ to list of plugins 2022-12-13 16:33:56 +01:00
Daniel Eklöf
9b93b0794a
Merge branch 'i3-workspace-rename'
Closes #216
2022-12-13 16:27:40 +01:00
Daniel Eklöf
266a2efbb6
changelog: sway: workspace ‘move’ and ‘rename’ events 2022-12-13 16:25:55 +01:00
Timur Celik
24a3b90a01
modules: Implement workspace move event
Implementing the move event required to pass the IPC socket to
`i3_ipc_callback_t`, because we won't get notified about any visibility
changes of other workspaces.  That's why we query all workspaces again
after a focused workspace was moved.
2022-12-13 16:21:55 +01:00
Timur Celik
8f89545b32
modules: Warn for all unknown workspace events 2022-12-13 16:19:11 +01:00
Timur Celik
bbd2394601
modules: Implement workspace rename event
A renamed workspace caused yambar to abort in a failed assertion,
because workspace lookup was done by name and the `rename` event was not
implemented.  To resolve this issue this patch implements the `rename`
event and as a necessity changes workspace_lookup() to use ids instead
of names.
2022-12-13 16:16:16 +01:00
Daniel Eklöf
6fa9c47c0b
meson: summary: dwl: Wayland with upper case ‘W’ 2022-12-13 16:00:07 +01:00
Daniel Eklöf
7ddd009a5c
meson: sort plugin list in summary output 2022-12-13 15:59:40 +01:00
Daniel Eklöf
4631e75e28
meson: require version >= 0.59
Required by feature_option.allowed()
2022-12-13 15:58:59 +01:00
Ogromny
f5cfc103d0
modules/dwl: new module 2022-12-13 15:56:16 +01:00
Daniel Eklöf
6027b2728b
ci (sr.ht): it’s pulseaudio-dev, not libpulse-dev 2022-12-13 10:47:07 +01:00
Daniel Eklöf
bd607d7697
ci: install pipewire-dev; should ensure we build the pipewire plugin 2022-12-13 10:45:08 +01:00
Daniel Eklöf
302e0d5cc6
ci (sr.ht): install ‘dev’ version of “libpulse” 2022-12-13 10:42:53 +01:00
Ogromny
19a9f099e2 modules/pipewire: new module 2022-12-13 10:16:58 +01:00
Willem van de Krol
dcf21f0b06 modules: add pulse
The pulse module shows information about PulseAudio sinks and sources.
2022-12-12 18:51:28 +01:00
Daniel Eklöf
54c70bb6ad
readme: add missing modules to list of modules 2022-12-12 16:53:10 +01:00
Daniel Eklöf
f9fa43845e
changelog: ‘map’ changes: add ref to 182 2022-12-11 18:44:24 +01:00
Daniel Eklöf
ec86a7d290
ci (sr.ht): add flex+bison 2022-12-11 18:33:07 +01:00
Daniel Eklöf
3c3a881638
ci (gitlab): add flex+bison 2022-12-11 18:32:52 +01:00
Leonardo Gibrowski Faé
4a41d4296a
Implement '&&' and '||' operators on map
'-' is a valid character for tags.

Commit 03e1c7d (module/network: Add link stats, 2022-04-30) introduced
two new tags for the network module: `ul-speed` and `dl-speed`. These
use the `-` character, that was previously never used in any tag.

We had two options: either change those tags to use `_` instead, or just
accept `-`s as a valid character. Going forward, I can see many people
deciding to name their tags with `-` instead of `_`, so I believe it is
better to just accept it once and for all.

Note that `-` cannot be used as the first character of a tag (e.g.
`-tag1`) since the `-` has a special meaning in `.yml` files. I don't
believe this will happen often, however, and should be easy to both
detect and correct if it does.
2022-12-10 22:53:30 -03:00
Daniel Eklöf
463b39b56d
changelog: line-wrap long line 2022-12-09 15:25:29 +01:00
Leonardo Gibrowski Faé
87854fa101
float tag: let user specify number of decimals
Closes #200
2022-12-09 15:24:59 +01:00
Daniel Eklöf
8deac539ef
module/river: new workaround for river issue 69
River seat status events are not fired if the river interface is bound
before the output globals are (despite
zriver_status_manager_v1_get_river_seat_status() not taking an output
as argument). See https://github.com/riverwm/river/issues/69 for
details.

Up until now, we had a workaround for this, where we deferred binding
the seat status interface until after all globals have been processed.

This did not help with runtime changes. For example, if a monitor is
turned off/on (with e.g. wlr-randr), all future river seat status
output events were lost, since the new output global was being
bound *after* the river seat status object.

This patch implements a new workaround, where we re-bind the river
seat status interface every time an output global is added.
2022-10-27 15:59:32 +02:00
Daniel Eklöf
794b1ed633
module/river: fix broken debug log 2022-10-27 15:59:09 +02:00
Daniel Eklöf
8f1a123de2
module/network: only log SSID when different from current one
This prevents log spamming with poll-interval set

Closes #232
2022-10-16 16:17:08 +02:00
Daniel Eklöf
028011a816
module/sway-xkb: don’t add the “same” device multiple times
Not sure if Sway bug or not, but we’ve seen Sway presenting multiple
input devices with the exact same ID (and nothing else differentiating
them).

This caused a crash in the sway-xkb module, since we didn’t check if
we were already tracking the device, and thus bumped the
“num_existing_inputs” variable multiple times for the same input
object.

This lead to a content() returning an array with uninitialized
elements, and thus a crash.

Closes #229
2022-10-04 21:16:56 +02:00
Peter Rice
6ed576c719
particle/on-click: support next/previous buttons 2022-10-04 21:14:29 +02:00
Daniel Eklöf
8d5deda4e4
module/river: fix “use of uninitialized variable” warning
There were “goto err” statements before “unlock_at_exit” had been
initialized.
2022-10-03 09:52:44 +02:00
Daniel Eklöf
4143099e94
module/network: resurrect SSID
Recent kernels, or possibly updated wireless drivers, no longer
provide the SSID in `NL80211_CMD_NEW_STATION` responses.

For yambar, this meant the SSID was always missing.

This patch fixes this, by also issuing a NL80211_CMD_GET_SCAN
command. The response to this (NL80211_CMD_SCAN_RESULTS) _may_ return
many access points. Pick out the one that we’re associated with, and
inspect its BSS_INFORMATION_ELEMENTS. This is a raw data structure
containing, among other things, the SSID.

I haven’t been able to find any documentation of the format, but could
glean enough from
https://git.kernel.org/pub/scm/linux/kernel/git/jberg/iw.git/tree/scan.c#n2313
to be able to parse out the SSID.

Note that we get a “device or resource busy” error if we try to issue
both the NL80211_CMD_GET_STATION and the NL80211_CMD_GET_SCAN commands
at the same time. Therefore, we issue the GET_SCAN command after
completing the GET_STATION command.

Closes #226
2022-09-10 13:46:34 +02:00
Midgard
d1a8029e6c
module/mpd: add “file” tag 2022-09-03 12:12:11 +02:00
Daniel Eklöf
5da1cd4a38
module/script: process *all* transactions received in a single read()
When the script module received multiple transactions in a single
batch, only the first were processed. This lead to multiple,
unprocessed transactions stacking up in the receive buffer. Every time
a new transaction was received, we popped the oldest transaction from
the buffer, but never actually getting to the last one. This is
perceived as "lag" by the user, where the bar displays outdated
information.

Closes #221
2022-09-03 12:09:30 +02:00
Daniel Eklöf
d002919bad
module/network: generate nl80211 sequence number from /dev/urandom 2022-09-03 12:05:58 +02:00
Horus
6427e8213c Merge pull request 'doc: string particle’s “max” uses Unicode … now' (#220) from midgard/yambar:doc-unicode-ellipsis into master
Reviewed-on: https://codeberg.org/dnkl/yambar/pulls/220
2022-09-01 14:42:28 +02:00
Midgard
aa09d88d8e
doc: string particle’s “max” uses Unicode … now
And fix a typo in the yambar-particles file.
2022-09-01 12:18:49 +02:00
Daniel Eklöf
2759ba6349
module/network: generate nl80211 sequence number from /dev/urandom 2022-08-28 20:19:22 +02:00
Daniel Eklöf
4257c80eee
ci (sr.ht): pull directly from git.sr.ht 2022-08-23 16:42:56 +02:00
Leonardo Gibrowski Faé
084a9021b9
fix configurations in example scripts
We forgot to update the configurations in the example scripts to the new
map syntax based on conditions. This fixes that.

Related to #201
2022-07-22 10:11:25 +02:00
Baptiste Daroussin
b331473a6b
meson: add missing dependencies on wayland_client 2022-07-02 23:21:38 +02:00
Baptiste Daroussin
d15d1f58f7
meson: declare CFLAGS dependencies for xcb-stuff
on FreeBSD we need append to the lookup path a non default path to
find headers
2022-07-02 23:21:38 +02:00
Baptiste Daroussin
138db05d70
portability: remove unused header 2022-07-02 23:21:37 +02:00
Baptiste Daroussin
dd1280f49d
portability: add missing header for signal related functions 2022-07-02 23:21:28 +02:00
Baptiste Daroussin
8d91cbd8a3 modules: use portable function to count cpus 2022-06-23 14:11:07 +02:00
Daniel Eklöf
6c10eb2153
module/alsa: use channel’s dB range instead of raw volume, if available
For channels that have a defined dB range, use that instead of the raw
volume range when calculating the volume percent.

Also use the same logic as alsamixer when calculating the percent from
the dB values: assume a linear scale if the dB range is “small
enough”, and otherwise normalize it against a logarithmic scale.

With this, yambar’s “percent” value matches alsamixer’s exactly.

The ‘volume’ tag remains unchanged - it always reflects the raw volume
values.

Instead, we add a new tag ‘dB’, that reflects the dB values.

Closes #202
2022-06-21 19:49:06 +02:00
Leonardo Gibrowski Faé
a0c07d7836
modules: creates disk-io-module
This creates the disk-io-module, which displays io information read from
`/proc/diskstats`. Details on `diskstats` can be found on:
https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats
2022-06-17 12:04:13 -03:00
hiog
cb47c53de4 Update 'examples/configurations/laptop.conf' 2022-06-16 11:29:04 +02:00
Daniel Eklöf
605a6a9ede
particle/string: don’t try to call c32len() on a NULL string 2022-06-13 12:07:24 +02:00
Daniel Eklöf
1b03bd6bc0
particle/string: simplify; no need to call c32len(wtext) twice 2022-06-13 12:06:59 +02:00
Daniel Eklöf
36a4250a96
changelog: i3: newly created, unfocused, workspaces are considered non-empty 2022-06-11 12:00:16 +02:00
Daniel Eklöf
b0e132beaf
module/i3: if a new workspace is created, but unfocused, assume it’s not empty
If a window is created on an unfocused workspace, yambar did not
update the empty tag correctly. At least not for persistent
workspaces.

This is because yambar relies on focus events to determine a
workspace's "empty" state. Since the new window, on the new workspace,
isn't focused, there's no focus event, and yambar thinks the workspace
is still empty.

This patch changes the logic slightly; a new workspace is considered
non-empty if it isn't focused (and has a non-zero node count).

Closes #191
2022-06-11 11:59:41 +02:00
Johannes
03e1c7dc13 module/network: Add link stats
Exports two new tags from network module, `ul-speed` and `dl-speed`.
Because these work through polling, poll-interval must be set.
Otherwise, these two tags always will be 0.
2022-06-06 15:11:24 +02:00
Daniel Eklöf
eb5cb9869c
module/network: log signal strength and TX/RX bitrates at debug level 2022-06-06 14:30:22 +02:00
Daniel Eklöf
9353aa14fe
bar: set clip region before calling the particles’ expose() methods
This ensures particles that are “too wide” doesn’t try to render
outside the bar, possibly overrunning both margins and borders. Or
worse, crashes yambar.

Closes #198
2022-06-05 00:33:30 +02:00
Daniel Eklöf
a8994b9268
tag: for now, always use 2 decimals for floats
In the future, we’ll want to add a way to specify the number of
decimals, as part of the formatter.
2022-06-03 19:43:28 +02:00
Daniel Eklöf
8a8a40bfb5
changelog: sway-xkb: don’t crash on “added” event for already tracked device 2022-06-03 19:37:57 +02:00
Daniel Eklöf
f4ceaaad52
module/sway-xkb: handle device being added again
If a device is removed while the computer is hibernating, and then
reconnected after waking it up, Sway sends an “added” event without
first sending a “removed” event.

Yambar used to assert that an “added” event didn’t refer to an already
tracked device.

This patch changes this, and simply ignores duplicate “added” events.

Closes #177
2022-06-03 19:37:52 +02:00
Daniel Eklöf
ca077447c2
module/network: tx/rx-bitrate is now in bits/s instead of Mb/s 2022-06-02 22:25:53 +02:00
Daniel Eklöf
d9d5671e04
doc: tags: fix divisors for kb/mb/gb and kib/mib/gib
They were inversed: kb/mb/gb uses 1000^n, while kib/mib/gib uses
1024^n.
2022-06-02 22:21:09 +02:00
Daniel Eklöf
de4814d16e
tag: use as_float() when kb/mb/gb formatting a float tag value 2022-06-02 21:33:04 +02:00
Daniel Eklöf
c738f1c63d
module/river: add support for the ‘mode’ event
Seat status v3 adds a new ‘mode’ event, that informs us of the current
mode (as set by e.g. ‘riverctl enter-mode passthrough’)

The mode is exposed as a tag (named “mode”) on river’s “title”
particle:

  - river:
      title:
        map:
          default: {empty: {}}
          conditions:
            mode == passthrough:
              string: {text: " {mode} ", deco: {background: {color: ff0000ff}}}
2022-06-02 17:24:42 +02:00
Daniel Eklöf
a3a0334069
changelog: dwl-tags updates 2022-04-24 22:10:57 +02:00
Daniel Eklöf
5fc092a874
examples: dwl-tags: adapt parsing of output to recent DWL changes(?)
Closes #178
2022-04-24 22:10:49 +02:00
Daniel Eklöf
acc20913ef
Merge branch 'conditions-on-tag'
Closes #137
2022-04-24 21:07:33 +02:00
Leonardo Gibrowski Faé
3b5845370c doc: explain that order in map conditions matter 2022-04-24 15:47:53 -03:00
Leonardo Gibrowski Faé
82a3b2ae11 Updates CHANGELOG.md and changes map.c formatting 2022-04-24 11:21:40 -03:00
Leonardo Gibrowski Faé
0d878e8b5c Trimming outer '"' when parsing the values. 2022-04-23 17:13:24 -03:00
Leonardo Gibrowski Faé
4c4a20d835 Updated docs to comply with new map syntax 2022-04-23 17:13:24 -03:00
Leonardo Gibrowski Faé
2b103b7acd Implement conditions on tag
A condition is formed by:
    <tag> <op> <value>

<tag> is the normal yambar tag. <op> is one of '==', '!=', '<', '<=', '>', or
'>='. <value> is what you wish to compare it to.

'boolean' tags must be used directly. They falsehood is matched with '~':

    <tag>
    ~<tag>

Finally, to match an empty string, one must use ' "" ':
    <tag> <op> ""
2022-04-23 17:13:24 -03:00
Daniel Eklöf
4496d82cfb
changelog: hyperlink lists under their corresponding sub-section 2022-04-23 11:14:50 +02:00
Daniel Eklöf
1206153b03
changelog: convert all issue links to reference links in ‘unreleased’ 2022-04-22 20:25:11 +02:00
Daniel Eklöf
acb8a09376
changelog: turn all issue links into markdown hyperlinks 2022-04-22 20:23:08 +02:00
Daniel Eklöf
c1e549df54
examples: config: remove duplicate volume icons 2022-04-16 11:59:10 +02:00
Daniel Eklöf
9c28d36898
changelog: cpu: don’t error out on systems with SMT disabled 2022-04-07 13:33:41 +02:00
Daniel Eklöf
62ca06eccb
module/cpu: don’t use core ID from /proc/stat as array index
/proc/stat lists CPU usage, in the form:

  cpu ...
  cpu0 ...
  cpu1 ...
  ...
  cpuN ...

where the first line is a summary line. We’ve been using the CPU
numbers from /proc/stat to index into our internal stats array.

This doesn’t work on systems where the core IDs aren’t
consecutive. Examples of such systems are SMT systems with SMT
disabled. Here, /proc/stat may look like this instead:

  cpu ...
  cpu0 ...
  cpu2 ...
  cpu4 ...
  ...

With this patch, we ignore the CPU ID from /proc/stat. Instead, we use
a simple counter that is incremented for each (valid) cpu line found
in /proc/stat. To protect against corrupt /proc/stat content, stop
parsing /proc/stat if the number of parsed CPU lines exceed what we
consider to the be total number of CPUs in the system.

Closes #172
2022-04-07 13:28:35 +02:00
Daniel Eklöf
fce2787bdf
module/cpu: use get_nprocs() to retrieve the CPU count 2022-04-07 13:21:41 +02:00
Daniel Eklöf
43de7c8da8
Merge branch 'cloexec'
Closes #169
2022-03-29 21:57:06 +02:00
Daniel Eklöf
fd014dc33b
Don’t loop 65536 FDs, trying to close them, when fork+exec:ing
All FDs should now have the CLOEXEC flag set, and thus there’s no
longer needed to manually loop “all” possible FDs and (trying to)
close them.

Note: the alsa module (alsalib, actually) is “racy” - while booting
up, it temporarily opens the asoundrc file without CLOEXEC. If
e.g. the script module starts its script inside this window, it’ll
have a leaked FD. Not much we can do about it though :/

Closes #169
2022-03-29 18:23:55 +02:00
Daniel Eklöf
068c25d8f6
module/script: open comm-pipe + /dev/null with CLOEXEC
This ensures we don’t leak FDs when exec:ing e.g. on-click
handlers.

Note that the comm-pipe FD is *supposed* to stay open when we execing
the script. This is handled by the call to dup2(), which drops the
CLOEXEC flag. Since dup2() is called after the fork, the dup:ed FD is
never visible in the “parent” yambar process.
2022-03-29 18:22:08 +02:00
Daniel Eklöf
2b6f5b1e36
module/removables: open /proc/self/mountinfo with CLOEXEC 2022-03-29 18:21:44 +02:00
Daniel Eklöf
4bb81e8940
modules: add SOCK_CLOEXEC to all socket() calls 2022-03-29 18:21:13 +02:00
Daniel Eklöf
3ff1c95208
char32: only include stdc-predef.h if it is available
Use the (relatively new) macro __has_include() to check if
stdc-predef.h exists, and only include it if it does.

If stdc-predef.h does not exist, or if the compiler does not implement
__has_include(), stdc-predef.h is *not* included.
2022-03-20 10:50:57 +01:00
Daniel Eklöf
4daa3d9904
meson: stop using deprecated functions, require meson >= 0.58
* get_pkgconfig_variable() -> get_variable()
* prog.path() -> prog.full_path()
* meson.build_root() -> meson.global_build_root()
2022-02-27 11:32:46 +01:00
Daniel Eklöf
ffccabbb13
config: add inheritable option “font-shaping”
This patch adds an inheritable option, “font-shaping”, that controls
whether a particle that renders text should enable font-shaping or
not.

The option works similar to the ‘font’ option: one can set it at the
top-level, and it gets inherited down through all modules and to their
particles.

Or, you can set it on a module and it gets inherited to all its
particles, but not to other modules’ particles.

Finally, you can set it on individual particles, in which case it only
applies to them (or “child” particles).

When font-shaping is enabled (the default), the string particle shapes
full text runs using the fcft_rasterize_text_run_utf32() API. In fcft,
this results in HarfBuzz being used to shape the string.

When disabled, the string particle instead uses the simpler
fcft_rasterize_char_utf32() API, which rasterizes individual
characters.

This gives user greater control over the font rendering. One example
is bitmap fonts, which HarfBuzz often doesn’t get right.

Closes #159
2022-02-23 18:43:13 +01:00
Leonardo Neumann
265188ca4c
char32: add missing header to work with musl 2022-02-21 02:18:54 -03:00
Daniel Eklöf
c44970717b
module/i3: workspace::focus is apparently Sway only
On i3, users are currently greeted with:

  err: modules/i3.c:94: workspace reply/event without 'name' and/or
       'output', and/or 'focus' properties

This patch makes ‘focus’ an optional attribute. When missing, we
assume a node-count of 0, which means the workspace’s ‘empty’ tag will
always be true. Document this in the i3 man page.
2022-02-15 21:14:08 +01:00
Daniel Eklöf
1ce108f24e
Merge branch 'i3-strip-workspace-numbers' 2022-02-14 18:33:59 +01:00
Daniel Eklöf
ca407cd166
module/i3: treat workspaces on the form N:name as numerical 2022-02-14 18:33:14 +01:00
Daniel Eklöf
a2cf05a64d
module/i3: add ‘strip-workspace-numbers’ option
This is a boolean option. When set, “N:” prefixes will be stripped
from the workspaces’ name tag, *after* having been sorted (if the
‘sort’ option is being used).

This makes it useful to arrange the workspaces in a fixed order, by
prefixing the names with a number in the Sway config:

  set $ws1 “1:xyz”
  set $ws2 “2:abc”

Then, in the yambar config:

  i3:
    sort: ascending
    strip-workspace-numbers: true
2022-02-11 21:44:43 +01:00
Daniel Eklöf
605490c872
overline: new decoration
Similar to the ‘underline’ decoration
2022-02-10 20:49:09 +01:00
Daniel Eklöf
6ac046dec3
config: implement font fallback
Fonts in the configuration may now be a comma separated list of
fonts (all using the fontconfig format). The first font is the primary
font, and the rest are fallback fonts that will be searched, in order.
2022-02-10 18:34:15 +01:00
Daniel Eklöf
2cfe45ee81
changelog: add new ‘unreleased’ section 2022-02-05 18:02:50 +01:00
Daniel Eklöf
1fc6e77926
Merge branch 'releases/1.8' 2022-02-05 18:02:29 +01:00
Daniel Eklöf
3cc142a273
changelog: fix 1.8.0 header 2022-02-05 18:00:01 +01:00
Daniel Eklöf
5edc226c00
meson/pkgbuild: bump version to 1.8.0 2022-02-05 17:57:40 +01:00
Daniel Eklöf
973aa929c1
changelog: prepare for 1.8.0 2022-02-05 17:57:17 +01:00
Daniel Eklöf
b8ab8669cb
Merge branch 'fcft-3' 2022-02-05 17:54:51 +01:00
Daniel Eklöf
227f9c608a
ci: use fcft master branch 2022-02-05 17:54:39 +01:00
Daniel Eklöf
ff238e62ba
fcft: adapt to API changes in fcft-3.x 2022-02-05 12:51:08 +01:00
Daniel Eklöf
4fea561c6c
log: fold long line 2022-02-05 12:50:39 +01:00
Daniel Eklöf
52e0b5f3d1
tag: fix inverted logic for KiB vs KB logic 2022-02-01 19:01:04 +01:00
Daniel Eklöf
1cf14a7f80
Merge branch 'river-example' 2022-02-01 18:58:48 +01:00
Soc Virnyl S. Estela
7945a561d0
Add river-tags example 2022-01-17 10:01:55 +08:00
Daniel Eklöf
1d9297593e
bar/wayland: error handling when dispatching Wayland events 2022-01-09 23:12:52 +01:00
Daniel Eklöf
a59d3cfbd6
Merge branch 'wayland-survive-output-disable-enable'
Closes #106
2022-01-01 11:48:39 +01:00
Daniel Eklöf
f053ddff7d
changelog: bar does not exit when monitor is disabled/unplugged 2022-01-01 11:47:24 +01:00
Daniel Eklöf
56c4a1c751
bar/wayland: add support for new events in wl-output v4
* name()
* description()
2022-01-01 11:47:24 +01:00
Daniel Eklöf
2a0a722c13
bar/wayland: handle layer surface being closed
If the output we’re mapped on is disabled (or disconnected), the
compositor will unmap us.

Up until now, our response was to simply shutdown.

Now, we destroy the surface, remove all pending rendering buffers, and
all further calls to commit() will return immediately, without doing
anything.

If the user has configured a specific monitor to use, we wait for that
output to come back. When it does, we re-create the layer surface and
then we’re up and running again.

Bars running on the “default” monitor are handled in a similar
way. Since we don’t have an output name from the configuration, we
instead store the name of the output we were mapped on, when we’re
either unmapped from that output, or that output global is destroyed.

As soon as we see that output come back, we re-create the layer
surface.
2022-01-01 11:43:40 +01:00
Daniel Eklöf
37e7c2b2c1
Merge branch 'removables-audio-cd'
Closes #146
2021-12-26 16:13:56 +01:00
Daniel Eklöf
0aff641d0c
changelog: audio CD support 2021-12-26 12:30:08 +01:00
Daniel Eklöf
52e2540d42
doc: yambar-modules-removables: add ‘audio’ tag 2021-12-26 12:25:00 +01:00
Daniel Eklöf
d9316a202d
module/removables: audio CD support
Audio CDs are special, in that they don’t (usually) have any data
partitions. They also don’t have a volume label. They just have
tracks.

Before this patch, we ignored all optical mediums that did *not* have
a filesystem (that includes audio CDs).

Now, instead of using the ID_FS_USAGE property to determine whether
there’s media present in the CD-ROM or not, we use the
ID_CDROM_MEDIA. This property is set to ‘1’ for both audio CDs and
data CDs.

Then, we read the ID_CDROM_MEDIA_TRACK_COUNT_AUDIO property to
determine how many audio tracks there are.

If the CD has a filesystem, we treat it as a data CD, and use the
already existing add_partition() function to track it.

If the CD does _not_ have a filesystem, but it _does_ have at least
one audio track, we treat it as an audio CD and use the new
add_audio_cd() function to track it.

This function is almost identical to add_partition(), but instead of
reading the ID_FS_LABEL property, it reads the
ID_CDROM_MEDIA_TRACK_COUNT_AUDIO property and synthesizes a label on
the form “Audio CD - N tracks”.

Finally, a new boolean tag, “audio”, has been added. It is set to true
for audio CD “partitions”, and to false in all other cases.
2021-12-26 12:22:54 +01:00
Daniel Eklöf
4ff1c43669
Merge branch 'mem-and-cpu-modules' 2021-12-21 19:21:44 +01:00
Alexandre Acebedo
e83c4bd8c1 misc: add format files for clang-format and editorconfig 2021-12-21 18:44:37 +01:00
Alexandre Acebedo
ae5c7e0fc3 modules: add cpu module 2021-12-21 18:44:36 +01:00
Alexandre Acebedo
337ce7681f modules: add mem module 2021-12-21 18:44:14 +01:00
Daniel Eklöf
d8d44b0f33
meson: generate-version: use CURRENT_SOURCE_DIR instead of SOURCE_ROOT 2021-12-21 13:41:46 +01:00
Daniel Eklöf
0af9ce354b
Merge branch 'document-on-click-advanced-syntax'
Closes #138
2021-12-20 19:52:41 +01:00
horus645
f7206ef08d Added documentation for discriminated on-click events 2021-12-20 14:03:19 -03:00
Daniel Eklöf
375c7ca89c
Merge branch 'i3-empty-tag'
Closes #139
2021-12-20 17:25:00 +01:00
Daniel Eklöf
26ea137938
changelog: i3/sway: ‘empty’ tag 2021-12-19 17:54:41 +01:00
Daniel Eklöf
8475ca1603
doc: i3: document the new ‘empty’ tag 2021-12-19 17:54:41 +01:00
Daniel Eklöf
3e133d8618
module/i3: add ‘empty’ boolean tag
Set to true for empty (no windows) workspaces.

Mostly useful with persistent workspaces, to be able to differentiate
between invisible non-empty workspaces and actually empty
workspaces (the latter not being possible with non-persistent
workspaces).
2021-12-19 17:54:40 +01:00
Daniel Eklöf
1bb77e59f3
Merge branch 'poll-failures' 2021-12-18 14:57:58 +01:00
Daniel Eklöf
9cff50a39e
module/xwindow: handle poll() failures 2021-12-17 11:55:38 +01:00
Daniel Eklöf
96c75b7f73
module/xkb: handle poll() failures 2021-12-17 11:55:38 +01:00
Daniel Eklöf
ffa86d84a5
module/removables: handle poll() failures 2021-12-17 11:55:36 +01:00
Daniel Eklöf
5d09e59f11
module/mpd: handle poll() failures 2021-12-17 11:55:36 +01:00
Daniel Eklöf
8a11a3fbe5
module/clock: handle poll() failures 2021-12-17 11:55:31 +01:00
Daniel Eklöf
cdd0b5b4f0
module/clock: fold long line 2021-12-17 11:28:05 +01:00
Daniel Eklöf
82ef48f666
module/clock: remove unused include 2021-12-17 11:27:53 +01:00
Daniel Eklöf
f922973450
module/battery: handle poll() failures 2021-12-17 11:25:37 +01:00
Daniel Eklöf
6612a9af9f
module/backlight: handle poll() failures 2021-12-17 11:23:59 +01:00
Daniel Eklöf
f71e7c2905
Merge branch 'x11-struts' 2021-12-11 12:32:09 +01:00
natemaia
39a5493450 Fix: X11 struts
https://specifications.freedesktop.org/wm-spec/wm-spec-1.3.html#idm45643490065584
2021-12-09 02:07:24 +01:00
Catterwocky
b562f1310b Fix yaml indentation in docs
It is unfortunate that the first example given by the manpage is not working.
2021-12-04 17:49:39 +01:00
Daniel Eklöf
9f2197ca1c
Merge branch 'config-unsigned-ints'
Closes #129
2021-11-15 20:02:11 +01:00
Daniel Eklöf
9d94cec549
changelog: integer options incorrectly allowed < 0 values 2021-11-15 18:21:19 +01:00
Daniel Eklöf
4f0d27bc7e
particles: verify: use conf_verify_unsigned() for options that should be >= 0 2021-11-15 18:17:52 +01:00
Daniel Eklöf
f166bbbf54
modules: verify: use conf_verify_unsigned() for options that should be >= 0 2021-11-15 18:17:29 +01:00
Daniel Eklöf
b6931c6ed0
decos: verify: all integer options are supposed to be unsigned 2021-11-15 18:17:12 +01:00
Daniel Eklöf
23f12a65b2
conf: bar/border: verify: all integer options are supposed to be unsigned 2021-11-15 18:16:37 +01:00
Daniel Eklöf
72056c50cf
config-verify: add conf_verify_unsigned()
Like conf_verify_int(), but also requires the integer to be >= 0
2021-11-15 18:16:15 +01:00
Daniel Eklöf
11bb45aa87
doc: script: add missing column in options table 2021-11-15 18:15:52 +01:00
Daniel Eklöf
d40220e511
bar/wayland: handle failure to set initial size in setup() 2021-11-15 18:06:10 +01:00
Daniel Eklöf
58038a4236
doc: battery: some batteries enter "unknown" under normal operation 2021-10-31 21:07:09 +01:00
Daniel Eklöf
8324112504
example: battery: map ‘unknown’ to ‘discharging’
Related to #123
2021-10-30 18:45:43 +02:00
Daniel Eklöf
66ec7a85a1
example: conf: foreign-toplevel: merge ‘: ’ and ‘{title}’ 2021-10-24 18:34:43 +02:00
Daniel Eklöf
0f6d165084
Merge branch 'border-deco' 2021-10-24 18:22:44 +02:00
Daniel Eklöf
134141b7c5
doc: decorations: document the new ‘border’ decoration 2021-10-24 18:22:21 +02:00
Daniel Eklöf
f0782d5124
deco: new decoration, ‘border’
Kind of like “underline”, but draws a border around the entire
particle.
2021-10-24 18:22:21 +02:00
Daniel Eklöf
c9abb6b98f
Merge branch 'network-dont-send-nl80211-req-if-family-id-not-set' 2021-10-24 18:21:51 +02:00
Daniel Eklöf
31f160141e
changelog: network: failure to retrieve wireless attributes 2021-10-24 17:50:10 +02:00
Daniel Eklöf
9ffd305b59
network: must re-send station request when we receive family ID
The get station request also fails if the family ID is invalid.
2021-10-24 17:47:54 +02:00
Daniel Eklöf
4e96dbd7f7
network: don’t send nl80211 request if we don’t have a family-id
This fixes an issue where we sometimes failed to retrieve the SSID; we
sent one initial request before the family ID had been set.

Then, we received the family ID and tried to send another
request. However, if the first request was still in progress, the
second request was never made.

Since the first request lacked the family ID, that response didn’t
contain anything useful.
2021-10-24 17:35:15 +02:00
Daniel Eklöf
7d715d81a8
example: conf: use foreign-toplevel instead of i3’s ‘current’ 2021-10-24 17:24:15 +02:00
Daniel Eklöf
cad9dd8efd
ci: also build release branches 2021-10-23 16:03:21 +02:00
Daniel Eklöf
bd44e82eca
bar/wayland: coalesce “refresh” commands
This reduces the number of repaints primarily during startup, but also
when e.g. switching workspace.
2021-10-22 18:07:14 +02:00
Daniel Eklöf
939b81a4ea
bar/wayland: create comm pipe with CLOEXEC | NONBLOCK 2021-10-22 18:06:43 +02:00
Daniel Eklöf
fee0b91174
bar/wayland: remove unused member ‘bar_expose’ 2021-10-22 18:06:19 +02:00
Daniel Eklöf
4f3aa8c6b0
bar: do a synchronous “refresh” *before* starting the modules
This ensures the surface has been mapped, and our “current” output is
known.

This fixes an issue where modules depending on the current output
being known failed to update correctly during startup.
2021-10-22 18:05:20 +02:00
Daniel Eklöf
957e25914c
Merge branch 'typo-in-example-conf' 2021-10-20 19:52:56 +02:00
mz
2b670ea612 remove an extra space
Remove the extra space due to `did not find expected key while parsing a block mapping` issue
2021-10-19 10:11:46 +02:00
Daniel Eklöf
d7a58e4ee0
Merge branch 'bar-initial-refresh'
Closes #116
2021-10-17 17:22:02 +02:00
Daniel Eklöf
94a0154c23
bar: refresh before starting the main loop
This ensures the bar surface gets mapped, and a background + border
rendered, even though no module “refreshes” it during initialization.

Closes #116
2021-10-17 16:48:09 +02:00
Daniel Eklöf
515b36da0d
foreign-toplevel: refresh the bar when a top-level is closed
Fixes an issue where the last active window is still being displayed
in the bar after the corresponding top-level has been closed.

Reported here: https://github.com/johanmalm/labwc/issues/73#issuecomment-945007028
2021-10-17 09:52:28 +02:00
Daniel Eklöf
9fe8ef2574
ci: run “yambar --version” after each build 2021-10-11 20:21:10 +02:00
Daniel Eklöf
9a1371b96a
generate-version: handle git repo not having any tags 2021-10-11 20:21:07 +02:00
Daniel Eklöf
6250360c58
ci: limit builds to the master branch (and pulls targeting it) 2021-10-10 19:05:47 +02:00
Daniel Eklöf
77303e8173
ci: codeberg CI 2021-10-10 18:46:45 +02:00
Daniel Eklöf
4cd031bc73
Merge branch 'clang13' 2021-10-04 16:37:14 +02:00
Jan Beich
03476e9360 yml: silence unused variable warning with Clang 13
yml.c:383:9: error: variable 'indent' set but not used [-Werror,-Wunused-but-set-variable]
    int indent = 0;
        ^
2021-10-03 23:40:23 +00:00
Jan Beich
ec465ba3b3 wayland: unbreak build without memfd_create
New FreeBSD versions have memfd_create but other BSDs don't.

bar/wayland.c:774:15: error: implicit declaration of function 'memfd_create' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
    pool_fd = memfd_create("yambar-wayland-shm-buffer-pool", MFD_CLOEXEC);
              ^
bar/wayland.c:774:62: error: use of undeclared identifier 'MFD_CLOEXEC'
    pool_fd = memfd_create("yambar-wayland-shm-buffer-pool", MFD_CLOEXEC);
                                                             ^
2021-10-03 23:07:23 +00:00
Daniel Eklöf
f8e544ae05
Merge branch 'freebsd-step1' 2021-10-02 12:33:08 +02:00
Baptiste Daroussin
82c92185ea memfd: linux/memfd does not exist on FreeBSD
memfd_create is defined in sys/mman.h on FreeBSD
2021-10-01 17:22:55 +02:00
Baptiste Daroussin
bfff39b7c9 main: cast sig_atomic_t to long, to fix the printf portability 2021-10-01 17:22:55 +02:00
Daniel Eklöf
804af178d5
Merge branch 'udev-monitor-receive-device-returning-null'
Closes #109
2021-10-01 16:45:22 +02:00
Baptiste Daroussin
6a9f66b837 meson: add the library needed for the FreeBSD support 2021-10-01 10:52:02 +02:00
Baptiste Daroussin
2e6847077e signal: add the signal header needed for the signal functions 2021-10-01 10:52:00 +02:00
Baptiste Daroussin
26892c66b2 headers: basename is defined under libgen.h 2021-10-01 10:51:56 +02:00
Daniel Eklöf
1bcf116859
modules: handle udev_monitor_receive_device() returning NULL
Closes #109
2021-09-30 10:01:36 +02:00
Daniel Eklöf
0f389435ca
changelog: battery: unknown states map to unknown, not discharging 2021-09-30 10:00:40 +02:00
Daniel Eklöf
fa760ffa65
Merge branch 'fix-battery-unknown' 2021-09-30 09:59:25 +02:00
Daniel Eklöf
76225a8366
doc: particles: line wrap 2021-09-20 19:30:37 +02:00
Daniel Eklöf
87f38d19a0
Merge branch 'ramp-overwrite-bounds' 2021-09-20 19:29:23 +02:00
Vincent Fischer
535d49e9c3 allow ramp particles to overwrite min and max 2021-09-20 19:08:18 +02:00
Stanislav Ochotnický
8709e8da38 Make default battery state as unknown
When a given battery is not found or otherwise state cannot be read - we
default to "unknown" state instead of discharging with incomplete data
2021-09-20 08:38:54 +02:00
Daniel Eklöf
7bbcad55e4
Merge branch 'add-mpris-docs-to-script' 2021-09-17 20:23:40 +02:00
Daniel Eklöf
185f313580
Merge branch 'make-mpd-support-optional' 2021-09-17 20:17:05 +02:00
Stanislav Ochotnický
13b5934e65 Add MPRIS example in script module documentation 2021-09-16 19:27:10 +02:00
Stanislav Ochotnický
e723b039ad Make libmpdclient an optional dependency
Without this change yambar can't be installed/used without libmpdclient even for
people who do not use MPD. Let's make this optional.

We could put the optional module summary in the module meson.build but we'd have
to move summary() in main meson.build so that they appear in proper order.
2021-09-16 11:46:22 +02:00
Daniel Eklöf
12f7802537
Merge branch 'clock-utc' 2021-09-12 21:00:11 +02:00
anb
ba5b28f437 clock: add a config option to show UTC time 2021-09-12 11:19:53 -07:00
Daniel Eklöf
60904bb18f
changelog: left- and right-margin options being rejected as invalid 2021-09-12 10:30:36 +02:00
Daniel Eklöf
8076a8da2a
Merge branch 'left-right-margin-typo' 2021-09-12 09:57:25 +02:00
Daniel Eklöf
3ff9fcab48
changelog: add new ‘unreleased’ section 2021-09-12 09:56:01 +02:00
Daniel Eklöf
dd19f070b3
Merge branch 'releases/1.7' 2021-09-12 09:55:25 +02:00
nogerine
ab0323e35e
config: fix incorrect attribute name in validation 2021-09-11 00:15:14 +00:00
Daniel Eklöf
42d944d020
meson/pkgbuild: bump version to 1.7.0 2021-09-03 20:39:01 +02:00
Daniel Eklöf
6fa118a5e5
changelog: prepare for 1.7.0 2021-09-03 20:38:26 +02:00
Daniel Eklöf
7a5ce73c94
Merge branch 'tag-formatters' 2021-09-01 19:12:42 +02:00
Daniel Eklöf
eff890ab9d
tag: add kib/mib/gib formatters 2021-09-01 19:12:29 +02:00
Daniel Eklöf
8c2e5d8bde
doc: yambar-tags: add missing last column to kb/mb formatters 2021-09-01 19:12:29 +02:00
Daniel Eklöf
f0b16033fe
doc: yambar-tags: codespell: mininum -> minimum 2021-09-01 19:12:28 +02:00
Daniel Eklöf
a6194c63e6
tag: add kb/mb/gb formatters 2021-09-01 19:12:27 +02:00
Daniel Eklöf
e201cc3d30
tag: add a ‘%’ formatter
Range tags can now be rendered as a percentage value, by using a ‘%’
formatter:

  {tag_name:%}
2021-09-01 19:12:16 +02:00
Daniel Eklöf
0da24198b3
Merge branch 'network-wifi' 2021-09-01 19:11:37 +02:00
Daniel Eklöf
4ab3263ebb
changelog: network: poll-interval 2021-09-01 19:11:14 +02:00
Daniel Eklöf
dabb2e1407
module/network: add support for periodically polling Wi-Fi stats
This adds a new ‘poll-interval’ option to the network module. When set
to a non-zero value, the following Wi-Fi stats will be updated:

* Signal strength
* RX+TX bitrate
2021-09-01 19:10:23 +02:00
Daniel Eklöf
d450bf12a1
module/network: re-request station info when we’re re-connected 2021-09-01 19:10:22 +02:00
Daniel Eklöf
e8a2f8df9a
module/network: reset signal strength and RX+TX bitrates on disconnect 2021-09-01 19:10:22 +02:00
Daniel Eklöf
066427d77a
examples: add ssid to wifi-network module 2021-09-01 19:10:22 +02:00
Daniel Eklöf
25379b7e1f
changelog: new network tags (ssid, signal rx+tx bitrate) 2021-09-01 19:10:22 +02:00
Daniel Eklöf
01ee028c4d
doc: network: document ssid, signal, rx-bitrate and tx-bitrate 2021-09-01 19:10:22 +02:00
Daniel Eklöf
a685dadb75
module/network: expose signal strength and rx+tx bitrates 2021-09-01 19:10:22 +02:00
Daniel Eklöf
b27eff36f9
module/network: nl80211: join the MLME mcast group, ignore messages not for us
This cleans up the nl80211 handling quite a bit, and adds initial
support for nl80211 notifications.

* We now join the nl80211 MLME multicast group (done by parsing the
  CTRL_ATTR_MCAST_GROUPS attribute in the reply to our
  CTRL_CMD_GETFAMILY request).  This gives us CONNECT and DISCONNECT
  notifications, allowing us to request and reset SSID that way, instead
  of detecting the link’s OPER state.

* Before parsing an nl80211 message, verify it’s for us, by looking
  for a NL80211_ATTR_IFINDEX attribute in the message (and comparing the
  contents with our previously detected ifindex).
2021-09-01 19:10:22 +02:00
Daniel Eklöf
9b3548736a
module/network: refactor: add foreach_nlattr()
This function iterates all attributes of a NETLINK_GENERIC message,
and calls the provided callback for each attribute.
2021-09-01 19:10:22 +02:00
Daniel Eklöf
5249d9ef79
module/network: use separate functions to connect to RT/GENL netlink 2021-09-01 19:10:21 +02:00
Daniel Eklöf
d39e6b8b94
module/network: initial support for Wifi extensions
Currently capable of getting the SSID.
2021-09-01 19:10:21 +02:00
Daniel Eklöf
e4e9587322
particle/progress-bar: fix regression in width calculation: = -> += 2021-08-30 18:00:41 +02:00
Daniel Eklöf
e9d762fa03
module/foreign-toplevel: disable debug logging 2021-08-27 10:14:54 +02:00
Daniel Eklöf
149798fe98
Merge branch 'alsa-capture-devices' 2021-08-27 06:42:42 +02:00
Daniel Eklöf
1079bca3eb
Silence “variable length array bound evaluates to non-positive value 0” 2021-08-26 13:27:29 +02:00
Daniel Eklöf
d1c7647b03
module/alsa: add support for capture devices
This mostly comes down to tracking whether each channel is a playback,
or capture channel, and using the appropriate APIs when dealing with
it.

Some cleanup related to this:

* Add a channel struct, for per-channel data. Previously, our channel
  list was just a list of ALSA channel IDs.
* We now store current volume per-channel (but volume min/max is
  per-device)
* Muted state is stored per-channel
* Track both the device’s playback and capture volume ranges, as well
  as whether the device *has* playback or capture volume.
* Get the playback/capture volume ranges once, during init, instead of
  at each update.
* Use struct pointers for the volume/muted channels. This way we don’t
  have to iterate all channels and to string comparisons on the name
  each time we update our state.
2021-08-26 11:03:12 +02:00
Daniel Eklöf
7c7c4e7ce9
module/alsa: rename {volume,muted}_channel -> {volume,muted}_name 2021-08-26 09:42:41 +02:00
Daniel Eklöf
b8dd247111
meson: run generate_version.sh in a C locale
Previously, only the date command inside the script was run with
LC_TIME=C.

But there’s no reason to be that conservative; we absolutely do not
want _anything_ in that script to generate locale dependent output.
2021-08-25 19:03:32 +02:00
Daniel Eklöf
427e0ce418
Merge branch 'zero-width-exposables-regressions' 2021-08-25 18:48:21 +02:00
Daniel Eklöf
7ca22a6dab
bar: don’t adjust spacing for left/center/right widths if all exposables are zero-width 2021-08-25 18:48:15 +02:00
Daniel Eklöf
af0b7e57d8
particle/ramp: don’t add margins if all sub-items are zero-width
That is, if all sub-items are zero-width, make sure *we* return a zero
width.
2021-08-25 18:48:15 +02:00
Daniel Eklöf
def90edde1
particle/progress-bar: don’t add margins if all sub-items are zero-width
That is, if all sub-items are zero-width, make sure *we* return a zero
width.
2021-08-25 18:48:15 +02:00
Daniel Eklöf
dab6428859
particle/map: don’t add margins if all sub-items are zero-width
That is, if all sub-items are zero-width, make sure *we* return a zero
width.
2021-08-25 18:48:15 +02:00
Daniel Eklöf
73e1d328c3
particle/list: don’t adjust spacing if all sub-items are zero-width
That is, if all sub-items are zero-width, make sure *we* return a zero
width, instead of a negative width.
2021-08-25 18:48:15 +02:00
Daniel Eklöf
ca43eb3016
particle/dynlist: don’t adjust spacing if all sub-items are zero-width
That is, if all sub-items are zero-width, make sure *we* return a zero
width, instead of a negative width.
2021-08-25 18:48:15 +02:00
Daniel Eklöf
0abc0f37dd
Merge branch 'river-rename-per-output-to-all-monitors' 2021-08-25 18:47:48 +02:00
Daniel Eklöf
103c3102a9
module/river: rename the ‘per-output’ option to ‘all-monitors’
This also inverts its meaning.
2021-08-25 18:47:33 +02:00
Daniel Eklöf
621f0e18e6
Merge branch 'foreign-toplevel' 2021-08-25 18:46:35 +02:00
Daniel Eklöf
9681e0aabe
module/foreign-toplevel: require version 3 of wl-output interface
The bar itself already does this, and doing so means we can always use
wl_output_release() (instead of wl_output_destroy()).
2021-08-25 18:46:19 +02:00
Daniel Eklöf
589a6f528a
module/foreign-toplevel: track outputs each toplevel is mapped on
* Bind the foreign-toplevel-manager object *after* the first round of
  global objects. This ensures we bind all pre-existing wl-output
  objects before binding the toplevel manager. This is important, since
  otherwise we wont get any output_enter() events for the initial set of
  toplevels.

* Bind xdg-output-manager, to be able to bind xdg-output objects for
  each wl-output.

* Add xdg-output-listener to each wl/xdg-output, to be able to get the
  outputs’ names.

* Add a list of outputs to each toplevel. The output_enter() event
  adds to this list, and output_leave() removes from it.

* Add option ‘all-monitors’. When not set (the default), toplevels are
  only included in the generated content if they are mapped on the same
  output as the bar itself. When *not* set, all toplevels are always
  included in the generated content.
2021-08-25 18:46:19 +02:00
Daniel Eklöf
01c6fc5f52
changelog: foreign-toplevel module 2021-08-25 18:46:19 +02:00
Daniel Eklöf
fe6cc43ad8
doc: river: provide -> provides 2021-08-25 18:46:19 +02:00
Daniel Eklöf
2173e0dc4d
doc: add man page for the new foreign-toplevel module 2021-08-25 18:46:19 +02:00
Daniel Eklöf
560d7464b4
module/foreign-toplevel: initial support
* ‘content’ is a template; the module returns a list of toplevels,
  each one instantiated using the content template.
* Each toplevel has 6 tags:
  - app-id (string)
  - title (string)
  - maximized (bool)
  - minimized (bool)
  - activated (bool)
  - fullscreen (bool)

To show the application name and title of the currently active window,
one can do:

    - foreign-toplevel:
        content:
          map:
            tag: activated
            values:
              false: {empty: {}}
              true: {string: {text: "{app-id}: {title}"}}
2021-08-25 18:46:18 +02:00
Nulo
eb94c8cceb
Add layer option
Only applies to Wayland and the default is still bottom.
2021-08-25 18:42:46 +02:00
Daniel Eklöf
7e7c011126
module/river: use wl_output_release() instead of wl_output_destroy() 2021-08-25 09:45:08 +02:00
Daniel Eklöf
0963afd282
example/conf: alsa: use the new ‘online’ tag 2021-08-24 16:57:22 +02:00
Daniel Eklöf
36de95cc2a
Merge branch 'alsa-inotify' 2021-08-23 18:34:18 +02:00
Daniel Eklöf
a5be550964
module/alsa: free channel list on each connect attempt
Otherwise we’ll keep adding the same channel(s) over and over again,
for each (successful) connect attempt.

I.e. if you plug and unplug an USB soundcard repeatedly, we’ll keep
extending the channel list each time.
2021-08-21 15:26:53 +02:00
Daniel Eklöf
360e1fbada
module/alsa: don’t re-create the /dev/snd inotify watcher after each connect failure
When e.g. a USB soundcard is inserted, we get several CREATE
events. In my experiments, we only succeed in connecting to ALSA after
the last event.

This means, we’ll have several CREATE events that we receive, remove
the watcher, attempt to connect, fail, and then re-add the watcher.

What if that “last” CREATE event occurs while our watcher has been
removed? That’s right, we miss it, and will get stuck waiting forever.

The solution is keep the watcher around.

Now, if we’ve been successfully connected to ALSA for a long time,
chances are we’ve built up events (for other cards, for example). We
don’t want to trigger a storm of re-connect attempts, so drain the
event queue after having been disconnected from ALSA.

There *is* a small race here - if a card is removed and
re-added *very* fast, we _may_ accidentally drain the CREATE event. I
don’t see this happening in reality though.
2021-08-21 11:08:45 +02:00
Daniel Eklöf
5af070ee1d
log: LOG_ERRNO: include numeric value of errno 2021-08-21 11:08:06 +02:00
Daniel Eklöf
25c20e5534
module/alsa: use inotify on /dev/snd instead of a poll timeout
While waiting for the configured ALSA card to become available, use
inotify and watch for CREATE events on /dev/snd instead of
polling (using a timeout in the poll(3) call).

Note that we don’t know the actual names of the files that (will) be
created. This means:

* Every time we see a CREATE event on /dev/snd, we *try* to connect to
  ALSA. If we fail, we go back to watching /dev/snd again.
* ALSA (not yambar) will log an error message each time we fail.
2021-08-21 10:52:12 +02:00
Daniel Eklöf
db12ceb026
module/alsa: volume/muted: default to “unset”; use first available channel 2021-08-20 21:41:21 +02:00
Daniel Eklöf
ae7d54fb80
module/alsa: add ‘volume’ and ‘muted’ options
These options allows you to select which channel to use as volume
source, and which channel to use as the source for the muted state.

With this, we can also remove the check for *all* (playback) channels
having the same volume/muted state. And with that, we no longer need
to warn when not all channels have the same volume/muted state.
2021-08-20 20:24:44 +02:00
Daniel Eklöf
591cae4c6d
Merge branch 'alsa-handle-device-disconnect'
Closes #59
Closes #61
Closes #86
2021-08-19 19:31:28 +02:00
Daniel Eklöf
be6e714eb0
module/alsa: handle ALSA device disappearing
With this patch, a non-existing ALSA device is no longer considered a
fatal error. Instead, we keep retrying until we succeed.

Furthermore, if we have successfully opened the ALSA device, and it
then disappears, we a) no longer crash, or cause 100% CPU usage, and
b) try to re-connect to the device.

With this, we now handle e.g. USB soundcards being disconnected and
then re-connected. We should also handle pseudo devices, like pipewire
provides ones, when yambar is started before pipewire.

Closes #59
Closes #61
Closes #86
2021-08-19 19:26:40 +02:00
Daniel Eklöf
8b6b82f1e5
module/river: add support for river-status v2 (‘urgent’ views) 2021-08-19 19:25:18 +02:00
Daniel Eklöf
b00954045b
Merge branch 'log-level'
Closes #84
2021-08-15 16:45:02 +02:00
Daniel Eklöf
495a4c8fb1
log: remove unused include “debug.h” 2021-08-15 16:34:46 +02:00
Daniel Eklöf
be10465a3b
main: add -d,--log-level=info|warning|error|none
Closes #84
2021-08-15 11:43:49 +02:00
Daniel Eklöf
7d3851046e
log: pull in log.{c,h} from foot 2021-08-15 11:41:12 +02:00
Daniel Eklöf
58a52512dd
changelog: slight rewording 2021-08-12 19:31:22 +02:00
Nulo
910522262f
Only add spacing if the module is not empty
If the module is empty (width is 0) no spacing will be rendered for it.

This makes modules that auto-hide (for example, network modules for interfaces not used all of the time) occupy no space in the bar.
2021-08-12 19:29:07 +02:00
Daniel Eklöf
315044d342
Merge branch 'river-per-output' 2021-08-09 19:56:57 +02:00
Daniel Eklöf
ba7b9e6244
module/river: add ‘per-output’ attribute
When set, river tags and seats’ view titles apply to the output yambar
is on, only.

The default is disabled, which implements the old behavior, where
river tags and seats’ view titles represent the union of all
outputs.
2021-08-09 19:56:48 +02:00
Daniel Eklöf
1c6c73928b
config-verify: add conf_verify_bool() 2021-08-09 19:56:48 +02:00
Daniel Eklöf
74016d7d33
bar: add bar->output_name(), returns the name of the output we’re on 2021-08-09 19:56:47 +02:00
Daniel Eklöf
96a35e5304
Merge branch 'progress-bar-where-tag' 2021-08-09 19:56:21 +02:00
Daniel Eklöf
a210d33320
particle/progress-bar: fix ‘where’ tag regression
After implementing support for more mouse buttons (and scrolling), the
‘where’ tag stopped working in the progress-bar.

This patch repairs it.
2021-08-09 18:21:45 +02:00
Daniel Eklöf
3072c2b13f
Merge branch 'backend-wayland-macro-typo' 2021-07-30 10:32:33 +02:00
Daniel Eklöf
64df5d9806
changelog: --backend=wayland 2021-07-30 10:31:31 +02:00
Érico Nogueira
7d94631991 bar: fix typo.
Running 'yambar --backend=wayland' would always error out saying it was
built without wayland support.
2021-07-29 15:57:00 -03:00
Daniel Eklöf
741107d31c
Merge branch 'border-left-right-top-bottom'
Closes #77
2021-07-28 11:00:26 +02:00
Daniel Eklöf
b97ba80aea
bar: add border.{left,right,top,bottom}-width
This allows you to configure the width of each side of the border
individually. border.width can still be used, and will set all four
borders to the same width.

Closes #77
2021-07-28 11:00:04 +02:00
Daniel Eklöf
8c095eb423
changelog: update contributors list 2021-07-28 10:44:39 +02:00
Daniel Eklöf
30f2880be7
Merge branch 'doc-typo-missing-value' 2021-07-28 10:43:41 +02:00
Rafael Escobar
b4ce851b4d doc: fix typo and missing values 2021-07-27 21:20:29 -03:00
Daniel Eklöf
239ce16a79
Merge branch 'persistent-i3-workspaces'
Closes #72
2021-07-27 15:45:44 +02:00
Daniel Eklöf
7da13a26d0
module/i3: add ‘persistent’ attribute
Add ‘persistent’, a list-of-strings specifying workspace names that
should be persistent. That is, workspaces that should never be
removed, even if empty.

Note that the workspaces _are_ still destroyed (in i3/Sway), but
yambar keeps abstractions for them around. This is useful to e.g. keep
a strict order between your “core” workspaces.

Closes #72
2021-07-26 21:41:59 +02:00
Daniel Eklöf
21adc40a52
changelog: add new ‘unreleased’ section 2021-07-24 13:43:13 +02:00
Daniel Eklöf
b1932872ce
Merge branch 'releases/1.6' 2021-07-24 13:42:51 +02:00
129 changed files with 14805 additions and 3266 deletions

View file

@ -19,13 +19,17 @@ packages:
- json-c-dev
- libmpdclient-dev
- alsa-lib-dev
- pulseaudio-dev
- pipewire-dev
- ttf-dejavu
- gcovr
- python3
- py3-pip
- flex
- bison
sources:
- https://codeberg.org/dnkl/yambar
- https://git.sr.ht/~dnkl/yambar
# triggers:
# - action: email
@ -33,10 +37,10 @@ sources:
# to: <comitter>
tasks:
- codespell: |
pip install codespell
cd yambar
~/.local/bin/codespell README.md CHANGELOG.md *.c *.h doc/*.scd
- fcft: |
cd yambar/subprojects
git clone https://codeberg.org/dnkl/fcft.git
cd ../..
- setup: |
mkdir -p bld/debug bld/release bld/x11-only bld/wayland-only bld/plugs-are-shared
meson --buildtype=debug -Db_coverage=true yambar bld/debug

24
.clang-format Normal file
View file

@ -0,0 +1,24 @@
---
BasedOnStyle: GNU
IndentWidth: 4
---
Language: Cpp
Standard: Auto
PointerAlignment: Right
ColumnLimit: 120
BreakBeforeBraces: Custom
BraceWrapping:
AfterEnum: false
AfterClass: false
SplitEmptyFunction: true
AfterFunction: true
AfterStruct: false
SpaceBeforeParens: ControlStatements
Cpp11BracedListStyle: true
WhitespaceSensitiveMacros:
- REGISTER_CORE_PARTICLE
- REGISTER_CORE_DECORATION
- REGISTER_CORE_PLUGIN
- REGISTER_CORE_MODULE

17
.editorconfig Normal file
View file

@ -0,0 +1,17 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = space
indent_size = 4
max_line_length = 70
[{meson.build,PKGBUILD}]
indent_size = 2
[*.scd]
indent_style = tab
trim_trailing_whitespace = false

View file

@ -1,98 +0,0 @@
image: alpine:latest
stages:
- info
- build
variables:
GIT_SUBMODULE_STRATEGY: normal
before_script:
- apk update
- apk add musl-dev eudev-libs eudev-dev linux-headers meson ninja gcc scdoc
- apk add pixman-dev freetype-dev fontconfig-dev
- apk add libxcb-dev xcb-util-wm-dev xcb-util-cursor-dev yaml-dev
- apk add wayland-dev wayland-protocols wlroots-dev
- apk add json-c-dev libmpdclient-dev alsa-lib-dev
- apk add ttf-dejavu
- apk add git
versions:
stage: info
script:
- meson --version
- ninja --version
- cc --version
debug:
stage: build
script:
- apk add gcovr
- mkdir -p bld/debug
- cd bld/debug
- meson --buildtype=debug -Db_coverage=true ../..
- ninja -k0
- meson test --print-errorlogs
- ninja coverage-html
- mv meson-logs/coveragereport ../../coverage
- ninja coverage-text
- tail -2 meson-logs/coverage.txt
artifacts:
paths:
- coverage
coverage: '/^TOTAL.*\s+(\d+\%)$/'
# valgrind:
# stage: build
# script:
# - apk add valgrind
# - mkdir -p bld/debug
# - cd bld/debug
# - meson --buildtype=debug ../..
# - ninja -k0
# - meson test --verbose --wrapper "valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=3"
release:
stage: build
script:
- mkdir -p bld/release
- cd bld/release
- meson --buildtype=minsize ../../
- ninja -k0
- meson test --print-errorlogs
x11_only:
stage: build
script:
- mkdir -p bld/debug
- cd bld/debug
- meson --buildtype=debug -Dbackend-x11=enabled -Dbackend-wayland=disabled ../../
- ninja -k0
- meson test --print-errorlogs
wayland_only:
stage: build
script:
- mkdir -p bld/debug
- cd bld/debug
- meson --buildtype=debug -Dbackend-x11=disabled -Dbackend-wayland=enabled ../../
- ninja -k0
- meson test --print-errorlogs
plugins_as_shared_modules:
stage: build
script:
- mkdir -p bld/debug
- cd bld/debug
- meson --buildtype=debug -Dcore-plugins-as-shared-libraries=true ../../
- ninja -k0
- meson test --print-errorlogs
codespell:
image: alpine:latest
stage: build
script:
- apk add python3
- apk add py3-pip
- pip install codespell
- codespell README.md CHANGELOG.md *.c *.h doc/*.scd

132
.woodpecker.yaml Normal file
View file

@ -0,0 +1,132 @@
steps:
- name: codespell
when:
- event: [manual, pull_request]
- event: [push, tag]
branch: [master, releases/*]
image: alpine:latest
commands:
- apk add openssl
- apk add python3
- apk add py3-pip
- python3 -m venv codespell-venv
- source codespell-venv/bin/activate
- pip install codespell
- codespell README.md CHANGELOG.md *.c *.h doc/*.scd bar decorations modules particles examples
- deactivate
- name: subprojects
when:
- event: [manual, pull_request]
- event: [push, tag]
branch: [master, releases/*]
image: alpine:latest
commands:
- apk add git
- mkdir -p subprojects && cd subprojects
- git clone https://codeberg.org/dnkl/tllist.git
- git clone https://codeberg.org/dnkl/fcft.git
- cd ..
- name: x64
when:
- event: [manual, pull_request]
- event: [push, tag]
branch: [master, releases/*]
depends_on: [subprojects]
image: alpine:latest
commands:
- apk update
- apk add musl-dev eudev-libs eudev-dev linux-headers meson ninja gcc scdoc
- apk add pixman-dev freetype-dev fontconfig-dev
- apk add libxcb-dev xcb-util-wm-dev xcb-util-cursor-dev yaml-dev
- apk add wayland-dev wayland-protocols wlroots-dev
- apk add json-c-dev libmpdclient-dev alsa-lib-dev pulseaudio-dev pipewire-dev
- apk add ttf-dejavu
- apk add git
- apk add flex bison
# Debug
- apk add gcovr
- mkdir -p bld/debug-x64
- cd bld/debug-x64
- meson --buildtype=debug -Db_coverage=true ../..
- ninja -k0
- meson test --print-errorlogs
- ninja coverage-html
- mv meson-logs/coveragereport ../../coverage
- ninja coverage-text
- tail -2 meson-logs/coverage.txt
- ./yambar --version
- cd ../..
# Release
- mkdir -p bld/release-x64
- cd bld/release-x64
- meson --buildtype=minsize ../../
- ninja -k0
- meson test --print-errorlogs
- ./yambar --version
- cd ../..
# X11 only
- mkdir -p bld/x11-only
- cd bld/x11-only
- meson --buildtype=debug -Dbackend-x11=enabled -Dbackend-wayland=disabled ../../
- ninja -k0
- meson test --print-errorlogs
- ./yambar --version
- cd ../..
# Wayland only
- mkdir -p bld/wayland-only
- cd bld/wayland-only
- meson --buildtype=debug -Dbackend-x11=disabled -Dbackend-wayland=enabled ../../
- ninja -k0
- meson test --print-errorlogs
- ./yambar --version
- cd ../..
- name: x86
when:
- event: [manual, pull_request]
- event: [push, tag]
branch: [master, releases/*]
depends_on: [subprojects]
image: i386/alpine:latest
commands:
- apk add musl-dev eudev-libs eudev-dev linux-headers meson ninja gcc scdoc
- apk add pixman-dev freetype-dev fontconfig-dev
- apk add libxcb-dev xcb-util-wm-dev xcb-util-cursor-dev yaml-dev
- apk add wayland-dev wayland-protocols wlroots-dev
- apk add json-c-dev libmpdclient-dev alsa-lib-dev pulseaudio-dev pipewire-dev
- apk add ttf-dejavu
- apk add git
- apk add flex bison
# Debug
- mkdir -p bld/debug-x86
- cd bld/debug-x86
- meson --buildtype=debug ../../
- ninja -k0
- meson test --print-errorlogs
- ./yambar --version
- cd ../..
# Release
- mkdir -p bld/release-x86
- cd bld/release-x86
- meson --buildtype=minsize ../../
- ninja -k0
- meson test --print-errorlogs
- ./yambar --version
- cd ../..
# Plugins as shared modules
- mkdir -p bld/shared-modules
- cd bld/shared-modules
- meson --buildtype=debug -Dcore-plugins-as-shared-libraries=true ../../
- ninja -k0
- meson test --print-errorlogs
- ./yambar --version
- cd ../..

View file

@ -1,50 +1,535 @@
# Changelog
* [Unreleased](#unreleased)
* [1.11.0](#1-11-0)
* [1.10.0](#1-10-0)
* [1.9.0](#1-9-0)
* [1.8.0](#1-8-0)
* [1.7.0](#1-7-0)
* [1.6.2](#1-6-2)
* [1.6.1](#1-6-1)
* [1.6.0](#1-6-0)
* [1.5.0](#1-5-0)
## Unreleased
### Added
* environment variable substitution in config files ([#96][96]).
* Log output now respects the [`NO_COLOR`](http://no-color.org/)
environment variable.
* network: `type` tag ([#380][380]).
* network: `type` and `kind` tags ([#380][380]).
* tags: `/<N>` tag formatter: divides the tag's decimal value with `N`
([#392][392]).
* i3/sway: `output` tag, reflecting the output (monitor) a workspace
is on.
* Added "string like" `~~` operator to Map particle. Allows glob-style
matching on strings using `*` and `?` characters ([#400][400]).
* Added "single" mode flag to the `mpd` module ([#428][428]).
* niri: add a new module for niri-workspaces and niri-language
([#404][404]).
* pipewire: added `spacing`, `left-spacing` and `right-spacing`
attributes.
* mpris: new module ([#53][53]).
[96]: https://codeberg.org/dnkl/yambar/issues/96
[380]: https://codeberg.org/dnkl/yambar/issues/380
[392]: https://codeberg.org/dnkl/yambar/issues/392
[400]: https://codeberg.org/dnkl/yambar/pulls/400
[428]: https://codeberg.org/dnkl/yambar/pulls/428
[404]: https://codeberg.org/dnkl/yambar/issues/404
[53]: https://codeberg.org/dnkl/yambar/issues/53
### Changed
* `river`: expand to an empty list of particles when river is not
running ([#384][384]).
[384]: https://codeberg.org/dnkl/yambar/issues/384
### Deprecated
### Removed
### Fixed
* network: fix missing break in switch statement ([#377][377]).
* i3/sway: crash when output is turned off an on ([#300][300]).
* mpd: yambar never attempting to reconnect after MPD closed the
connection (for example, when MPD is restarted).
* Bar positioning on multi-monitor setups, when `location=bottom`.
* pipewire: Improve handling of node switching ([#424][424]).
[377]: https://codeberg.org/dnkl/yambar/issues/377
[300]: https://codeberg.org/dnkl/yambar/issues/300
[424]: https://codeberg.org/dnkl/yambar/pulls/424
### Security
### Contributors
## 1.11.0
### Added
* battery: current smoothing, for improved discharge estimates.
* battery: scale option, for batteries that report 'charge' at a
different scale than 'current'.
* network: new `quality` tag (Wi-Fi only).
* Read alternative config from pipes and FIFOs (e.g. `--config
/dev/stdin`) ([#340][340]).
* Added `overlay` and `background` as possible `layer` values
([#372][372]).
[340]: https://codeberg.org/dnkl/yambar/pulls/340
[372]: https://codeberg.org/dnkl/yambar/issues/372
### Changed
* log-level: default to `warning`
* network: use dynlist instead of fixed name ([#355][355])
[355]: https://codeberg.org/dnkl/yambar/pulls/355
### Fixed
* Compiler error _fmt may be used uninitialized_ ([#311][311]).
* map: conditions failing to match when they contain multiple, quoted
tag values ([#302][302]).
* Crash when hidden by an opaque window.
* Bar not resizing itself when the screen resolution is changed
([#330][330]).
* i3/sway: incorrect empty/title state of workspaces ([#343][343]).
* mem: state updated on each bar redraw ([#352][352]).
* script: buffer overflow when reading large amounts of data.
* i3/sway: module fails when reloading config file ([#361][361]).
* Worked around bug in gcc causing a compilation error ([#350][350]).
* Miscalculation of list width in presence of empty particles ([#369][369]).
* Log-level not respected by syslog.
[311]: https://codeberg.org/dnkl/yambar/issues/311
[302]: https://codeberg.org/dnkl/yambar/issues/302
[330]: https://codeberg.org/dnkl/yambar/issues/330
[343]: https://codeberg.org/dnkl/yambar/issues/343
[352]: https://codeberg.org/dnkl/yambar/issues/352
[361]: https://codeberg.org/dnkl/yambar/issues/361
[350]: https://codeberg.org/dnkl/yambar/issues/350
[369]: https://codeberg.org/dnkl/yambar/issues/369
### Contributors
* Delgan
* Haden Collins
* Jordan Isaacs
* kotyk
* Leonardo Hernández Hernández
* oob
* rdbo
* Sertonix
* steovd
* Väinö Mäkelä
* Yiyu Zhou
## 1.10.0
### Added
* Field width tag format option ([#246][246])
* river: support for layout events.
* dwl: support for specifying name of tags ([#256][256])
* i3/sway: extend option `sort`; use `native` to sort numbered workspaces only.
* modules/dwl: handle the appid status ([#284][284])
* battery: also show estimation for time to full ([#303][303]).
* on-click: tilde expansion ([#307][307])
* script: tilde expansion of `path` ([#307][307]).
[246]: https://codeberg.org/dnkl/yambar/issues/246
[256]: https://codeberg.org/dnkl/yambar/pulls/256
[284]: https://codeberg.org/dnkl/yambar/pulls/284
[307]: https://codeberg.org/dnkl/yambar/issues/307
### Changed
* disk-io: `interval` renamed to `poll-interval`
* mem: `interval` renamed to `poll-interval`
* battery/network/script: `poll-interval` unit changed from seconds to
milliseconds ([#244][244]).
* all modules: minimum poll interval changed from 500ms to 250ms.
* network: do not use IPv6 link-local ([#281][281])
[244]: https://codeberg.org/dnkl/yambar/issues/244
[281]: https://codeberg.org/dnkl/yambar/pulls/281
### Fixed
* Build failures for certain combinations of enabled and disabled
plugins ([#239][239]).
* Documentation for the `cpu` module; `interval` has been renamed to
`poll-interval` ([#241][241]).
* battery: module was not thread safe.
* dwl module reporting only the last part of the title ([#251][251])
* i3/sway: regression; persistent workspaces shown twice
([#253][253]).
* pipewire: use `roundf()` instead of `ceilf()` for more accuracy
([#262][262])
* Crash when a yaml anchor has a value that already exists in the
target yaml node ([#286][286]).
* battery: Fix time conversion in battery estimation ([#303][303]).
* battery: poll timeout being reset when receiving irrelevant udev
notification (leading to battery status never updating, in worst
case) ([#305][305]).
[239]: https://codeberg.org/dnkl/yambar/issues/239
[241]: https://codeberg.org/dnkl/yambar/issues/241
[251]: https://codeberg.org/dnkl/yambar/pulls/251
[253]: https://codeberg.org/dnkl/yambar/issues/253
[262]: https://codeberg.org/dnkl/yambar/issues/262
[286]: https://codeberg.org/dnkl/yambar/issues/286
[305]: https://codeberg.org/dnkl/yambar/issues/305
### Contributors
* Leonardo Gibrowski Faé (Horus)
* Armin Fisslthaler
* Ben Brown
* David Bimmler
* Leonardo Hernández Hernández
* Ogromny
* Oleg Hahm
* Stanislav Ochotnický
* tiosgz
* Yutaro Ohno
## 1.9.0
### Added
* Support for specifying number of decimals when printing a float tag
([#200][200]).
* Support for custom font fallbacks ([#153][153]).
* overline: new decoration ([#153][153]).
* i3/sway: boolean option `strip-workspace-numbers`.
* font-shaping: new inheritable configuration option, allowing you to
configure whether strings should be _shaped_ using HarfBuzz, or not
([#159][159]).
* river: support for the new “mode” event present in version 3 of the
river status manager protocol, in the form of a new tag, _”mode”_,
in the `title` particle.
* network: request link stats and expose under tags `dl-speed` and
`ul-speed` when `poll-interval` is set.
* new module: disk-io.
* new module: pulse ([#223][223]).
* alsa: `dB` tag ([#202][202]).
* mpd: `file` tag ([#219][219]).
* pipewire: add a new module for pipewire ([#224][224])
* on-click: support `next`/`previous` mouse buttons ([#228][228]).
* dwl: add a new module for DWL ([#218][218])
* sway: support for workspace rename and move events
([#216][216]).
[153]: https://codeberg.org/dnkl/yambar/issues/153
[159]: https://codeberg.org/dnkl/yambar/issues/159
[200]: https://codeberg.org/dnkl/yambar/issues/200
[202]: https://codeberg.org/dnkl/yambar/issues/202
[218]: https://codeberg.org/dnkl/yambar/pulls/218
[219]: https://codeberg.org/dnkl/yambar/pulls/219
[223]: https://codeberg.org/dnkl/yambar/pulls/223
[224]: https://codeberg.org/dnkl/yambar/pulls/224
[228]: https://codeberg.org/dnkl/yambar/pulls/228
[216]: https://codeberg.org/dnkl/yambar/issues/216
### Changed
* All modules are now compile-time optional.
* Minimum required meson version is now 0.59.
* Float tags are now treated as floats instead of integers when
formatted with the `kb`/`kib`/`mb`/`mib`/`gb`/`gib` string particle
formatters.
* network: `tx-bitrate` and `rx-bitrate` are now in bits/s instead of
Mb/s. Use the `mb` string formatter to render these tags as before
(e.g. `string: {text: "{tx-bitrate:mb}"}`).
* i3: newly created, and **unfocused** workspaces are now considered
non-empty ([#191][191])
* alsa: use dB instead of raw volume values, if possible, when
calculating the `percent` tag ([#202][202])
* cpu: `content` particle is now a template instantiated once for each
core, and once for the total CPU usage. See
**yambar-modules-cpu**(5) for more information ([#207][207]).
* **BREAKING CHANGE**: overhaul of the `map` particle. Instead of
specifying a `tag` and then an array of `values`, you must now
simply use an array of `conditions`, that consist of:
`<tag> <operation> <value>`
where `<operation>` is one of:
`== != < <= > >=`
Note that boolean tags must be used as is:
`online`
`~online # use '~' to match for their falsehood`
As an example, if you previously had something like:
```
map:
tag: State
values:
unrecognized:
...
```
You would now write it as:
```
map:
conditions:
State == unrecognized:
...
```
Note that if `<value>` contains any non-alphanumerical characters,
it **must** be surrounded by `""`:
`State == "very confused!!!"`
Finally, you can mix and match conditions using the boolean
operators `&&` and `||`:
```
<condition1> && <condition2>
<condition1> && (<condition2> || <condition3>) # parenthesis work
~(<condition1> && <condition2>) # '~' can be applied to any condition
```
For a more thorough explanation, see the updated map section in the
man page for yambar-particles([#137][137], [#175][175] and [#][182]).
[137]: https://codeberg.org/dnkl/yambar/issues/137
[175]: https://codeberg.org/dnkl/yambar/issues/172
[182]: https://codeberg.org/dnkl/yambar/issues/182
[191]: https://codeberg.org/dnkl/yambar/issues/191
[202]: https://codeberg.org/dnkl/yambar/issues/202
[207]: https://codeberg.org/dnkl/yambar/issues/207
### Fixed
* i3: fixed “missing workspace indicator” (_err: modules/i3.c:94:
workspace reply/event without 'name' and/or 'output', and/or 'focus'
properties_).
* Slow/laggy behavior when quickly spawning many `on-click` handlers,
e.g. when handling mouse wheel events ([#169][169]).
* cpu: dont error out on systems where SMT has been disabled
([#172][172]).
* examples/dwl-tags: updated parsing of `output` name ([#178][178]).
* sway-xkb: dont crash when Sway sends an _”added”_ event for a
device yambar is already tracking ([#177][177]).
* Crash when a particle is “too wide”, and tries to render outside the
bar ([#198][198]).
* string: crash when failing to convert string to UTF-32.
* script: only first transaction processed when receiving multiple
transactions in a single batch ([#221][221]).
* network: missing SSID (recent kernels, or possibly wireless drivers,
no longer provide the SSID in the `NL80211_CMD_NEW_STATION`
response) ([#226][226]).
* sway-xkb: crash when compositor presents multiple inputs with
identical IDs ([#229][229]).
[169]: https://codeberg.org/dnkl/yambar/issues/169
[172]: https://codeberg.org/dnkl/yambar/issues/172
[178]: https://codeberg.org/dnkl/yambar/issues/178
[177]: https://codeberg.org/dnkl/yambar/issues/177
[198]: https://codeberg.org/dnkl/yambar/issues/198
[221]: https://codeberg.org/dnkl/yambar/issues/221
[226]: https://codeberg.org/dnkl/yambar/issues/226
[229]: https://codeberg.org/dnkl/yambar/issues/229
### Contributors
* Baptiste Daroussin
* Horus
* Johannes
* Leonardo Gibrowski Faé
* Leonardo Neumann
* Midgard
* Ogromny
* Peter Rice
* Timur Celik
* Willem van de Krol
* hiog
## 1.8.0
### Added
* ramp: can now have custom min and max values
([#103](https://codeberg.org/dnkl/yambar/issues/103)).
* border: new decoration.
* i3/sway: new boolean tag: `empty`
([#139](https://codeberg.org/dnkl/yambar/issues/139)).
* mem: a module handling system memory monitoring
* cpu: a module offering cpu usage monitoring
* removables: support for audio CDs
([#146](https://codeberg.org/dnkl/yambar/issues/146)).
* removables: new boolean tag: `audio`.
### Changed
* fcft >= 3.0 is now required.
* Made `libmpdclient` an optional dependency
* battery: unknown battery states are now mapped to unknown, instead
of discharging.
* Wayland: the bar no longer exits when the monitor is
disabled/unplugged ([#106](https://codeberg.org/dnkl/yambar/issues/106)).
### Fixed
* `left-margin` and `right-margin` from being rejected as invalid
options.
* Crash when `udev_monitor_receive_device()` returned `NULL`. This
affected the “backlight”, “battery” and “removables” modules
([#109](https://codeberg.org/dnkl/yambar/issues/109)).
* foreign-toplevel: update bar when a top-level is closed.
* Bar not being mapped on an output before at least one module has
“refreshed” it ([#116](https://codeberg.org/dnkl/yambar/issues/116)).
* network: failure to retrieve wireless attributes (SSID, RX/TX
bitrate, signal strength etc).
* Integer options that were supposed to be >= 0 were incorrectly
allowed, leading to various bad things; including yambar crashing,
or worse, the compositor crashing
([#129](https://codeberg.org/dnkl/yambar/issues/129)).
* kib/kb, mib/mb and gib/gb formatters were inverted.
### Contributors
* [sochotnicky](https://codeberg.org/sochotnicky)
* Alexandre Acebedo
* anb
* Baptiste Daroussin
* Catterwocky
* horus645
* Jan Beich
* mz
* natemaia
* nogerine
* Soc Virnyl S. Estela
* Vincent Fischer
## 1.7.0
### Added
* i3: `persistent` attribute, allowing persistent workspaces
([#72](https://codeberg.org/dnkl/yambar/issues/72)).
* bar: `border.{left,right,top,bottom}-width`, allowing the width of
each side of the border to be configured
individually. `border.width` is now a short-hand for setting all
four borders to the same value
([#77](https://codeberg.org/dnkl/yambar/issues/77)).
* bar: `layer: top|bottom`, allowing the layer which the bar is
rendered on to be changed. Wayland only - ignored on X11.
* river: `all-monitors: false|true`.
* `-d,--log-level=info|warning|error|none` command line option
([#84](https://codeberg.org/dnkl/yambar/issues/84)).
* river: support for the river-status protocol, version 2 (urgent
views).
* `online` tag to the `alsa` module.
* alsa: `volume` and `muted` options, allowing you to configure which
channels to use as source for the volume level and muted state.
* foreign-toplevel: Wayland module that provides information about
currently opened windows.
* alsa: support for capture devices.
* network: `ssid`, `signal`, `rx-bitrate` and `rx-bitrate` tags.
* network: `poll-interval` option (for the new `signal` and
`*-bitrate` tags).
* tags: percentage tag formatter, for range tags: `{tag_name:%}`.
* tags: kb/mb/gb, and kib/mib/gib tag formatters.
* clock: add a config option to show UTC time.
### Changed
* bar: do not add `spacing` around empty (zero-width) modules.
* alsa: do not error out if we fail to connect to the ALSA device, or
if we get disconnected. Instead, keep retrying until we succeed
([#86](https://codeberg.org/dnkl/yambar/issues/86)).
### Fixed
* `yambar --backend=wayland` always erroring out with _”yambar was
compiled without the Wayland backend”_.
* Regression: `{where}` tag not being expanded in progress-bar
`on-click` handlers.
* `alsa` module causing yambar to use 100% CPU if the ALSA device is
disconnected ([#61](https://codeberg.org/dnkl/yambar/issues/61)).
### Contributors
* [paemuri](https://codeberg.org/paemuri)
* [ericonr](https://codeberg.org/ericonr)
* [Nulo](https://nulo.in)
## 1.6.2
### Added
* Text shaping support.
* Support for middle and right mouse buttons, mouse wheel and trackpad
scrolling (https://codeberg.org/dnkl/yambar/issues/39).
scrolling ([#39](https://codeberg.org/dnkl/yambar/issues/39)).
* script: polling mode. See the new `poll-interval` option
(https://codeberg.org/dnkl/yambar/issues/67).
([#67](https://codeberg.org/dnkl/yambar/issues/67)).
### Changed
* doc: split up **yambar-modules**(5) into multiple man pages, one for
each module (https://codeberg.org/dnkl/yambar/issues/15).
each module ([#15](https://codeberg.org/dnkl/yambar/issues/15)).
* fcft >= 2.4.0 is now required.
* sway-xkb: non-keyboard inputs are now ignored
(https://codeberg.org/dnkl/yambar/issues/51).
([#51](https://codeberg.org/dnkl/yambar/issues/51)).
* battery: dont terminate (causing last status to “freeze”) when
failing to update; retry again later
(https://codeberg.org/dnkl/yambar/issues/44).
([#44](https://codeberg.org/dnkl/yambar/issues/44)).
* battery: differentiate "Not Charging" and "Discharging" in state
tag of battery module.
(https://codeberg.org/dnkl/yambar/issues/57).
([#57](https://codeberg.org/dnkl/yambar/issues/57)).
* string: use HORIZONTAL ELLIPSIS instead of three regular periods
when truncating a string
(https://codeberg.org/dnkl/yambar/issues/73).
([#73](https://codeberg.org/dnkl/yambar/issues/73)).
### Fixed
* Crash when merging non-dictionary anchors in the YAML configuration
(https://codeberg.org/dnkl/yambar/issues/32).
([#32](https://codeberg.org/dnkl/yambar/issues/32)).
* Crash in the `ramp` particle when the tags value was out-of-bounds
(https://codeberg.org/dnkl/yambar/issues/45).
([#45](https://codeberg.org/dnkl/yambar/issues/45)).
* Crash when a string particle contained `{}`
(https://codeberg.org/dnkl/yambar/issues/48).
([#48](https://codeberg.org/dnkl/yambar/issues/48)).
* `script` module rejecting range tag end values containing the digit
`9` (https://codeberg.org/dnkl/yambar/issues/60).
`9` ([#60](https://codeberg.org/dnkl/yambar/issues/60)).
### Contributors
@ -59,7 +544,7 @@
* i3: workspaces with numerical names are sorted separately from
non-numerically named workspaces
(https://codeberg.org/dnkl/yambar/issues/30).
([#30](https://codeberg.org/dnkl/yambar/issues/30)).
### Fixed
@ -67,7 +552,7 @@
* mpd: `elapsed` tag not working (regression, introduced in 1.6.0).
* Wrong background color for (semi-) transparent backgrounds.
* battery: stats sometimes getting stuck at 0, or impossibly large
values (https://codeberg.org/dnkl/yambar/issues/25).
values ([#25](https://codeberg.org/dnkl/yambar/issues/25)).
## 1.6.0
@ -76,17 +561,17 @@
* alsa: `percent` tag. This is an integer tag that represents the
current volume as a percentage value
(https://codeberg.org/dnkl/yambar/issues/10).
([#10](https://codeberg.org/dnkl/yambar/issues/10)).
* river: added documentation
(https://codeberg.org/dnkl/yambar/issues/9).
([#9](https://codeberg.org/dnkl/yambar/issues/9)).
* script: new module, adds support for custom user scripts
(https://codeberg.org/dnkl/yambar/issues/11).
([#11](https://codeberg.org/dnkl/yambar/issues/11)).
* mpd: `volume` tag. This is a range tag that represents MPD's current
volume in percentage (0-100)
* i3: `sort` configuration option, that controls how the workspace
list is sorted. Can be set to one of `none`, `ascending` or
`descending`. Default is `none`
(https://codeberg.org/dnkl/yambar/issues/17).
([#17](https://codeberg.org/dnkl/yambar/issues/17)).
* i3: `mode` tag: the name of the currently active mode
@ -96,12 +581,12 @@
error”_.
* Memory leak when a YAML parsing error was encountered.
* clock: update every second when necessary
(https://codeberg.org/dnkl/yambar/issues/12).
([#12](https://codeberg.org/dnkl/yambar/issues/12)).
* mpd: fix compilation with clang
(https://codeberg.org/dnkl/yambar/issues/16).
([#16](https://codeberg.org/dnkl/yambar/issues/16)).
* Crash when the alpha component in a color value was 0.
* XCB: Fallback to non-primary monitor when the primary monitor is
disconnected (https://codeberg.org/dnkl/yambar/issues/20)
disconnected ([#20](https://codeberg.org/dnkl/yambar/issues/20))
### Contributors

View file

@ -1,7 +1,8 @@
pkgname=yambar
pkgver=1.6.2
pkgver=1.11.0
pkgrel=1
pkgdesc="Simplistic and highly configurable status panel for X and Wayland"
changelog=CHANGELOG.md
arch=('x86_64' 'aarch64')
url=https://codeberg.org/dnkl/yambar
license=(mit)
@ -15,7 +16,9 @@ depends=(
'libudev.so'
'json-c'
'libmpdclient'
'fcft>=2.4.0')
'libpulse'
'pipewire'
'fcft>=3.0.0' 'fcft<4.0.0')
optdepends=('xcb-util-errors: better X error messages')
source=()

View file

@ -1,5 +1,5 @@
pkgname=yambar-wayland
pkgver=1.6.2
pkgver=1.11.0
pkgrel=1
pkgdesc="Simplistic and highly configurable status panel for Wayland"
arch=('x86_64' 'aarch64')
@ -16,8 +16,11 @@ depends=(
'libudev.so'
'json-c'
'libmpdclient'
'fcft>=2.4.0')
'libpulse'
'pipewire'
'fcft>=3.0.0' 'fcft<4.0.0')
source=()
changelog=CHANGELOG.md
pkgver() {
cd ../.git &> /dev/null && git describe --tags --long | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' ||

View file

@ -1,6 +1,8 @@
[![CI status](https://ci.codeberg.org/api/badges/dnkl/yambar/status.svg)](https://ci.codeberg.org/dnkl/yambar)
# Yambar
[![Packaging status](https://repology.org/badge/vertical-allrepos/yambar.svg)](https://repology.org/project/yambar/versions)
[![Packaging status](https://repology.org/badge/vertical-allrepos/yambar.svg?columns=4)](https://repology.org/project/yambar/versions)
## Index
@ -57,9 +59,9 @@ bar:
right:
- clock:
content:
- string: {text: , font: "Font Awesome 5 Free:style=solid:size=12"}
- string: {text: , font: "Font Awesome 6 Free:style=solid:size=12"}
- string: {text: "{date}", right-margin: 5}
- string: {text: , font: "Font Awesome 5 Free:style=solid:size=12"}
- string: {text: , font: "Font Awesome 6 Free:style=solid:size=12"}
- string: {text: "{time}"}
```
@ -76,10 +78,17 @@ Available modules:
* backlight
* battery
* clock
* cpu
* disk-io
* dwl
* foreign-toplevel
* i3 (and Sway)
* label
* mem
* mpd
* network
* pipewire
* pulse
* removables
* river
* script (see script [examples](examples/scripts))
@ -98,7 +107,7 @@ mkdir -p bld/release && cd bld/release
Second, configure the build (if you intend to install it globally, you
might also want `--prefix=/usr`):
```sh
meson --buildtype=release ../..
meson setup --buildtype=release ../..
```
Optionally, explicitly disable a backend (or enable, if you want a

View file

@ -7,11 +7,10 @@
struct backend {
bool (*setup)(struct bar *bar);
void (*cleanup)(struct bar *bar);
void (*loop)(struct bar *bar,
void (*expose)(const struct bar *bar),
void (*on_mouse)(struct bar *bar, enum mouse_event event,
enum mouse_button btn, int x, int y));
void (*loop)(struct bar *bar, void (*expose)(const struct bar *bar),
void (*on_mouse)(struct bar *bar, enum mouse_event event, enum mouse_button btn, int x, int y));
void (*commit)(const struct bar *bar);
void (*refresh)(const struct bar *bar);
void (*set_cursor)(struct bar *bar, const char *cursor);
const char *(*output_name)(const struct bar *bar);
};

175
bar/bar.c
View file

@ -1,15 +1,15 @@
#include "bar.h"
#include "private.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <threads.h>
#include <assert.h>
#include <unistd.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <threads.h>
#include <unistd.h>
#include <sys/eventfd.h>
@ -18,15 +18,17 @@
#include "../log.h"
#if defined(ENABLE_X11)
#include "xcb.h"
#include "xcb.h"
#endif
#if defined(ENABLE_WAYLAND)
#include "wayland.h"
#include "wayland.h"
#endif
#define max(x, y) ((x) > (y) ? (x) : (y))
/*
* Calculate total width of left/center/rigth groups.
* Calculate total width of left/center/right groups.
* Note: begin_expose() must have been called
*/
static void
@ -38,23 +40,33 @@ calculate_widths(const struct private *b, int *left, int *center, int *right)
for (size_t i = 0; i < b->left.count; i++) {
struct exposable *e = b->left.exps[i];
*left += b->left_spacing + e->width + b->right_spacing;
if (e->width > 0)
*left += b->left_spacing + e->width + b->right_spacing;
}
for (size_t i = 0; i < b->center.count; i++) {
struct exposable *e = b->center.exps[i];
*center += b->left_spacing + e->width + b->right_spacing;
if (e->width > 0)
*center += b->left_spacing + e->width + b->right_spacing;
}
for (size_t i = 0; i < b->right.count; i++) {
struct exposable *e = b->right.exps[i];
*right += b->left_spacing + e->width + b->right_spacing;
if (e->width > 0)
*right += b->left_spacing + e->width + b->right_spacing;
}
/* No spacing on the edges (that's what the margins are for) */
*left -= b->left_spacing + b->right_spacing;
*center -= b->left_spacing + b->right_spacing;
*right -= b->left_spacing + b->right_spacing;
if (*left > 0)
*left -= b->left_spacing + b->right_spacing;
if (*center > 0)
*center -= b->left_spacing + b->right_spacing;
if (*right > 0)
*right -= b->left_spacing + b->right_spacing;
assert(*left >= 0);
assert(*center >= 0);
assert(*right >= 0);
}
static void
@ -63,20 +75,26 @@ expose(const struct bar *_bar)
const struct private *bar = _bar->private;
pixman_image_t *pix = bar->pix;
pixman_image_fill_rectangles(
PIXMAN_OP_SRC, pix, &bar->background, 1,
&(pixman_rectangle16_t){0, 0, bar->width, bar->height_with_border});
pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &bar->background, 1,
&(pixman_rectangle16_t){0, 0, bar->width, bar->height_with_border});
if (bar->border.width > 0) {
pixman_image_fill_rectangles(
PIXMAN_OP_OVER, pix, &bar->border.color, 4,
(pixman_rectangle16_t[]){
{0, 0, bar->width, bar->border.width},
{0, 0, bar->border.width, bar->height_with_border},
{bar->width - bar->border.width, 0, bar->border.width, bar->height_with_border},
{0, bar->height_with_border - bar->border.width, bar->width, bar->border.width},
});
}
pixman_image_fill_rectangles(
PIXMAN_OP_OVER, pix, &bar->border.color, 4,
(pixman_rectangle16_t[]){
/* Left */
{0, 0, bar->border.left_width, bar->height_with_border},
/* Right */
{bar->width - bar->border.right_width, 0, bar->border.right_width, bar->height_with_border},
/* Top */
{bar->border.left_width, 0, bar->width - bar->border.left_width - bar->border.right_width,
bar->border.top_width},
/* Bottom */
{bar->border.left_width, bar->height_with_border - bar->border.bottom_width,
bar->width - bar->border.left_width - bar->border.right_width, bar->border.bottom_width},
});
for (size_t i = 0; i < bar->left.count; i++) {
struct module *m = bar->left.mods[i];
@ -84,6 +102,7 @@ expose(const struct bar *_bar)
if (e != NULL)
e->destroy(e);
bar->left.exps[i] = module_begin_expose(m);
assert(bar->left.exps[i]->width >= 0);
}
for (size_t i = 0; i < bar->center.count; i++) {
@ -92,6 +111,7 @@ expose(const struct bar *_bar)
if (e != NULL)
e->destroy(e);
bar->center.exps[i] = module_begin_expose(m);
assert(bar->center.exps[i]->width >= 0);
}
for (size_t i = 0; i < bar->right.count; i++) {
@ -100,42 +120,49 @@ expose(const struct bar *_bar)
if (e != NULL)
e->destroy(e);
bar->right.exps[i] = module_begin_expose(m);
assert(bar->right.exps[i]->width >= 0);
}
int left_width, center_width, right_width;
calculate_widths(bar, &left_width, &center_width, &right_width);
int y = bar->border.width;
int x = bar->border.width + bar->left_margin - bar->left_spacing;
int y = bar->border.top_width;
int x = bar->border.left_width + bar->left_margin - bar->left_spacing;
pixman_region32_t clip;
pixman_region32_init_rect(
&clip, bar->border.left_width + bar->left_margin, bar->border.top_width,
(bar->width - bar->left_margin - bar->right_margin - bar->border.left_width - bar->border.right_width),
bar->height);
pixman_image_set_clip_region32(pix, &clip);
pixman_region32_fini(&clip);
for (size_t i = 0; i < bar->left.count; i++) {
const struct exposable *e = bar->left.exps[i];
e->expose(e, pix, x + bar->left_spacing, y, bar->height);
x += bar->left_spacing + e->width + bar->right_spacing;
if (e->width > 0)
x += bar->left_spacing + e->width + bar->right_spacing;
}
x = bar->width / 2 - center_width / 2 - bar->left_spacing;
for (size_t i = 0; i < bar->center.count; i++) {
const struct exposable *e = bar->center.exps[i];
e->expose(e, pix, x + bar->left_spacing, y, bar->height);
x += bar->left_spacing + e->width + bar->right_spacing;
if (e->width > 0)
x += bar->left_spacing + e->width + bar->right_spacing;
}
x = bar->width - (
right_width +
bar->left_spacing +
bar->right_margin +
bar->border.width);
x = bar->width - (right_width + bar->left_spacing + bar->right_margin + bar->border.right_width);
for (size_t i = 0; i < bar->right.count; i++) {
const struct exposable *e = bar->right.exps[i];
e->expose(e, pix, x + bar->left_spacing, y, bar->height);
x += bar->left_spacing + e->width + bar->right_spacing;
if (e->width > 0)
x += bar->left_spacing + e->width + bar->right_spacing;
}
bar->backend.iface->commit(_bar);
}
static void
refresh(const struct bar *bar)
{
@ -150,16 +177,20 @@ set_cursor(struct bar *bar, const char *cursor)
b->backend.iface->set_cursor(bar, cursor);
}
static const char *
output_name(const struct bar *bar)
{
const struct private *b = bar->private;
return b->backend.iface->output_name(bar);
}
static void
on_mouse(struct bar *_bar, enum mouse_event event, enum mouse_button btn,
int x, int y)
on_mouse(struct bar *_bar, enum mouse_event event, enum mouse_button btn, int x, int y)
{
struct private *bar = _bar->private;
if ((y < bar->border.width ||
y >= (bar->height_with_border - bar->border.width)) ||
(x < bar->border.width || x >= (bar->width - bar->border.width)))
{
if ((y < bar->border.top_width || y >= (bar->height_with_border - bar->border.bottom_width))
|| (x < bar->border.left_width || x >= (bar->width - bar->border.right_width))) {
set_cursor(_bar, "left_ptr");
return;
}
@ -167,10 +198,13 @@ on_mouse(struct bar *_bar, enum mouse_event event, enum mouse_button btn,
int left_width, center_width, right_width;
calculate_widths(bar, &left_width, &center_width, &right_width);
int mx = bar->border.width + bar->left_margin - bar->left_spacing;
int mx = bar->border.left_width + bar->left_margin - bar->left_spacing;
for (size_t i = 0; i < bar->left.count; i++) {
struct exposable *e = bar->left.exps[i];
if (e->width == 0)
continue;
mx += bar->left_spacing;
if (x >= mx && x < mx + e->width) {
if (e->on_mouse != NULL)
@ -185,6 +219,9 @@ on_mouse(struct bar *_bar, enum mouse_event event, enum mouse_button btn,
for (size_t i = 0; i < bar->center.count; i++) {
struct exposable *e = bar->center.exps[i];
if (e->width == 0)
continue;
mx += bar->left_spacing;
if (x >= mx && x < mx + e->width) {
if (e->on_mouse != NULL)
@ -195,14 +232,14 @@ on_mouse(struct bar *_bar, enum mouse_event event, enum mouse_button btn,
mx += e->width + bar->right_spacing;
}
mx = bar->width - (right_width
+ bar->left_spacing +
bar->right_margin +
bar->border.width);
mx = bar->width - (right_width + bar->left_spacing + bar->right_margin + bar->border.right_width);
for (size_t i = 0; i < bar->right.count; i++) {
struct exposable *e = bar->right.exps[i];
if (e->width == 0)
continue;
mx += bar->left_spacing;
if (x >= mx && x < mx + e->width) {
if (e->on_mouse != NULL)
@ -236,7 +273,7 @@ run(struct bar *_bar)
{
struct private *bar = _bar->private;
bar->height_with_border = bar->height + 2 * bar->border.width;
bar->height_with_border = bar->height + bar->border.top_width + bar->border.bottom_width;
if (!bar->backend.iface->setup(_bar)) {
bar->backend.iface->cleanup(_bar);
@ -246,11 +283,12 @@ run(struct bar *_bar)
}
set_cursor(_bar, "left_ptr");
expose(_bar);
/* Start modules */
thrd_t thrd_left[bar->left.count];
thrd_t thrd_center[bar->center.count];
thrd_t thrd_right[bar->right.count];
thrd_t thrd_left[max(bar->left.count, 1)];
thrd_t thrd_center[max(bar->center.count, 1)];
thrd_t thrd_right[max(bar->right.count, 1)];
for (size_t i = 0; i < bar->left.count; i++) {
struct module *mod = bar->left.mods[i];
@ -285,20 +323,26 @@ run(struct bar *_bar)
int mod_ret;
for (size_t i = 0; i < bar->left.count; i++) {
thrd_join(thrd_left[i], &mod_ret);
if (mod_ret != 0)
LOG_ERR("module: LEFT #%zu: non-zero exit value: %d", i, mod_ret);
if (mod_ret != 0) {
const struct module *m = bar->left.mods[i];
LOG_ERR("module: LEFT #%zu (%s): non-zero exit value: %d", i, m->description(m), mod_ret);
}
ret = ret == 0 && mod_ret != 0 ? mod_ret : ret;
}
for (size_t i = 0; i < bar->center.count; i++) {
thrd_join(thrd_center[i], &mod_ret);
if (mod_ret != 0)
LOG_ERR("module: CENTER #%zu: non-zero exit value: %d", i, mod_ret);
if (mod_ret != 0) {
const struct module *m = bar->center.mods[i];
LOG_ERR("module: CENTER #%zu (%s): non-zero exit value: %d", i, m->description(m), mod_ret);
}
ret = ret == 0 && mod_ret != 0 ? mod_ret : ret;
}
for (size_t i = 0; i < bar->right.count; i++) {
thrd_join(thrd_right[i], &mod_ret);
if (mod_ret != 0)
LOG_ERR("module: RIGHT #%zu: non-zero exit value: %d", i, mod_ret);
if (mod_ret != 0) {
const struct module *m = bar->right.mods[i];
LOG_ERR("module: RIGHT #%zu (%s): non-zero exit value: %d", i, m->description(m), mod_ret);
}
ret = ret == 0 && mod_ret != 0 ? mod_ret : ret;
}
@ -386,7 +430,7 @@ bar_new(const struct bar_config *config)
break;
case BAR_BACKEND_WAYLAND:
#if defined(BAR_WAYLAND)
#if defined(ENABLE_WAYLAND)
backend_data = bar_backend_wayland_new();
backend_iface = &wayland_backend_iface;
#else
@ -401,6 +445,7 @@ bar_new(const struct bar_config *config)
struct private *priv = calloc(1, sizeof(*priv));
priv->monitor = config->monitor != NULL ? strdup(config->monitor) : NULL;
priv->layer = config->layer;
priv->location = config->location;
priv->height = config->height;
priv->background = config->background;
@ -409,7 +454,10 @@ bar_new(const struct bar_config *config)
priv->left_margin = config->left_margin;
priv->right_margin = config->right_margin;
priv->trackpad_sensitivity = config->trackpad_sensitivity;
priv->border.width = config->border.width;
priv->border.left_width = config->border.left_width;
priv->border.right_width = config->border.right_width;
priv->border.top_width = config->border.top_width;
priv->border.bottom_width = config->border.bottom_width;
priv->border.color = config->border.color;
priv->border.left_margin = config->border.left_margin;
priv->border.right_margin = config->border.right_margin;
@ -440,6 +488,7 @@ bar_new(const struct bar_config *config)
bar->destroy = &destroy;
bar->refresh = &refresh;
bar->set_cursor = &set_cursor;
bar->output_name = &output_name;
for (size_t i = 0; i < priv->left.count; i++)
priv->left.mods[i]->bar = bar;

View file

@ -1,6 +1,7 @@
#pragma once
#include "../color.h"
#include "../font-shaping.h"
#include "../module.h"
struct bar {
@ -12,16 +13,21 @@ struct bar {
void (*refresh)(const struct bar *bar);
void (*set_cursor)(struct bar *bar, const char *cursor);
const char *(*output_name)(const struct bar *bar);
};
enum bar_location { BAR_TOP, BAR_BOTTOM };
enum bar_layer { BAR_LAYER_OVERLAY, BAR_LAYER_TOP, BAR_LAYER_BOTTOM, BAR_LAYER_BACKGROUND };
enum bar_backend { BAR_BACKEND_AUTO, BAR_BACKEND_XCB, BAR_BACKEND_WAYLAND };
struct bar_config {
enum bar_backend backend;
const char *monitor;
enum bar_layer layer;
enum bar_location location;
enum font_shaping font_shaping;
int height;
int left_spacing, right_spacing;
int left_margin, right_margin;
@ -30,7 +36,8 @@ struct bar_config {
pixman_color_t background;
struct {
int width;
int left_width, right_width;
int top_width, bottom_width;
pixman_color_t color;
int left_margin, right_margin;
int top_margin, bottom_margin;

View file

@ -7,11 +7,11 @@ endif
if backend_wayland
wayland_protocols = dependency('wayland-protocols')
wayland_protocols_datadir = wayland_protocols.get_pkgconfig_variable('pkgdatadir')
wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir')
wscanner = dependency('wayland-scanner', native: true)
wscanner_prog = find_program(
wscanner.get_pkgconfig_variable('wayland_scanner'), native: true)
wscanner.get_variable('wayland_scanner'), native: true)
wl_proto_headers = []
wl_proto_src = []

View file

@ -3,9 +3,11 @@
#include "../bar/bar.h"
#include "backend.h"
struct private {
struct private
{
/* From bar_config */
char *monitor;
enum bar_layer layer;
enum bar_location location;
int height;
int left_spacing, right_spacing;
@ -15,7 +17,8 @@ struct private {
pixman_color_t background;
struct {
int width;
int left_width, right_width;
int top_width, bottom_width;
pixman_color_t color;
int left_margin, right_margin;
int top_margin, bottom_margin;

File diff suppressed because it is too large Load diff

222
bar/xcb.c
View file

@ -1,16 +1,16 @@
#include "xcb.h"
#include <string.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <pthread.h>
#include <unistd.h>
#include <pixman.h>
#include <xcb/xcb.h>
#include <xcb/randr.h>
#include <xcb/render.h>
#include <xcb/xcb.h>
#include <xcb/xcb_aux.h>
#include <xcb/xcb_cursor.h>
#include <xcb/xcb_event.h>
@ -39,7 +39,6 @@ struct xcb_backend {
void *client_pixmap;
size_t client_pixmap_size;
pixman_image_t *pix;
};
void *
@ -55,11 +54,8 @@ setup(struct bar *_bar)
struct private *bar = _bar->private;
struct xcb_backend *backend = bar->backend.data;
if (bar->border.left_margin != 0 ||
bar->border.right_margin != 0 ||
bar->border.top_margin != 0 ||
bar->border.bottom_margin)
{
if (bar->border.left_margin != 0 || bar->border.right_margin != 0 || bar->border.top_margin != 0
|| bar->border.bottom_margin) {
LOG_WARN("non-zero border margins ignored in X11 backend");
}
@ -76,10 +72,8 @@ setup(struct bar *_bar)
xcb_screen_t *screen = xcb_aux_get_screen(backend->conn, default_screen);
xcb_randr_get_monitors_reply_t *monitors = xcb_randr_get_monitors_reply(
backend->conn,
xcb_randr_get_monitors(backend->conn, screen->root, 0),
&e);
xcb_randr_get_monitors_reply_t *monitors
= xcb_randr_get_monitors_reply(backend->conn, xcb_randr_get_monitors(backend->conn, screen->root, 0), &e);
if (e != NULL) {
LOG_ERR("failed to get monitor list: %s", xcb_error(e));
@ -90,17 +84,13 @@ setup(struct bar *_bar)
/* Find monitor coordinates and width/height */
bool found_monitor = false;
for (xcb_randr_monitor_info_iterator_t it =
xcb_randr_get_monitors_monitors_iterator(monitors);
it.rem > 0;
xcb_randr_monitor_info_next(&it))
{
for (xcb_randr_monitor_info_iterator_t it = xcb_randr_get_monitors_monitors_iterator(monitors); it.rem > 0;
xcb_randr_monitor_info_next(&it)) {
const xcb_randr_monitor_info_t *mon = it.data;
char *name = get_atom_name(backend->conn, mon->name);
LOG_INFO("monitor: %s: %ux%u+%u+%u (%ux%umm)", name,
mon->width, mon->height, mon->x, mon->y,
mon->width_in_millimeters, mon->height_in_millimeters);
LOG_INFO("monitor: %s: %ux%u+%u+%u (%ux%umm)", name, mon->width, mon->height, mon->x, mon->y,
mon->width_in_millimeters, mon->height_in_millimeters);
/* User wants a specific monitor, and this is not the one */
if (bar->monitor != NULL && strcmp(bar->monitor, name) != 0) {
@ -111,14 +101,11 @@ setup(struct bar *_bar)
backend->x = mon->x;
backend->y = mon->y;
bar->width = mon->width;
backend->y += bar->location == BAR_TOP ? 0
: screen->height_in_pixels - bar->height_with_border;
backend->y += bar->location == BAR_TOP ? 0 : mon->height - bar->height_with_border;
found_monitor = true;
if ((bar->monitor != NULL && strcmp(bar->monitor, name) == 0) ||
(bar->monitor == NULL && mon->primary))
{
if ((bar->monitor != NULL && strcmp(bar->monitor, name) == 0) || (bar->monitor == NULL && mon->primary)) {
/* Exact match */
free(name);
break;
@ -155,74 +142,47 @@ setup(struct bar *_bar)
LOG_DBG("using a %hhu-bit visual", depth);
backend->colormap = xcb_generate_id(backend->conn);
xcb_create_colormap(
backend->conn, 0, backend->colormap, screen->root, vis->visual_id);
xcb_create_colormap(backend->conn, 0, backend->colormap, screen->root, vis->visual_id);
backend->win = xcb_generate_id(backend->conn);
xcb_create_window(
backend->conn,
depth, backend->win, screen->root,
backend->x, backend->y, bar->width, bar->height_with_border,
0,
XCB_WINDOW_CLASS_INPUT_OUTPUT, vis->visual_id,
(XCB_CW_BACK_PIXEL |
XCB_CW_BORDER_PIXEL |
XCB_CW_EVENT_MASK |
XCB_CW_COLORMAP),
(const uint32_t []){
screen->black_pixel,
screen->white_pixel,
(XCB_EVENT_MASK_EXPOSURE |
XCB_EVENT_MASK_BUTTON_RELEASE |
XCB_EVENT_MASK_BUTTON_PRESS |
XCB_EVENT_MASK_POINTER_MOTION |
XCB_EVENT_MASK_STRUCTURE_NOTIFY),
backend->colormap}
);
backend->conn, depth, backend->win, screen->root, backend->x, backend->y, bar->width, bar->height_with_border,
0, XCB_WINDOW_CLASS_INPUT_OUTPUT, vis->visual_id,
(XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | XCB_CW_EVENT_MASK | XCB_CW_COLORMAP),
(const uint32_t[]){screen->black_pixel, screen->white_pixel,
(XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_BUTTON_PRESS
| XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_STRUCTURE_NOTIFY),
backend->colormap});
const char *title = "yambar";
xcb_change_property(
backend->conn,
XCB_PROP_MODE_REPLACE, backend->win,
XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8,
strlen(title), title);
xcb_change_property(backend->conn, XCB_PROP_MODE_REPLACE, backend->win, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8,
strlen(title), title);
xcb_change_property(
backend->conn,
XCB_PROP_MODE_REPLACE, backend->win,
_NET_WM_PID, XCB_ATOM_CARDINAL, 32, 1, (const uint32_t []){getpid()});
xcb_change_property(
backend->conn,
XCB_PROP_MODE_REPLACE, backend->win,
_NET_WM_WINDOW_TYPE, XCB_ATOM_ATOM, 32,
1, (const uint32_t []){_NET_WM_WINDOW_TYPE_DOCK});
xcb_change_property(
backend->conn,
XCB_PROP_MODE_REPLACE, backend->win,
_NET_WM_STATE, XCB_ATOM_ATOM, 32,
2, (const uint32_t []){_NET_WM_STATE_ABOVE, _NET_WM_STATE_STICKY});
xcb_change_property(
backend->conn,
XCB_PROP_MODE_REPLACE, backend->win,
_NET_WM_DESKTOP, XCB_ATOM_CARDINAL, 32, 1, (const uint32_t []){0xffffffff});
xcb_change_property(backend->conn, XCB_PROP_MODE_REPLACE, backend->win, _NET_WM_PID, XCB_ATOM_CARDINAL, 32, 1,
(const uint32_t[]){getpid()});
xcb_change_property(backend->conn, XCB_PROP_MODE_REPLACE, backend->win, _NET_WM_WINDOW_TYPE, XCB_ATOM_ATOM, 32, 1,
(const uint32_t[]){_NET_WM_WINDOW_TYPE_DOCK});
xcb_change_property(backend->conn, XCB_PROP_MODE_REPLACE, backend->win, _NET_WM_STATE, XCB_ATOM_ATOM, 32, 2,
(const uint32_t[]){_NET_WM_STATE_ABOVE, _NET_WM_STATE_STICKY});
xcb_change_property(backend->conn, XCB_PROP_MODE_REPLACE, backend->win, _NET_WM_DESKTOP, XCB_ATOM_CARDINAL, 32, 1,
(const uint32_t[]){0xffffffff});
/* Always on top */
xcb_configure_window(
backend->conn, backend->win, XCB_CONFIG_WINDOW_STACK_MODE,
(const uint32_t []){XCB_STACK_MODE_ABOVE});
xcb_configure_window(backend->conn, backend->win, XCB_CONFIG_WINDOW_STACK_MODE,
(const uint32_t[]){XCB_STACK_MODE_ABOVE});
uint32_t top_strut, bottom_strut;
uint32_t top_pair[2], bottom_pair[2];
if (bar->location == BAR_TOP) {
top_strut = backend->y + bar->height_with_border;
top_strut = bar->height_with_border;
top_pair[0] = backend->x;
top_pair[1] = backend->x + bar->width - 1;
bottom_strut = 0;
bottom_pair[0] = bottom_pair[1] = 0;
} else {
bottom_strut = screen->height_in_pixels - backend->y;
bottom_strut = bar->height_with_border;
bottom_pair[0] = backend->x;
bottom_pair[1] = backend->x + bar->width - 1;
@ -232,42 +192,38 @@ setup(struct bar *_bar)
uint32_t strut[] = {
/* left/right/top/bottom */
0, 0,
0,
0,
top_strut,
bottom_strut,
/* start/end pairs for left/right/top/bottom */
0, 0,
0, 0,
top_pair[0], top_pair[1],
bottom_pair[0], bottom_pair[1],
0,
0,
0,
0,
top_pair[0],
top_pair[1],
bottom_pair[0],
bottom_pair[1],
};
xcb_change_property(
backend->conn,
XCB_PROP_MODE_REPLACE, backend->win,
_NET_WM_STRUT, XCB_ATOM_CARDINAL, 32,
4, strut);
xcb_change_property(backend->conn, XCB_PROP_MODE_REPLACE, backend->win, _NET_WM_STRUT, XCB_ATOM_CARDINAL, 32, 4,
strut);
xcb_change_property(
backend->conn,
XCB_PROP_MODE_REPLACE, backend->win,
_NET_WM_STRUT_PARTIAL, XCB_ATOM_CARDINAL, 32,
12, strut);
xcb_change_property(backend->conn, XCB_PROP_MODE_REPLACE, backend->win, _NET_WM_STRUT_PARTIAL, XCB_ATOM_CARDINAL,
32, 12, strut);
backend->gc = xcb_generate_id(backend->conn);
xcb_create_gc(backend->conn, backend->gc, backend->win,
XCB_GC_FOREGROUND | XCB_GC_GRAPHICS_EXPOSURES,
(const uint32_t []){screen->white_pixel, 0});
xcb_create_gc(backend->conn, backend->gc, backend->win, XCB_GC_FOREGROUND | XCB_GC_GRAPHICS_EXPOSURES,
(const uint32_t[]){screen->white_pixel, 0});
const uint32_t stride = stride_for_format_and_width(
PIXMAN_a8r8g8b8, bar->width);
const uint32_t stride = stride_for_format_and_width(PIXMAN_a8r8g8b8, bar->width);
backend->client_pixmap_size = stride * bar->height_with_border;
backend->client_pixmap = malloc(backend->client_pixmap_size);
backend->pix = pixman_image_create_bits_no_clear(
PIXMAN_a8r8g8b8, bar->width, bar->height_with_border,
(uint32_t *)backend->client_pixmap, stride);
backend->pix = pixman_image_create_bits_no_clear(PIXMAN_a8r8g8b8, bar->width, bar->height_with_border,
(uint32_t *)backend->client_pixmap, stride);
bar->pix = backend->pix;
xcb_map_window(backend->conn, backend->win);
@ -310,10 +266,8 @@ cleanup(struct bar *_bar)
}
static void
loop(struct bar *_bar,
void (*expose)(const struct bar *bar),
void (*on_mouse)(struct bar *bar, enum mouse_event event,
enum mouse_button btn, int x, int y))
loop(struct bar *_bar, void (*expose)(const struct bar *bar),
void (*on_mouse)(struct bar *bar, enum mouse_event event, enum mouse_button btn, int x, int y))
{
struct private *bar = _bar->private;
struct xcb_backend *backend = bar->backend.data;
@ -323,10 +277,7 @@ loop(struct bar *_bar,
const int fd = xcb_get_file_descriptor(backend->conn);
while (true) {
struct pollfd fds[] = {
{.fd = _bar->abort_fd, .events = POLLIN},
{.fd = fd, .events = POLLIN}
};
struct pollfd fds[] = {{.fd = _bar->abort_fd, .events = POLLIN}, {.fd = fd, .events = POLLIN}};
poll(fds, sizeof(fds) / sizeof(fds[0]), -1);
@ -335,18 +286,14 @@ loop(struct bar *_bar,
if (fds[1].revents & POLLHUP) {
LOG_WARN("disconnected from XCB");
if (write(_bar->abort_fd, &(uint64_t){1}, sizeof(uint64_t))
!= sizeof(uint64_t))
{
if (write(_bar->abort_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) {
LOG_ERRNO("failed to signal abort to modules");
}
break;
}
for (xcb_generic_event_t *e = xcb_wait_for_event(backend->conn);
e != NULL;
e = xcb_poll_for_event(backend->conn))
{
for (xcb_generic_event_t *e = xcb_wait_for_event(backend->conn); e != NULL;
e = xcb_poll_for_event(backend->conn)) {
switch (XCB_EVENT_RESPONSE_TYPE(e)) {
case 0:
LOG_ERR("XCB: %s", xcb_error((const xcb_generic_error_t *)e));
@ -369,9 +316,12 @@ loop(struct bar *_bar,
const xcb_button_release_event_t *evt = (void *)e;
switch (evt->detail) {
case 1: case 2: case 3: case 4: case 5:
on_mouse(_bar, ON_MOUSE_CLICK,
evt->detail, evt->event_x, evt->event_y);
case 1:
case 2:
case 3:
case 4:
case 5:
on_mouse(_bar, ON_MOUSE_CLICK, evt->detail, evt->event_x, evt->event_y);
break;
}
break;
@ -405,10 +355,9 @@ commit(const struct bar *_bar)
const struct private *bar = _bar->private;
const struct xcb_backend *backend = bar->backend.data;
xcb_put_image(
backend->conn, XCB_IMAGE_FORMAT_Z_PIXMAP, backend->win, backend->gc,
bar->width, bar->height_with_border, 0, 0, 0,
backend->depth, backend->client_pixmap_size, backend->client_pixmap);
xcb_put_image(backend->conn, XCB_IMAGE_FORMAT_Z_PIXMAP, backend->win, backend->gc, bar->width,
bar->height_with_border, 0, 0, 0, backend->depth, backend->client_pixmap_size,
backend->client_pixmap);
xcb_flush(backend->conn);
}
@ -420,23 +369,19 @@ refresh(const struct bar *_bar)
/* Send an event to handle refresh from main thread */
/* Note: docs say that all X11 events are 32 bytes, reglardless of
/* Note: docs say that all X11 events are 32 bytes, regardless of
* the size of the event structure */
xcb_expose_event_t *evt = calloc(32, 1);
*evt = (xcb_expose_event_t){
.response_type = XCB_EXPOSE,
.window = backend->win,
.x = 0,
.y = 0,
.width = bar->width,
.height = bar->height,
.count = 1
};
*evt = (xcb_expose_event_t){.response_type = XCB_EXPOSE,
.window = backend->win,
.x = 0,
.y = 0,
.width = bar->width,
.height = bar->height,
.count = 1};
xcb_send_event(
backend->conn, false, backend->win, XCB_EVENT_MASK_EXPOSURE,
(char *)evt);
xcb_send_event(backend->conn, false, backend->win, XCB_EVENT_MASK_EXPOSURE, (char *)evt);
xcb_flush(backend->conn);
free(evt);
@ -458,8 +403,14 @@ set_cursor(struct bar *_bar, const char *cursor)
xcb_free_cursor(backend->conn, backend->cursor);
backend->cursor = xcb_cursor_load_cursor(backend->cursor_ctx, cursor);
xcb_change_window_attributes(
backend->conn, backend->win, XCB_CW_CURSOR, &backend->cursor);
xcb_change_window_attributes(backend->conn, backend->win, XCB_CW_CURSOR, &backend->cursor);
}
static const char *
output_name(const struct bar *_bar)
{
/* Not implemented */
return NULL;
}
const struct backend xcb_backend_iface = {
@ -469,4 +420,5 @@ const struct backend xcb_backend_iface = {
.commit = &commit,
.refresh = &refresh,
.set_cursor = &set_cursor,
.output_name = &output_name,
};

83
char32.c Normal file
View file

@ -0,0 +1,83 @@
#include "char32.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#if defined __has_include
#if __has_include(<stdc-predef.h>)
#include <stdc-predef.h>
#endif
#endif
#define LOG_MODULE "char32"
#define LOG_ENABLE_DBG 0
#include "log.h"
/*
* For now, assume we can map directly to the corresponding wchar_t
* functions. This is true if:
*
* - both data types have the same size
* - both use the same encoding (though we require that encoding to be UTF-32)
*/
_Static_assert(sizeof(wchar_t) == sizeof(char32_t), "wchar_t vs. char32_t size mismatch");
#if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__
#error "char32_t does not use UTF-32"
#endif
#if (!defined(__STDC_ISO_10646__) || !__STDC_ISO_10646__) && !defined(__FreeBSD__)
#error "wchar_t does not use UTF-32"
#endif
size_t
c32len(const char32_t *s)
{
return wcslen((const wchar_t *)s);
}
char32_t *
ambstoc32(const char *src)
{
if (src == NULL)
return NULL;
const size_t src_len = strlen(src);
char32_t *ret = malloc((src_len + 1) * sizeof(ret[0]));
if (ret == NULL)
return NULL;
mbstate_t ps = {0};
char32_t *out = ret;
const char *in = src;
const char *const end = src + src_len + 1;
size_t chars = 0;
size_t rc;
while ((rc = mbrtoc32(out, in, end - in, &ps)) != 0) {
switch (rc) {
case (size_t)-1:
case (size_t)-2:
case (size_t)-3:
goto err;
}
in += rc;
out++;
chars++;
}
*out = U'\0';
ret = realloc(ret, (chars + 1) * sizeof(ret[0]));
return ret;
err:
free(ret);
return NULL;
}

7
char32.h Normal file
View file

@ -0,0 +1,7 @@
#pragma once
#include <stddef.h>
#include <uchar.h>
size_t c32len(const char32_t *s);
char32_t *ambstoc32(const char *src);

View file

@ -8,5 +8,6 @@ _arguments \
'(-c --config)'{-c,--config}'[alternative configuration file]:filename:_files' \
'(-C --validate)'{-C,--validate}'[verify configuration then quit]' \
'(-p --print-pid)'{-p,--print-pid}'[print PID to this file or FD when up and running]:pidfile:_files' \
'(-d --log-level)'{-d,--log-level}'[log level (warning)]:loglevel:(info warning error none)' \
'(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \
'(-s --log-no-syslog)'{-s,--log-no-syslog}'[disable syslog logging]'

View file

@ -1,7 +1,7 @@
#include "config.h"
#include <string.h>
#include <assert.h>
#include <string.h>
#include <tllist.h>
@ -16,11 +16,9 @@ conf_err_prefix(const keychain_t *chain, const struct yml_node *node)
static char msg[4096];
int idx = 0;
idx += snprintf(&msg[idx], sizeof(msg) - idx, "%zu:%zu: ",
yml_source_line(node), yml_source_column(node));
idx += snprintf(&msg[idx], sizeof(msg) - idx, "%zu:%zu: ", yml_source_line(node), yml_source_column(node));
tll_foreach(*chain, key)
idx += snprintf(&msg[idx], sizeof(msg) - idx, "%s.", key->item);
tll_foreach(*chain, key) idx += snprintf(&msg[idx], sizeof(msg) - idx, "%s.", key->item);
/* Remove trailing "." */
msg[idx - 1] = '\0';
@ -45,8 +43,27 @@ conf_verify_int(keychain_t *chain, const struct yml_node *node)
if (yml_value_is_int(node))
return true;
LOG_ERR("%s: value is not an integer: '%s'",
conf_err_prefix(chain, node), yml_value_as_string(node));
LOG_ERR("%s: value is not an integer: '%s'", conf_err_prefix(chain, node), yml_value_as_string(node));
return false;
}
bool
conf_verify_unsigned(keychain_t *chain, const struct yml_node *node)
{
if (yml_value_is_int(node) && yml_value_as_int(node) >= 0)
return true;
LOG_ERR("%s: value is not a positive integer: '%s'", conf_err_prefix(chain, node), yml_value_as_string(node));
return false;
}
bool
conf_verify_bool(keychain_t *chain, const struct yml_node *node)
{
if (yml_value_is_bool(node))
return true;
LOG_ERR("%s: value is not a boolean: '%s'", conf_err_prefix(chain, node), yml_value_as_string(node));
return false;
}
@ -59,10 +76,7 @@ conf_verify_list(keychain_t *chain, const struct yml_node *node,
return false;
}
for (struct yml_list_iter iter = yml_list_iter(node);
iter.node != NULL;
yml_list_next(&iter))
{
for (struct yml_list_iter iter = yml_list_iter(node); iter.node != NULL; yml_list_next(&iter)) {
if (!verify(chain, iter.node))
return false;
}
@ -71,8 +85,7 @@ conf_verify_list(keychain_t *chain, const struct yml_node *node,
}
bool
conf_verify_enum(keychain_t *chain, const struct yml_node *node,
const char *values[], size_t count)
conf_verify_enum(keychain_t *chain, const struct yml_node *node, const char *values[], size_t count)
{
const char *s = yml_value_as_string(node);
if (s == NULL) {
@ -93,8 +106,7 @@ conf_verify_enum(keychain_t *chain, const struct yml_node *node,
}
bool
conf_verify_dict(keychain_t *chain, const struct yml_node *node,
const struct attr_info info[])
conf_verify_dict(keychain_t *chain, const struct yml_node *node, const struct attr_info info[])
{
if (!yml_is_dict(node)) {
LOG_ERR("%s: must be a dictionary", conf_err_prefix(chain, node));
@ -109,10 +121,7 @@ conf_verify_dict(keychain_t *chain, const struct yml_node *node,
bool exists[count];
memset(exists, 0, sizeof(exists));
for (struct yml_dict_iter it = yml_dict_iter(node);
it.key != NULL;
yml_dict_next(&it))
{
for (struct yml_dict_iter it = yml_dict_iter(node); it.key != NULL; yml_dict_next(&it)) {
const char *key = yml_value_as_string(it.key);
if (key == NULL) {
LOG_ERR("%s: key must be a string", conf_err_prefix(chain, it.key));
@ -152,21 +161,43 @@ conf_verify_dict(keychain_t *chain, const struct yml_node *node,
return true;
}
static bool
verify_on_click_path(keychain_t *chain, const struct yml_node *node)
{
if (!conf_verify_string(chain, node))
return false;
#if 1
/* We allow non-absolute paths in on-click handlers */
return true;
#else
const char *path = yml_value_as_string(node);
const bool is_absolute = path[0] == '/';
const bool is_tilde = path[0] == '~' && path[1] == '/';
if (!is_absolute && !is_tilde) {
LOG_ERR("%s: path must be either absolute, or begin with '~/", conf_err_prefix(chain, node));
return false;
}
return true;
#endif
}
bool
conf_verify_on_click(keychain_t *chain, const struct yml_node *node)
{
/* on-click: <command> */
const char *s = yml_value_as_string(node);
if (s != NULL)
return true;
return verify_on_click_path(chain, node);
static const struct attr_info info[] = {
{"left", false, &conf_verify_string},
{"middle", false, &conf_verify_string},
{"right", false, &conf_verify_string},
{"wheel-up", false, &conf_verify_string},
{"wheel-down", false, &conf_verify_string},
{NULL, false, NULL},
{"left", false, &verify_on_click_path}, {"middle", false, &verify_on_click_path},
{"right", false, &verify_on_click_path}, {"wheel-up", false, &verify_on_click_path},
{"wheel-down", false, &verify_on_click_path}, {"previous", false, &verify_on_click_path},
{"next", false, &verify_on_click_path}, {NULL, false, NULL},
};
return conf_verify_dict(chain, node, info);
@ -185,27 +216,30 @@ conf_verify_color(keychain_t *chain, const struct yml_node *node)
int v = sscanf(s, "%02x%02x%02x%02x", &r, &g, &b, &a);
if (strlen(s) != 8 || v != 4) {
LOG_ERR("%s: value must be a color ('rrggbbaa', e.g ff00ffff)",
conf_err_prefix(chain, node));
LOG_ERR("%s: value must be a color ('rrggbbaa', e.g ff00ffff)", conf_err_prefix(chain, node));
return false;
}
return true;
}
bool
conf_verify_font(keychain_t *chain, const struct yml_node *node)
{
if (!yml_is_scalar(node)) {
LOG_ERR("%s: font must be a fontconfig-formatted string",
conf_err_prefix(chain, node));
LOG_ERR("%s: font must be a fontconfig-formatted string", conf_err_prefix(chain, node));
return false;
}
return true;
}
bool
conf_verify_font_shaping(keychain_t *chain, const struct yml_node *node)
{
return conf_verify_enum(chain, node, (const char *[]){"full", /*"graphemes",*/ "none"}, 2);
}
bool
conf_verify_decoration(keychain_t *chain, const struct yml_node *node)
{
@ -213,7 +247,8 @@ conf_verify_decoration(keychain_t *chain, const struct yml_node *node)
if (yml_dict_length(node) != 1) {
LOG_ERR("%s: decoration must be a dictionary with a single key; "
"the name of the particle", conf_err_prefix(chain, node));
"the name of the particle",
conf_err_prefix(chain, node));
return false;
}
@ -229,8 +264,7 @@ conf_verify_decoration(keychain_t *chain, const struct yml_node *node)
const struct deco_iface *iface = plugin_load_deco(deco_name);
if (iface == NULL) {
LOG_ERR("%s: invalid decoration name: %s",
conf_err_prefix(chain, deco), deco_name);
LOG_ERR("%s: invalid decoration name: %s", conf_err_prefix(chain, deco), deco_name);
return false;
}
@ -247,10 +281,7 @@ conf_verify_particle_list_items(keychain_t *chain, const struct yml_node *node)
{
assert(yml_is_list(node));
for (struct yml_list_iter it = yml_list_iter(node);
it.node != NULL;
yml_list_next(&it))
{
for (struct yml_list_iter it = yml_list_iter(node); it.node != NULL; yml_list_next(&it)) {
if (!conf_verify_particle(chain, it.node))
return false;
}
@ -265,7 +296,8 @@ conf_verify_particle_dictionary(keychain_t *chain, const struct yml_node *node)
if (yml_dict_length(node) != 1) {
LOG_ERR("%s: particle must be a dictionary with a single key; "
"the name of the particle", conf_err_prefix(chain, node));
"the name of the particle",
conf_err_prefix(chain, node));
return false;
}
@ -281,8 +313,7 @@ conf_verify_particle_dictionary(keychain_t *chain, const struct yml_node *node)
const struct particle_iface *iface = plugin_load_particle(particle_name);
if (iface == NULL) {
LOG_ERR("%s: invalid particle name: %s",
conf_err_prefix(chain, particle), particle_name);
LOG_ERR("%s: invalid particle name: %s", conf_err_prefix(chain, particle), particle_name);
return false;
}
@ -304,19 +335,18 @@ conf_verify_particle(keychain_t *chain, const struct yml_node *node)
else if (yml_is_list(node))
return conf_verify_particle_list_items(chain, node);
else {
LOG_ERR("%s: particle must be either a dictionary or a list",
conf_err_prefix(chain, node));
LOG_ERR("%s: particle must be either a dictionary or a list", conf_err_prefix(chain, node));
return false;
}
}
static bool
verify_module(keychain_t *chain, const struct yml_node *node)
{
if (!yml_is_dict(node) || yml_dict_length(node) != 1) {
LOG_ERR("%s: module must be a dictionary with a single key; "
"the name of the module", conf_err_prefix(chain, node));
"the name of the module",
conf_err_prefix(chain, node));
return false;
}
@ -332,8 +362,7 @@ verify_module(keychain_t *chain, const struct yml_node *node)
const struct module_iface *iface = plugin_load_module(mod_name);
if (iface == NULL) {
LOG_ERR(
"%s: invalid module name: %s", conf_err_prefix(chain, node), mod_name);
LOG_ERR("%s: invalid module name: %s", conf_err_prefix(chain, node), mod_name);
return false;
}
@ -355,10 +384,7 @@ verify_module_list(keychain_t *chain, const struct yml_node *node)
return false;
}
for (struct yml_list_iter it = yml_list_iter(node);
it.node != NULL;
yml_list_next(&it))
{
for (struct yml_list_iter it = yml_list_iter(node); it.node != NULL; yml_list_next(&it)) {
if (!verify_module(chain, it.node))
return false;
}
@ -370,14 +396,12 @@ static bool
verify_bar_border(keychain_t *chain, const struct yml_node *node)
{
static const struct attr_info attrs[] = {
{"width", false, &conf_verify_int},
{"color", false, &conf_verify_color},
{"margin", false, &conf_verify_int},
{"left-margin", false, &conf_verify_int},
{"right-margin", false, &conf_verify_int},
{"top-margin", false, &conf_verify_int},
{"bottom-margin", false, &conf_verify_int},
{NULL, false, NULL},
{"width", false, &conf_verify_unsigned}, {"left-width", false, &conf_verify_unsigned},
{"right-width", false, &conf_verify_unsigned}, {"top-width", false, &conf_verify_unsigned},
{"bottom-width", false, &conf_verify_unsigned}, {"color", false, &conf_verify_color},
{"margin", false, &conf_verify_unsigned}, {"left-margin", false, &conf_verify_unsigned},
{"right-margin", false, &conf_verify_unsigned}, {"top-margin", false, &conf_verify_unsigned},
{"bottom-margin", false, &conf_verify_unsigned}, {NULL, false, NULL},
};
return conf_verify_dict(chain, node, attrs);
@ -389,6 +413,12 @@ verify_bar_location(keychain_t *chain, const struct yml_node *node)
return conf_verify_enum(chain, node, (const char *[]){"top", "bottom"}, 2);
}
static bool
verify_bar_layer(keychain_t *chain, const struct yml_node *node)
{
return conf_verify_enum(chain, node, (const char *[]){"overlay", "top", "bottom", "background"}, 4);
}
bool
conf_verify_bar(const struct yml_node *bar)
{
@ -401,29 +431,31 @@ conf_verify_bar(const struct yml_node *bar)
chain_push(&chain, "bar");
static const struct attr_info attrs[] = {
{"height", true, &conf_verify_int},
{"height", true, &conf_verify_unsigned},
{"location", true, &verify_bar_location},
{"background", true, &conf_verify_color},
{"monitor", false, &conf_verify_string},
{"layer", false, &verify_bar_layer},
{"spacing", false, &conf_verify_int},
{"left-spacing", false, &conf_verify_int},
{"right-spacing", false, &conf_verify_int},
{"spacing", false, &conf_verify_unsigned},
{"left-spacing", false, &conf_verify_unsigned},
{"right-spacing", false, &conf_verify_unsigned},
{"margin", false, &conf_verify_int},
{"left_margin", false, &conf_verify_int},
{"right_margin", false, &conf_verify_int},
{"margin", false, &conf_verify_unsigned},
{"left-margin", false, &conf_verify_unsigned},
{"right-margin", false, &conf_verify_unsigned},
{"border", false, &verify_bar_border},
{"font", false, &conf_verify_font},
{"font-shaping", false, &conf_verify_font_shaping},
{"foreground", false, &conf_verify_color},
{"left", false, &verify_module_list},
{"center", false, &verify_module_list},
{"right", false, &verify_module_list},
{"trackpad-sensitivity", false, &conf_verify_int},
{"trackpad-sensitivity", false, &conf_verify_unsigned},
{NULL, false, NULL},
};

View file

@ -26,15 +26,14 @@ chain_pop(keychain_t *chain)
tll_pop_back(*chain);
}
const char *conf_err_prefix(
const keychain_t *chain, const struct yml_node *node);
const char *conf_err_prefix(const keychain_t *chain, const struct yml_node *node);
bool conf_verify_string(keychain_t *chain, const struct yml_node *node);
bool conf_verify_int(keychain_t *chain, const struct yml_node *node);
bool conf_verify_unsigned(keychain_t *chain, const struct yml_node *node);
bool conf_verify_bool(keychain_t *chain, const struct yml_node *node);
bool conf_verify_enum(keychain_t *chain, const struct yml_node *node,
const char *values[], size_t count);
bool conf_verify_enum(keychain_t *chain, const struct yml_node *node, const char *values[], size_t count);
bool conf_verify_list(keychain_t *chain, const struct yml_node *node,
bool (*verify)(keychain_t *chain, const struct yml_node *node));
bool conf_verify_dict(keychain_t *chain, const struct yml_node *node,
@ -43,6 +42,7 @@ bool conf_verify_dict(keychain_t *chain, const struct yml_node *node,
bool conf_verify_on_click(keychain_t *chain, const struct yml_node *node);
bool conf_verify_color(keychain_t *chain, const struct yml_node *node);
bool conf_verify_font(keychain_t *chain, const struct yml_node *node);
bool conf_verify_font_shaping(keychain_t *chain, const struct yml_node *node);
bool conf_verify_particle(keychain_t *chain, const struct yml_node *node);
bool conf_verify_particle_list_items(keychain_t *chain, const struct yml_node *node);

237
config.c
View file

@ -1,9 +1,10 @@
#include "config.h"
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
@ -20,9 +21,7 @@
static uint8_t
hex_nibble(char hex)
{
assert((hex >= '0' && hex <= '9') ||
(hex >= 'a' && hex <= 'f') ||
(hex >= 'A' && hex <= 'F'));
assert((hex >= '0' && hex <= '9') || (hex >= 'a' && hex <= 'f') || (hex >= 'A' && hex <= 'F'));
if (hex >= '0' && hex <= '9')
return hex - '0';
@ -56,9 +55,9 @@ conf_to_color(const struct yml_node *node)
alpha |= alpha << 8;
return (pixman_color_t){
.red = (uint32_t)(red << 8 | red) * alpha / 0xffff,
.red = (uint32_t)(red << 8 | red) * alpha / 0xffff,
.green = (uint32_t)(green << 8 | green) * alpha / 0xffff,
.blue = (uint32_t)(blue << 8 | blue) * alpha / 0xffff,
.blue = (uint32_t)(blue << 8 | blue) * alpha / 0xffff,
.alpha = alpha,
};
}
@ -66,7 +65,69 @@ conf_to_color(const struct yml_node *node)
struct fcft_font *
conf_to_font(const struct yml_node *node)
{
return fcft_from_name(1, &(const char *){yml_value_as_string(node)}, NULL);
const char *font_spec = yml_value_as_string(node);
size_t count = 0;
size_t size = 0;
const char **fonts = NULL;
char *copy = strdup(font_spec);
for (const char *font = strtok(copy, ","); font != NULL; font = strtok(NULL, ",")) {
/* Trim spaces, strictly speaking not necessary, but looks nice :) */
while (isspace(font[0]))
font++;
if (font[0] == '\0')
continue;
if (count + 1 > size) {
size += 4;
fonts = realloc(fonts, size * sizeof(fonts[0]));
}
assert(count + 1 <= size);
fonts[count++] = font;
}
struct fcft_font *ret = fcft_from_name(count, fonts, NULL);
free(fonts);
free(copy);
return ret;
}
enum font_shaping
conf_to_font_shaping(const struct yml_node *node)
{
const char *v = yml_value_as_string(node);
if (strcmp(v, "none") == 0)
return FONT_SHAPE_NONE;
else if (strcmp(v, "graphemes") == 0) {
static bool have_warned = false;
if (!have_warned && !(fcft_capabilities() & FCFT_CAPABILITY_GRAPHEME_SHAPING)) {
have_warned = true;
LOG_WARN("cannot enable grapheme shaping; no support in fcft");
}
return FONT_SHAPE_GRAPHEMES;
}
else if (strcmp(v, "full") == 0) {
static bool have_warned = false;
if (!have_warned && !(fcft_capabilities() & FCFT_CAPABILITY_TEXT_RUN_SHAPING)) {
have_warned = true;
LOG_WARN("cannot enable full text shaping; no support in fcft");
}
return FONT_SHAPE_FULL;
}
else {
assert(false);
return FONT_SHAPE_NONE;
}
}
struct deco *
@ -84,25 +145,20 @@ conf_to_deco(const struct yml_node *node)
}
static struct particle *
particle_simple_list_from_config(const struct yml_node *node,
struct conf_inherit inherited)
particle_simple_list_from_config(const struct yml_node *node, struct conf_inherit inherited)
{
size_t count = yml_list_length(node);
struct particle *parts[count];
size_t idx = 0;
for (struct yml_list_iter it = yml_list_iter(node);
it.node != NULL;
yml_list_next(&it), idx++)
{
for (struct yml_list_iter it = yml_list_iter(node); it.node != NULL; yml_list_next(&it), idx++) {
parts[idx] = conf_to_particle(it.node, inherited);
}
/* Lazy-loaded function pointer to particle_list_new() */
static struct particle *(*particle_list_new)(
struct particle *common,
struct particle *particles[], size_t count,
int left_spacing, int right_spacing) = NULL;
static struct particle *(*particle_list_new)(struct particle *common, struct particle *particles[], size_t count,
int left_spacing, int right_spacing)
= NULL;
if (particle_list_new == NULL) {
const struct plugin *plug = plugin_load("list", PLUGIN_PARTICLE);
@ -111,8 +167,8 @@ particle_simple_list_from_config(const struct yml_node *node,
assert(particle_list_new != NULL);
}
struct particle *common = particle_common_new(
0, 0, NULL, fcft_clone(inherited.font), inherited.foreground, NULL);
struct particle *common = particle_common_new(0, 0, NULL, fcft_clone(inherited.font), inherited.font_shaping,
inherited.foreground, NULL);
return particle_list_new(common, parts, count, 0, 2);
}
@ -131,28 +187,53 @@ conf_to_particle(const struct yml_node *node, struct conf_inherit inherited)
const struct yml_node *right_margin = yml_get_value(pair.value, "right-margin");
const struct yml_node *on_click = yml_get_value(pair.value, "on-click");
const struct yml_node *font_node = yml_get_value(pair.value, "font");
const struct yml_node *font_shaping_node = yml_get_value(pair.value, "font-shaping");
const struct yml_node *foreground_node = yml_get_value(pair.value, "foreground");
const struct yml_node *deco_node = yml_get_value(pair.value, "deco");
int left = margin != NULL ? yml_value_as_int(margin) :
left_margin != NULL ? yml_value_as_int(left_margin) : 0;
int right = margin != NULL ? yml_value_as_int(margin) :
right_margin != NULL ? yml_value_as_int(right_margin) : 0;
int left = margin != NULL ? yml_value_as_int(margin) : left_margin != NULL ? yml_value_as_int(left_margin) : 0;
int right = margin != NULL ? yml_value_as_int(margin) : right_margin != NULL ? yml_value_as_int(right_margin) : 0;
const char *on_click_templates[MOUSE_BTN_COUNT] = {NULL};
char *on_click_templates[MOUSE_BTN_COUNT] = {NULL};
if (on_click != NULL) {
const char *legacy = yml_value_as_string(on_click);
const char *yml_legacy = yml_value_as_string(on_click);
if (yml_legacy != NULL) {
char *legacy = NULL;
if (yml_legacy[0] == '~' && yml_legacy[1] == '/') {
const char *home_dir = getenv("HOME");
if (home_dir != NULL)
if (asprintf(&legacy, "%s/%s", home_dir, yml_legacy + 2) < 0)
legacy = NULL;
if (legacy == NULL)
legacy = strdup(yml_legacy);
} else
legacy = strdup(yml_legacy);
if (legacy != NULL)
on_click_templates[MOUSE_BTN_LEFT] = legacy;
}
if (yml_is_dict(on_click)) {
for (struct yml_dict_iter it = yml_dict_iter(on_click);
it.key != NULL;
yml_dict_next(&it))
{
else if (yml_is_dict(on_click)) {
for (struct yml_dict_iter it = yml_dict_iter(on_click); it.key != NULL; yml_dict_next(&it)) {
const char *key = yml_value_as_string(it.key);
const char *template = yml_value_as_string(it.value);
const char *yml_template = yml_value_as_string(it.value);
char *template = NULL;
if (yml_template[0] == '~' && yml_template[1] == '/') {
const char *home_dir = getenv("HOME");
if (home_dir != NULL)
if (asprintf(&template, "%s/%s", home_dir, yml_template + 2) < 0)
template = NULL;
if (template == NULL)
template = strdup(yml_template);
} else
template = strdup(yml_template);
if (strcmp(key, "left") == 0)
on_click_templates[MOUSE_BTN_LEFT] = template;
@ -164,6 +245,10 @@ conf_to_particle(const struct yml_node *node, struct conf_inherit inherited)
on_click_templates[MOUSE_BTN_WHEEL_UP] = template;
else if (strcmp(key, "wheel-down") == 0)
on_click_templates[MOUSE_BTN_WHEEL_DOWN] = template;
else if (strcmp(key, "previous") == 0)
on_click_templates[MOUSE_BTN_PREVIOUS] = template;
else if (strcmp(key, "next") == 0)
on_click_templates[MOUSE_BTN_NEXT] = template;
else
assert(false);
}
@ -181,14 +266,14 @@ conf_to_particle(const struct yml_node *node, struct conf_inherit inherited)
* clone the font, since each particle takes ownership of its own
* font.
*/
struct fcft_font *font = font_node != NULL
? conf_to_font(font_node) : fcft_clone(inherited.font);
pixman_color_t foreground = foreground_node != NULL
? conf_to_color(foreground_node) : inherited.foreground;
struct fcft_font *font = font_node != NULL ? conf_to_font(font_node) : fcft_clone(inherited.font);
enum font_shaping font_shaping
= font_shaping_node != NULL ? conf_to_font_shaping(font_shaping_node) : inherited.font_shaping;
pixman_color_t foreground = foreground_node != NULL ? conf_to_color(foreground_node) : inherited.foreground;
/* Instantiate base/common particle */
struct particle *common = particle_common_new(
left, right, on_click_templates, font, foreground, deco);
struct particle *common
= particle_common_new(left, right, on_click_templates, font, font_shaping, foreground, deco);
const struct particle_iface *iface = plugin_load_particle(type);
@ -204,6 +289,8 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend)
struct bar_config conf = {
.backend = backend,
.layer = BAR_LAYER_BOTTOM,
.font_shaping = FONT_SHAPE_FULL,
};
/*
@ -214,8 +301,7 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend)
conf.height = yml_value_as_int(height);
const struct yml_node *location = yml_get_value(bar, "location");
conf.location = strcmp(yml_value_as_string(location), "top") == 0
? BAR_TOP : BAR_BOTTOM;
conf.location = strcmp(yml_value_as_string(location), "top") == 0 ? BAR_TOP : BAR_BOTTOM;
const struct yml_node *background = yml_get_value(bar, "background");
conf.background = conf_to_color(background);
@ -228,6 +314,21 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend)
if (monitor != NULL)
conf.monitor = yml_value_as_string(monitor);
const struct yml_node *layer = yml_get_value(bar, "layer");
if (layer != NULL) {
const char *tmp = yml_value_as_string(layer);
if (strcmp(tmp, "overlay") == 0)
conf.layer = BAR_LAYER_OVERLAY;
else if (strcmp(tmp, "top") == 0)
conf.layer = BAR_LAYER_TOP;
else if (strcmp(tmp, "bottom") == 0)
conf.layer = BAR_LAYER_BOTTOM;
else if (strcmp(tmp, "background") == 0)
conf.layer = BAR_LAYER_BACKGROUND;
else
assert(false);
}
const struct yml_node *spacing = yml_get_value(bar, "spacing");
if (spacing != NULL)
conf.left_spacing = conf.right_spacing = yml_value_as_int(spacing);
@ -252,15 +353,16 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend)
if (right_margin != NULL)
conf.right_margin = yml_value_as_int(right_margin);
const struct yml_node *trackpad_sensitivity =
yml_get_value(bar, "trackpad-sensitivity");
conf.trackpad_sensitivity = trackpad_sensitivity != NULL
? yml_value_as_int(trackpad_sensitivity)
: 30;
const struct yml_node *trackpad_sensitivity = yml_get_value(bar, "trackpad-sensitivity");
conf.trackpad_sensitivity = trackpad_sensitivity != NULL ? yml_value_as_int(trackpad_sensitivity) : 30;
const struct yml_node *border = yml_get_value(bar, "border");
if (border != NULL) {
const struct yml_node *width = yml_get_value(border, "width");
const struct yml_node *left_width = yml_get_value(border, "left-width");
const struct yml_node *right_width = yml_get_value(border, "right-width");
const struct yml_node *top_width = yml_get_value(border, "top-width");
const struct yml_node *bottom_width = yml_get_value(border, "bottom-width");
const struct yml_node *color = yml_get_value(border, "color");
const struct yml_node *margin = yml_get_value(border, "margin");
const struct yml_node *left_margin = yml_get_value(border, "left-margin");
@ -269,16 +371,24 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend)
const struct yml_node *bottom_margin = yml_get_value(border, "bottom-margin");
if (width != NULL)
conf.border.width = yml_value_as_int(width);
conf.border.left_width = conf.border.right_width = conf.border.top_width = conf.border.bottom_width
= yml_value_as_int(width);
if (left_width != NULL)
conf.border.left_width = yml_value_as_int(left_width);
if (right_width != NULL)
conf.border.right_width = yml_value_as_int(right_width);
if (top_width != NULL)
conf.border.top_width = yml_value_as_int(top_width);
if (bottom_width != NULL)
conf.border.bottom_width = yml_value_as_int(bottom_width);
if (color != NULL)
conf.border.color = conf_to_color(color);
if (margin != NULL)
conf.border.left_margin =
conf.border.right_margin =
conf.border.top_margin =
conf.border.bottom_margin = yml_value_as_int(margin);
conf.border.left_margin = conf.border.right_margin = conf.border.top_margin = conf.border.bottom_margin
= yml_value_as_int(margin);
if (left_margin != NULL)
conf.border.left_margin = yml_value_as_int(left_margin);
@ -298,6 +408,7 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend)
* foreground color at top-level.
*/
struct fcft_font *font = fcft_from_name(1, &(const char *){"sans"}, NULL);
enum font_shaping font_shaping = FONT_SHAPE_FULL;
pixman_color_t foreground = {0xffff, 0xffff, 0xffff, 0xffff}; /* White */
const struct yml_node *font_node = yml_get_value(bar, "font");
@ -306,12 +417,17 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend)
font = conf_to_font(font_node);
}
const struct yml_node *font_shaping_node = yml_get_value(bar, "font-shaping");
if (font_shaping_node != NULL)
font_shaping = conf_to_font_shaping(font_shaping_node);
const struct yml_node *foreground_node = yml_get_value(bar, "foreground");
if (foreground_node != NULL)
foreground = conf_to_color(foreground_node);
struct conf_inherit inherited = {
.font = font,
.font_shaping = font_shaping,
.foreground = foreground,
};
@ -327,10 +443,7 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend)
struct module **mods = calloc(count, sizeof(*mods));
size_t idx = 0;
for (struct yml_list_iter it = yml_list_iter(node);
it.node != NULL;
yml_list_next(&it), idx++)
{
for (struct yml_list_iter it = yml_list_iter(node); it.node != NULL; yml_list_next(&it), idx++) {
struct yml_dict_iter m = yml_dict_iter(it.node);
const char *mod_name = yml_value_as_string(m.key);
@ -341,14 +454,14 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend)
* applied to all its particles.
*/
const struct yml_node *mod_font = yml_get_value(m.value, "font");
const struct yml_node *mod_foreground = yml_get_value(
m.value, "foreground");
const struct yml_node *mod_font_shaping = yml_get_value(m.value, "font-shaping");
const struct yml_node *mod_foreground = yml_get_value(m.value, "foreground");
struct conf_inherit mod_inherit = {
.font = mod_font != NULL
? conf_to_font(mod_font) : inherited.font,
.foreground = mod_foreground != NULL
? conf_to_color(mod_foreground) : inherited.foreground,
.font = mod_font != NULL ? conf_to_font(mod_font) : inherited.font,
.font_shaping
= mod_font_shaping != NULL ? conf_to_font_shaping(mod_font_shaping) : inherited.font_shaping,
.foreground = mod_foreground != NULL ? conf_to_color(mod_foreground) : inherited.foreground,
};
const struct module_iface *iface = plugin_load_module(mod_name);

View file

@ -1,8 +1,9 @@
#pragma once
#include <fcft/fcft.h>
#include "yml.h"
#include "bar/bar.h"
#include "font-shaping.h"
#include "yml.h"
#include <fcft/fcft.h>
struct bar;
struct particle;
@ -16,12 +17,13 @@ struct bar *conf_to_bar(const struct yml_node *bar, enum bar_backend backend);
pixman_color_t conf_to_color(const struct yml_node *node);
struct fcft_font *conf_to_font(const struct yml_node *node);
enum font_shaping conf_to_font_shaping(const struct yml_node *node);
struct conf_inherit {
const struct fcft_font *font;
enum font_shaping font_shaping;
pixman_color_t foreground;
};
struct particle *conf_to_particle(
const struct yml_node *node, struct conf_inherit inherited);
struct particle *conf_to_particle(const struct yml_node *node, struct conf_inherit inherited);
struct deco *conf_to_deco(const struct yml_node *node);

View file

@ -4,10 +4,11 @@
struct deco {
void *private;
void (*expose)(const struct deco *deco, pixman_image_t *pix,
int x, int y, int width, int height);
void (*expose)(const struct deco *deco, pixman_image_t *pix, int x, int y, int width, int height);
void (*destroy)(struct deco *deco);
};
#define DECORATION_COMMON_ATTRS \
{NULL, false, NULL}
#define DECORATION_COMMON_ATTRS \
{ \
NULL, false, NULL \
}

View file

@ -1,12 +1,13 @@
#include <stdlib.h>
#include "../config.h"
#include "../config-verify.h"
#include "../config.h"
#include "../decoration.h"
#include "../plugin.h"
struct private {
//struct rgba color;
struct private
{
// struct rgba color;
pixman_color_t color;
};
@ -22,9 +23,7 @@ static void
expose(const struct deco *deco, pixman_image_t *pix, int x, int y, int width, int height)
{
const struct private *d = deco->private;
pixman_image_fill_rectangles(
PIXMAN_OP_OVER, pix, &d->color, 1,
&(pixman_rectangle16_t){x, y, width, height});
pixman_image_fill_rectangles(PIXMAN_OP_OVER, pix, &d->color, 1, &(pixman_rectangle16_t){x, y, width, height});
}
static struct deco *

91
decorations/border.c Normal file
View file

@ -0,0 +1,91 @@
#include <stdlib.h>
#include "../config-verify.h"
#include "../config.h"
#include "../decoration.h"
#include "../plugin.h"
#define LOG_MODULE "border"
#define LOG_ENABLE_DBG 0
#include "../log.h"
#define min(x, y) ((x) < (y) ? (x) : (y))
#define max(x, y) ((x) > (y) ? (x) : (y))
struct private
{
pixman_color_t color;
int size;
};
static void
destroy(struct deco *deco)
{
struct private *d = deco->private;
free(d);
free(deco);
}
static void
expose(const struct deco *deco, pixman_image_t *pix, int x, int y, int width, int height)
{
const struct private *d = deco->private;
pixman_image_fill_rectangles(PIXMAN_OP_OVER, pix, &d->color, 4,
(pixman_rectangle16_t[]){
/* Top */
{x, y, width, min(d->size, height)},
/* Bottom */
{x, max(y + height - d->size, y), width, min(d->size, height)},
/* Left */
{x, y, min(d->size, width), height},
/* Right */
{max(x + width - d->size, x), y, min(d->size, width), height},
});
}
static struct deco *
border_new(pixman_color_t color, int size)
{
struct private *priv = calloc(1, sizeof(*priv));
priv->color = color;
priv->size = size;
struct deco *deco = calloc(1, sizeof(*deco));
deco->private = priv;
deco->expose = &expose;
deco->destroy = &destroy;
return deco;
}
static struct deco *
from_conf(const struct yml_node *node)
{
const struct yml_node *color = yml_get_value(node, "color");
const struct yml_node *size = yml_get_value(node, "size");
return border_new(conf_to_color(color), size != NULL ? yml_value_as_int(size) : 1);
}
static bool
verify_conf(keychain_t *chain, const struct yml_node *node)
{
static const struct attr_info attrs[] = {
{"color", true, &conf_verify_color},
{"size", false, &conf_verify_unsigned},
DECORATION_COMMON_ATTRS,
};
return conf_verify_dict(chain, node, attrs);
}
const struct deco_iface deco_border_iface = {
.verify_conf = &verify_conf,
.from_conf = &from_conf,
};
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
extern const struct deco_iface iface __attribute__((weak, alias("deco_border_iface")));
#endif

View file

@ -1,7 +1,7 @@
deco_sdk = declare_dependency(dependencies: [pixman, tllist, fcft])
decorations = []
foreach deco : ['background', 'stack', 'underline']
foreach deco : ['background', 'border', 'stack', 'underline', 'overline']
if plugs_as_libs
shared_module('@0@'.format(deco), '@0@.c'.format(deco),
dependencies: deco_sdk,

71
decorations/overline.c Normal file
View file

@ -0,0 +1,71 @@
#include <stdlib.h>
#include "../config-verify.h"
#include "../config.h"
#include "../decoration.h"
#include "../plugin.h"
struct private
{
int size;
pixman_color_t color;
};
static void
destroy(struct deco *deco)
{
struct private *d = deco->private;
free(d);
free(deco);
}
static void
expose(const struct deco *deco, pixman_image_t *pix, int x, int y, int width, int height)
{
const struct private *d = deco->private;
pixman_image_fill_rectangles(PIXMAN_OP_OVER, pix, &d->color, 1, &(pixman_rectangle16_t){x, y, width, d->size});
}
static struct deco *
overline_new(int size, pixman_color_t color)
{
struct private *priv = calloc(1, sizeof(*priv));
priv->size = size;
priv->color = color;
struct deco *deco = calloc(1, sizeof(*deco));
deco->private = priv;
deco->expose = &expose;
deco->destroy = &destroy;
return deco;
}
static struct deco *
from_conf(const struct yml_node *node)
{
const struct yml_node *size = yml_get_value(node, "size");
const struct yml_node *color = yml_get_value(node, "color");
return overline_new(yml_value_as_int(size), conf_to_color(color));
}
static bool
verify_conf(keychain_t *chain, const struct yml_node *node)
{
static const struct attr_info attrs[] = {
{"size", true, &conf_verify_unsigned},
{"color", true, &conf_verify_color},
DECORATION_COMMON_ATTRS,
};
return conf_verify_dict(chain, node, attrs);
}
const struct deco_iface deco_overline_iface = {
.verify_conf = &verify_conf,
.from_conf = &from_conf,
};
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
extern const struct deco_iface iface __attribute__((weak, alias("deco_overline_iface")));
#endif

View file

@ -1,13 +1,14 @@
#include <stdlib.h>
#define LOG_MODULE "stack"
#include "../log.h"
#include "../config.h"
#include "../config-verify.h"
#include "../config.h"
#include "../decoration.h"
#include "../log.h"
#include "../plugin.h"
struct private {
struct private
{
struct deco **decos;
size_t count;
};
@ -57,10 +58,7 @@ from_conf(const struct yml_node *node)
struct deco *decos[count];
size_t idx = 0;
for (struct yml_list_iter it = yml_list_iter(node);
it.node != NULL;
yml_list_next(&it), idx++)
{
for (struct yml_list_iter it = yml_list_iter(node); it.node != NULL; yml_list_next(&it), idx++) {
decos[idx] = conf_to_deco(it.node);
}
@ -75,10 +73,7 @@ verify_conf(keychain_t *chain, const struct yml_node *node)
return false;
}
for (struct yml_list_iter it = yml_list_iter(node);
it.node != NULL;
yml_list_next(&it))
{
for (struct yml_list_iter it = yml_list_iter(node); it.node != NULL; yml_list_next(&it)) {
if (!conf_verify_decoration(chain, it.node))
return false;
}

View file

@ -1,11 +1,12 @@
#include <stdlib.h>
#include "../config.h"
#include "../config-verify.h"
#include "../config.h"
#include "../decoration.h"
#include "../plugin.h"
struct private {
struct private
{
int size;
pixman_color_t color;
};
@ -22,9 +23,8 @@ static void
expose(const struct deco *deco, pixman_image_t *pix, int x, int y, int width, int height)
{
const struct private *d = deco->private;
pixman_image_fill_rectangles(
PIXMAN_OP_OVER, pix, &d->color, 1,
&(pixman_rectangle16_t){x, y + height - d->size, width, d->size});
pixman_image_fill_rectangles(PIXMAN_OP_OVER, pix, &d->color, 1,
&(pixman_rectangle16_t){x, y + height - d->size, width, d->size});
}
static struct deco *
@ -54,7 +54,7 @@ static bool
verify_conf(keychain_t *chain, const struct yml_node *node)
{
static const struct attr_info attrs[] = {
{"size", true, &conf_verify_int},
{"size", true, &conf_verify_unsigned},
{"color", true, &conf_verify_color},
DECORATION_COMMON_ATTRS,
};

View file

@ -1,18 +1,86 @@
sh = find_program('sh', native: true)
scdoc = dependency('scdoc', native: true)
scdoc_prog = find_program(scdoc.get_pkgconfig_variable('scdoc'), native: true)
scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true)
foreach man_src : ['yambar.1.scd', 'yambar.5.scd', 'yambar-decorations.5.scd',
'yambar-modules-alsa.5.scd', 'yambar-modules-backlight.5.scd',
'yambar-modules-battery.5.scd', 'yambar-modules-clock.5.scd',
'yambar-modules-i3.5.scd', 'yambar-modules-label.5.scd',
'yambar-modules-mpd.5.scd', 'yambar-modules-network.5.scd',
'yambar-modules-removables.5.scd', 'yambar-modules-river.5.scd',
'yambar-modules-script.5.scd', 'yambar-modules-sway-xkb.5.scd',
'yambar-modules-sway.5.scd', 'yambar-modules-xkb.5.scd',
'yambar-modules-xwindow.5.scd', 'yambar-modules.5.scd',
'yambar-particles.5.scd', 'yambar-tags.5.scd']
plugin_pages = []
if plugin_alsa_enabled
plugin_pages += ['yambar-modules-alsa.5.scd']
endif
if plugin_backlight_enabled
plugin_pages += ['yambar-modules-backlight.5.scd']
endif
if plugin_battery_enabled
plugin_pages += ['yambar-modules-battery.5.scd']
endif
if plugin_clock_enabled
plugin_pages += ['yambar-modules-clock.5.scd']
endif
if plugin_cpu_enabled
plugin_pages += ['yambar-modules-cpu.5.scd']
endif
if plugin_disk_io_enabled
plugin_pages += ['yambar-modules-disk-io.5.scd']
endif
if plugin_dwl_enabled
plugin_pages += ['yambar-modules-dwl.5.scd']
endif
if plugin_foreign_toplevel_enabled
plugin_pages += ['yambar-modules-foreign-toplevel.5.scd']
endif
if plugin_mem_enabled
plugin_pages += ['yambar-modules-mem.5.scd']
endif
if plugin_mpd_enabled
plugin_pages += ['yambar-modules-mpd.5.scd']
endif
if plugin_mpris_enabled
plugin_pages += ['yambar-modules-mpris.5.scd']
endif
if plugin_i3_enabled
plugin_pages += ['yambar-modules-i3.5.scd']
plugin_pages += ['yambar-modules-sway.5.scd']
endif
if plugin_label_enabled
plugin_pages += ['yambar-modules-label.5.scd']
endif
if plugin_network_enabled
plugin_pages += ['yambar-modules-network.5.scd']
endif
if plugin_niri_language_enabled
plugin_pages += ['yambar-modules-niri-language.5.scd']
endif
if plugin_niri_workspaces_enabled
plugin_pages += ['yambar-modules-niri-workspaces.5.scd']
endif
if plugin_pipewire_enabled
plugin_pages += ['yambar-modules-pipewire.5.scd']
endif
if plugin_pulse_enabled
plugin_pages += ['yambar-modules-pulse.5.scd']
endif
if plugin_removables_enabled
plugin_pages += ['yambar-modules-removables.5.scd']
endif
if plugin_river_enabled
plugin_pages += ['yambar-modules-river.5.scd']
endif
if plugin_script_enabled
plugin_pages += ['yambar-modules-script.5.scd']
endif
if plugin_sway_xkb_enabled
plugin_pages += ['yambar-modules-sway-xkb.5.scd']
endif
if plugin_xkb_enabled
plugin_pages += ['yambar-modules-xkb.5.scd']
endif
foreach man_src : ['yambar.1.scd',
'yambar.5.scd',
'yambar-decorations.5.scd',
'yambar-modules.5.scd',
'yambar-particles.5.scd',
'yambar-tags.5.scd'] + plugin_pages
parts = man_src.split('.')
name = parts[-3]
section = parts[-2]
@ -22,7 +90,7 @@ foreach man_src : ['yambar.1.scd', 'yambar.5.scd', 'yambar-decorations.5.scd',
out,
output: out,
input: man_src,
command: [sh, '-c', '@0@ < @INPUT@'.format(scdoc_prog.path())],
command: [sh, '-c', '@0@ < @INPUT@'.format(scdoc_prog.full_path())],
capture: true,
install: true,
install_dir: join_paths(get_option('mandir'), 'man@0@'.format(section)))

View file

@ -23,7 +23,7 @@ This decoration sets the particles background color.
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| color
: color
: yes
@ -49,7 +49,7 @@ bottom of the particle.
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| size
: int
: yes
@ -70,9 +70,74 @@ content:
color: ff0000ff
```
# OVERLINE
Similar to _underline_, this decoration renders a line of configurable
size and color at the top of the particle.
## CONFIGURATION
[[ *Name*
:[ *Type*
:[ *Req*
:< *Description*
| size
: int
: yes
: The size (height/thickness) of the line, in pixels
| color
: color
: yes
: The color of the line. See *yambar*(5) for format.
## EXAMPLES
```
content:
string:
deco:
overline:
size: 2
color: ff0000ff
```
# BORDER
This decoration renders a border of configurable size (i.e border
width) around the particle.
## CONFIGURATION
[[ *Name*
:[ *Type*
:[ *Req*
:< *Description*
| color
: color
: yes
: The color of the line. See *yambar*(5) for format.
| size
: int
: no
: Border width, in pixels. Defaults to 1px.
## EXAMPLES
```
content:
string:
deco:
border:
size: 2
color: ff0000ff
```
# STACK
This particles combines multiple decorations.
This particle combines multiple decorations.
## CONFIGURATION

View file

@ -7,13 +7,21 @@ alsa - Monitors an alsa soundcard for volume and mute/unmute changes
[[ *Name*
:[ *Type*
:[ *Description*
:< *Description*
| online
: bool
: True when the ALSA device has successfully been opened
| dB
: range
: Volume level (in dB), with min and max as start and end range
values.
| volume
: range
: Volume level, with min and max as start and end range values
: Volume level (raw), with min and max as start and end range values
| percent
: range
: Volume level, as a percentage
: Volume level, as a percentage. This value is based on the *dB* tag
if available, otherwise the *volume* tag.
| muted
: bool
: True if muted, otherwise false
@ -24,7 +32,7 @@ alsa - Monitors an alsa soundcard for volume and mute/unmute changes
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| card
: string
: yes
@ -33,6 +41,17 @@ alsa - Monitors an alsa soundcard for volume and mute/unmute changes
: string
: yes
: Mixer channel to monitor. _Master_ might work.
| volume
: string
: no
: The name of the channel to use as source for the volume level
(default: first available channel, usually "Front Left").
| muted
: string
: no
: The name of the channel to use as source for the muted state
(default: first available channel, usually "Front Left").
# EXAMPLES

View file

@ -11,7 +11,7 @@ _/sys/class/backlight_, and uses *udev* to monitor for changes.
[[ *Name*
:[ *Type*
:[ *Description*
:< *Description*
| brightness
: range
: The current brightness level, in absolute value

View file

@ -8,11 +8,22 @@ battery - This module reads battery status
This module reads battery status from _/sys/class/power_supply_ and
uses *udev* to monitor for changes.
Note that it is common (and "normal") for batteries to be in the state
*unknown* under certain conditions.
For example, some have been seen to enter the *unknown* state when
charging and the capacity reaches ~90%. The battery then stays in
*unknown*, rather than *charging*, until it has been fully charged and
enters the state *full*.
This does not happen with all batteries, and other batteries may enter
the state *unknown* under other conditions.
# TAGS
[[ *Name*
:[ *Type*
:[ *Description*
:< *Description*
| name
: string
: Battery device name
@ -38,7 +49,7 @@ uses *udev* to monitor for changes.
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| name
: string
: yes
@ -46,7 +57,20 @@ uses *udev* to monitor for changes.
| poll-interval
: int
: no
: How often, in seconds, to poll for capacity changes (default=*60*). Set to `0` to disable polling (*warning*: many batteries do not support asynchronous reporting).
: How often, in milliseconds, to poll for capacity changes
(default=*60000*). Set to `0` to disable polling (*warning*: many
batteries do not support asynchronous reporting). Cannot be less
than 250ms.
| battery-scale
: int
: no
: How much to scale down the battery charge amount. Some batteries
report too high resulting in bad discharge estimates. Default=1.
| smoothing-secs
: int
: no
: How many seconds to perform smoothing over for battery discharge
estimates. Default=100s.
# EXAMPLES
@ -55,7 +79,7 @@ bar:
left:
- battery:
name: BAT0
poll-interval: 30
poll-interval: 30000
content:
string: {text: "BAT: {capacity}% {estimate}"}
```

View file

@ -7,7 +7,7 @@ clock - This module provides the current date and time
[[ *Name*
:[ *Type*
:[ *Description*
:< *Description*
| time
: string
: Current time, formatted using the _time-format_ attribute
@ -20,7 +20,7 @@ clock - This module provides the current date and time
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| time-format
: string
: no
@ -29,6 +29,10 @@ clock - This module provides the current date and time
: string
: no
: *strftime* formatter for the _date_ date (default=*%x*)
| utc
: bool
: no
: Use GMT instead of local timezone (default=false)
# EXAMPLES

View file

@ -0,0 +1,79 @@
yambar-modules-cpu(5)
# NAME
cpu - This module provides the CPU usage
# DESCRIPTION
This module reports CPU usage, in percent. The _content_ particle is a
template that is instantiated once for each core, and once for the
total CPU usage.
# TAGS
[[ *Name*
:[ *Type*
:< *Description*
| id
: int
: Core ID. 0..n represents individual cores, and -1 represents the
total usage
| cpu
: range
: Current usage of CPU core {id}, in percent
# CONFIGURATION
[[ *Name*
:[ *Type*
:[ *Req*
:< *Description*
| poll-interval
: int
: no
: Refresh interval of the CPU usage stats in milliseconds
(default=500). Cannot be less then 250ms.
# EXAMPLES
## Display total CPU usage as a number
```
bar:
left:
- cpu:
poll-interval: 2500
content:
map:
conditions:
id < 0:
- string: {text: , font: Font Awesome 6 Free:style=solid}
- string: {text: "{cpu}%"}
```
## Display a vertical bar for each core
```
bar:
left:
- cpu:
poll-interval: 2500
content:
map:
conditions:
id >= 0:
- ramp:
tag: cpu
items:
- string: {text: ▁}
- string: {text: ▂}
- string: {text: ▃}
- string: {text: ▄}
- string: {text: ▅}
- string: {text: ▆}
- string: {text: ▇}
- string: {text: █}
```
# SEE ALSO
*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5)

View file

@ -0,0 +1,85 @@
yambar-modules-disk-io(5)
# NAME
disk-io - This module keeps track of the amount of bytes being
read/written from/to disk. It can distinguish between all partitions
currently present in the machine.
# TAGS
[[ *Name*
:[ *Type*
:< *Description*
| device
: string
: Name of the device being tracked (use the command *lsblk* to see these).
There is a special device, "Total", that reports the total stats
for the machine
| is_disk
: boolean
: whether or not the device is a disk (e.g., sda, sdb) or a partition
(e.g., sda1, sda2, ...). "Total" is advertised as a disk.
| read_speed
: int
: bytes read, in bytes/s
| write_speed
: int
: bytes written, in bytes/s
| ios_in_progress
: int
: number of ios that are happening at the time of polling
# CONFIGURATION
[[ *Name*
:[ *Type*
:[ *Req*
:< *Description*
| poll-interval
: int
: no
: Refresh interval of disk's stats in milliseconds (default=500).
Cannot be less then 250ms.
# EXAMPLES
This reports the total amount of bytes being read and written every second,
formatting in b/s, kb/s, mb/s, or gb/s, as appropriate.
```
bar:
left:
- disk-io:
poll-interval: 1000
content:
map:
conditions:
device == Total:
list:
items:
- string: {text: "Total read: "}
- map:
default: {string: {text: "{read_speed} B/s"}}
conditions:
read_speed > 1073741824:
string: {text: "{read_speed:gib} GB/s"}
read_speed > 1048576:
string: {text: "{read_speed:mib} MB/s"}
read_speed > 1024:
string: {text: "{read_speed:kib} KB/s"}
- string: {text: " | "}
- string: {text: "Total written: "}
- map:
default: {string: {text: "{write_speed} B/s"}}
conditions:
write_speed > 1073741824:
string: {text: "{write_speed:gib} GB/s"}
write_speed > 1048576:
string: {text: "{write_speed:mib} MB/s"}
write_speed > 1024:
string: {text: "{write_speed:kib} KB/s"}
```
# SEE ALSO
*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5)

View file

@ -0,0 +1,107 @@
yambar-modules-dwl(5)
# NAME
dwl - This module provides information about dwl tags, and information.
# DESCRIPTION
This module provides a map of each tags present in dwl.
Each tags has its _id_, its _name_, its status (_selected_, _empty_, _urgent_)
and the global data like _title_, _appid_, _fullscreen_, _floating_,
_selmon_, and _layout_). The tags start a 1. For needs where
you only want information about the global data and not the _tags_,
there is a tag with the id _0_ that contains only the global data.
This module will track *only* the monitor where yambar was launched on.
If you have a multi monitor setup, please launch yambar on each of your
monitors.
Please, be aware that only *one instance* of this module is supported.
Running multiple instances at the same time may result in
*undefined behavior*.
# TAGS
[[ *Name*
:[ *Type*
:< *Description*
| id
: int
: dwl tag id.
| name
: string
: The name of the tag (defaults to _id_ if not set).
| selected
: bool
: True if the tag is currently selected.
| empty
: bool
: True if there are no windows in the tag.
| urgent
: bool
: True if the tag has the urgent flag set.
| title
: string
: The currently focused window's title.
| appid
: string
: The currently focused window's application id.
| fullscreen
: bool
: True if there is a fullscreen window in the current tag.
| floating
: bool
: True if there is a floating window in the current tag.
| selmon
: bool
: True if the monitor is actually focused.
| layout
: string
: The actual layout name of the tag.
# CONFIGURATION
[[ *Name*
:[ *Type*
:[ *Req*
:< *Description*
| number-of-tags
: int
: yes
: The number of defined tags in the dwl `config.def.h`.
| name-of-tags
: list
: false
: The name of the tags (must have the same length that _number-of-tags_).
| dwl-info-filename
: string
: yes
: The filepath to the log emitted by dwl when running.
# EXAMPLES
```
bar:
left:
- dwl:
number-of-tags: 9
dwl-info-filename: "/home/ogromny/dwl_info"
name-of-tags: [ , , , , , , , ,  ]
content:
list:
items:
- map:
conditions:
# default tag
id == 0: {string: {text: "{layout} {title}"}}
selected: {string: {text: "-> {name}"}}
~empty: {string: {text: "{name}"}}
urgent: {string: {text: "=> {name} <="}}
```
# SEE ALSO
*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5)

View file

@ -0,0 +1,78 @@
yambar-modules-foreign-toplevel(5)
# NAME
foreign-toplevel - This module provides information about toplevel windows in Wayland
# DESCRIPTION
This module uses the _wlr foreign toplevel management_ Wayland
protocol to provide information about currently open windows, such as
their application ID, window title, and their current state
(maximized/minimized/fullscreen/activated).
The configuration for the foreign-toplevel module specifies a
_template_ particle. This particle will be instantiated once for each
window.
Note: Wayland only.
# TAGS
[[ *Name*
:[ *Type*
:< *Description*
| app-id
: string
: The application ID (typically the application name)
| title
: string
: The window title
| maximized
: bool
: True if the window is currently maximized
| minimized
: bool
: True if the window is currently minimized
| fullscreen
: bool
: True if the window is currently fullscreened
| activated
: bool
: True if the window is currently activated (i.e. has focus)
# CONFIGURATION
[[ *Name*
:[ *Type*
:[ *Req*
:< *Description*
| content
: particle
: yes
: Template particle that will be instantiated once for each window
| all-monitors
: bool
: no
: When set to true, only windows on the same monitor the bar will be
used. The default is false.
# EXAMPLES
```
bar:
left:
- foreign-toplevel:
content:
map:
conditions:
~activated: {empty: {}}
activated:
- string: {text: "{app-id}: {title}"}
```
# SEE ALSO
*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5)

View file

@ -22,10 +22,13 @@ with the _application_ and _title_ tags to replace the X11-only
[[ *Name*
:[ *Type*
:[ *Description*
:< *Description*
| name
: string
: The workspace name
| output
: string
: The output (monitor) the workspace is on
| visible
: bool
: True if the workspace is currently visible (on any output)
@ -35,6 +38,9 @@ with the _application_ and _title_ tags to replace the X11-only
| urgent
: bool
: True if the workspace has the urgent flag set
| empty
: bool
: True if the workspace is empty (Sway only)
| state
: string
: One of *urgent*, *focused*, *unfocused* or *invisible* (note:
@ -54,7 +60,7 @@ with the _application_ and _title_ tags to replace the X11-only
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| content
: associative array
: yes
@ -64,7 +70,15 @@ with the _application_ and _title_ tags to replace the X11-only
| sort
: enum
: no
: How to sort the list of workspaces; one of _none_, _ascending_ or _descending_, defaults to _none_.
: How to sort the list of workspaces; one of _none_, _native_, _ascending_ or _descending_, defaults to _none_. Use _native_ to sort numbered workspaces only.
| strip-workspace-numbers
: bool
: no
: If true, *N:* prefixes will be stripped from workspace names. Useful together with *sort*, to have the workspace order fixed.
| persistent
: list of strings
: no
: Persistent workspaces. I.e. workspaces that are never removed, even if empty.
| left-spacing
: int
: no
@ -91,10 +105,9 @@ bar:
content:
"":
map:
tag: state
default: {string: {text: "{name}"}}
values:
focused: {string: {text: "{name}*"}}
conditions:
state == focused: {string: {text: "{name}*"}}
current: { string: {text: "{application}: {title}"}}
```

View file

@ -0,0 +1,52 @@
yambar-modules-mem(5)
# NAME
mem - This module provides the memory usage
# TAGS
[[ *Name*
:[ *Type*
:< *Description*
| free
: int
: Free memory in bytes
| used
: int
: Used memory in bytes
| total
: int
: Total memory in bytes
| percent_free
: range
: Free memory in percent
| percent_used
: range
: Used memory in percent
# CONFIGURATION
[[ *Name*
:[ *Type*
:[ *Req*
:< *Description*
| poll-interval
: string
: no
: Refresh interval of the memory usage stats in milliseconds
(default=500). Cannot be less then 250ms.
# EXAMPLES
```
bar:
left:
- mem:
poll-interval: 2500
content:
string: {text: "{used:mb}MB"}
```
# SEE ALSO
*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5)

View file

@ -7,7 +7,7 @@ mpd - This module provides MPD status such as currently playing artist/album/son
[[ *Name*
:[ *Type*
:[ *Description*
:< *Description*
| state
: string
: One of *offline*, *stopped*, *paused* or *playing*
@ -20,6 +20,9 @@ mpd - This module provides MPD status such as currently playing artist/album/son
| consume
: bool
: True if the *consume* flag is set
| single
: bool
: True if the *single* flag is set
| volume
: range
: Volume of MPD in percentage
@ -32,6 +35,9 @@ mpd - This module provides MPD status such as currently playing artist/album/son
| title
: string
: Title of currently playing song (also valid in *paused* state)
| file
: string
: Filename or URL of currently playing song (also valid in *paused* state)
| pos
: string
: *%M:%S*-formatted string describing the song's current position
@ -53,7 +59,7 @@ mpd - This module provides MPD status such as currently playing artist/album/son
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| host
: string
: yes

View file

@ -0,0 +1,101 @@
yambar-modules-mpris(5)
# NAME
mpris - This module provides MPRIS status such as currently playing artist/album/song
# TAGS
[[ *Name*
:[ *Type*
:< *Description*
| state
: string
: One of *offline*, *stopped*, *paused* or *playing*
| shuffle
: bool
: True if the *shuffle* flag is set
| repeat
: string
: One of *none*, *track* or *paylist*
| volume
: range
: Volume in percentage
| album
: string
: Currently playing album
| artist
: string
: Artist of currently playing song
| title
: string
: Title of currently playing song
| file
: string
: Filename or URL of currently playing song
| pos
: string
: *%M:%S*-formatted string describing the song's current position
(also see _elapsed_)
| end
: string
: *%M:%S*-formatted string describing the song's total length (also
see _duration_)
| elapsed
: realtime
: Position in currently playing song, in milliseconds. Can be used
with a _progress-bar_ particle.
# CONFIGURATION
[[ *Name*
:[ *Type*
:[ *Req*
:< *Description*
| identities
: list of string
: yes
: A list of MPRIS client identities
| query_timeout
: int
: no
: Dbus/MPRIS client connection timeout in ms. Try setting/incrementing
this value if the module reports a timeout error. Defaults to 500.
# EXAMPLES
```
bar:
center:
- mpris:
identities:
- "spotify"
- "firefox"
content:
map:
conditions:
state != offline && state != stopped:
- string: {text: "{artist}", max: 30 }
- string: {text: "-" }
- string: {text: "{title}", max: 30 }
```
# NOTE
The 'identity' refers a part of your clients DBus bus name.
You can obtain a list of active client names using:
```
Systemd: > busctl --user --list
Playerctl: > playerctl --list-all
Libdbus: > dbus-send --session --print-reply --type=method_call \
--dest='org.freedesktop.DBus' /org org.freedesktop.DBus.ListNames
```
MPRIS client bus names start with 'org.mpris.MediaPlayer2.<identity>'.
For example, firefox may use the bus name:
'org.mpris.MediaPlayer2.firefox.instance_1_7' which
gives us the identity 'firefox'
# SEE ALSO
*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5)

View file

@ -6,20 +6,31 @@ network - This module monitors network connection state
# DESCRIPTION
This module monitors network connection state; disconnected/connected
state and MAC/IP addresses.
state and MAC/IP addresses. It instantiates the provided _content_
particle for each network interface.
Note: while the module internally tracks all assigned IPv4/IPv6
addresses, it currently exposes only a single IPv4 and a single IPv6
address.
address per network interface.
# TAGS
[[ *Name*
:[ *Type*
:[ *Description*
:< *Description*
| name
: string
: Network interface name
| type
: string
: Interface type (*ether*, *wlan*, *loopback*, or *ARPHRD_NNN*, where
*N* is a number).
| kind
: string
: Interface kind. Empty for non-virtual interfaces. For virtual
interfaces, this value is taken from the _IFLA\_INFO\_KIND_ netlink
attribute. Examples of valid values are *bond*, *bridge*, *gre*, *tun*
and *veth*.
| index
: int
: Network interface index
@ -39,26 +50,74 @@ address.
| ipv6
: string
: IPv6 address assigned to the interface, or *""* if none
| ssid
: string
: SSID the adapter is connected to (Wi-Fi only)
| signal
: int
: Signal strength, in dBm (Wi-Fi only)
| quality
: range
: Quality of the signal, in percent (Wi-Fi only)
| rx-bitrate
: int
: RX bitrate, in bits/s
| tx-bitrate
: int
: TX bitrate in bits/s
| dl-speed
: int
: Download speed in bits/s
| ul-speed
: int
: Upload speed in bits/s
# CONFIGURATION
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
| name
: string
: Name of network interface to monitor
:< *Description*
| left-spacing
: int
: no
: Space, in pixels, in the left side of each rendered volume
| right-spacing
: int
: no
: Space, in pixels, on the right side of each rendered volume
| spacing
: int
: no
: Short-hand for setting both _left-spacing_ and _right-spacing_
| poll-interval
: int
: no
: Periodically (in milliseconds) update the signal, quality, rx+tx bitrate, and
ul+dl speed tags (default=0). Setting it to 0 disables updates. Cannot be less
than 250ms.
# EXAMPLES
Display all Ethernet (including WLAN) devices. This excludes loopback,
bridges etc.
```
bar:
left:
- network:
name: wlp3s0
content:
string: {text: "{name}: {state} ({ipv4})"}
map:
conditions:
type == ether || type == wlan:
map:
default:
string: {text: "{name}: {state} ({ipv4})"}
conditions:
ipv4 == "":
string: {text: "{name}: {state}"}
```
# SEE ALSO

View file

@ -0,0 +1,34 @@
yambar-modules-niri-language(5)
# NAME
niri-language - This module provides information about niri's currently
selected language.
# TAGS
[[ *Name*
:[ *Type*
:< *Description*
| language
: string
: The currently selected language.
# CONFIGURATION
No additional attributes supported, only the generic ones (see
*GENERIC CONFIGURATION* in *yambar-modules*(5))
# EXAMPLES
```
bar:
left:
- niri-language:
content:
string: {text: "{language}"}
```
# SEE ALSO
*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5)

View file

@ -0,0 +1,60 @@
yambar-modules-niri-workspaces(5)
# NAME
niri-workspaces - This module provides information about niri workspaces.
# DESCRIPTION
This module provides a map of each workspace present in niri.
Each workspace has its _id_, _name_, and its status (_focused_,
_active_, _empty_). The workspaces are sorted by their ids.
This module will *only* track the monitor where yambar was launched.
If you have a multi monitor setup, please launch yambar on each
individual monitor to track its workspaces.
# TAGS
[[ *Name*
:[ *Type*
:< *Description*
| id
: int
: The workspace id.
| name
: string
: The name of the workspace.
| active
: bool
: True if the workspace is currently visible on the current output.
| focused
: bool
: True if the workspace is currently focused.
| empty
: bool
: True if the workspace contains no window.
# CONFIGURATION
No additional attributes supported, only the generic ones (see
*GENERIC CONFIGURATION* in *yambar-modules*(5))
# EXAMPLES
```
bar:
left:
- niri-workspaces:
content:
map:
default: {string: {text: "| {id}"}}
conditions:
active: {string: {text: "-> {id}"}}
~empty: {string: {text: "@ {id}"}}
```
# SEE ALSO
*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5)

View file

@ -0,0 +1,103 @@
yambar-modules-pipewire(5)
# NAME
pipewire - Monitors pipewire for volume, mute/unmute, device change
# TAGS
[[ *Name*
:[ *Type*
:< *Description*
| type
: string
: Either "source" (capture) or "sink" (speaker)
| name
: string
: Current device name
| description
: string
: Current device description
| form_factor
: string
: Current device form factor (headset, speaker, mic, etc.)
| bus
: string
: Current device bus (bluetooth, alsa, etc.)
| icon
: string
: Current device icon name
| muted
: bool
: True if muted, otherwise false
| linear_volume
: range
: Linear volume in percentage (with 0 as min and 100 as max)
| cubic_volume
: range
: Cubic volume (used by pulseaudio) in percentage (with 0 as min and 100 as max)
# CONFIGURATION
[[ *Name*
:[ *Type*
:[ *Req*
:< *Description*
| left-spacing
: int
: no
: Space, in pixels, in the left side of each rendered volume
| right-spacing
: int
: no
: Space, in pixels, on the right side of each rendered volume
| spacing
: int
: no
: Short-hand for setting both _left-spacing_ and _right-spacing_
| content
: particle
: yes
: Unlike other modules, _content_ is a template particle that will be
expanded twice (i.e. into a list of two elements). The first
element is the 'sink', and the second element the 'source'.
# EXAMPLES
```
bar:
left:
- pipewire:
anchors:
volume: &volume
conditions:
muted: {string: {text: "{linear_volume}%", foreground: ff0000ff}}
~muted: {string: {text: "{linear_volume}%"}}
content:
list:
items:
- map:
conditions:
type == "sink":
map:
conditions:
icon == "audio-headset-bluetooth":
string: {text: "🎧 "}
default:
- ramp:
tag: linear_volume
items:
- string: {text: "🔈 "}
- string: {text: "🔉 "}
- string: {text: "🔊 "}
type == "source":
- string: {text: "🎙 "}
- map:
<<: *volume
```
# SEE ALSO
*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5)

View file

@ -0,0 +1,67 @@
yambar-modules-pulse(5)
# NAME
pulse - Monitors a PulseAudio source and/or sink
# TAGS
[[ *Name*
:[ *Type*
:< *Description*
| online
: bool
: True when connected to the PulseAudio server
| sink_online
: bool
: True when the sink is present
| source_online
: bool
: True when the source is present
| sink_percent
: range
: Sink volume level, as a percentage
| source_percent
: range
: Source volume level, as a percentage
| sink_muted
: bool
: True if the sink is muted, otherwise false
| source_muted
: bool
: True if the source is muted, otherwise false
| sink_port
: string
: Description of the active sink port
| source_port
: string
: Description of the active source port
# CONFIGURATION
[[ *Name*
:[ *Type*
:[ *Req*
:< *Description*
| sink
: string
: no
: Name of sink to monitor (default: _@DEFAULT\_SINK@_).
| source
: string
: no
: Name of source to monitor (default: _@DEFAULT\_SOURCE@_).
# EXAMPLES
```
bar:
left:
- pulse:
content:
string: {text: "{sink_percent}% ({sink_port})"}
```
# SEE ALSO
*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5)

View file

@ -12,7 +12,7 @@ instantiates the provided _content_ particle for each detected drive.
[[ *Name*
:[ *Type*
:[ *Description*
:< *Description*
| vendor
: string
: Name of the drive vendor
@ -22,6 +22,10 @@ instantiates the provided _content_ particle for each detected drive.
| optical
: bool
: True if the drive is an optical drive (CD-ROM, DVD-ROM etc)
| audio
: bool
: True if an optical drive has an audio CD inserted (i.e. this
property is always false for non-optical drives).
| device
: string
: Volume device name (typically */dev/sd?*)
@ -44,7 +48,7 @@ instantiates the provided _content_ particle for each detected drive.
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| left-spacing
: int
: no
@ -70,13 +74,12 @@ bar:
- removables:
content:
map:
tag: mounted
values:
false:
conditions:
~mounted:
string:
on-click: udisksctl mount -b {device}
text: "{label}"
true:
mounted:
string:
on-click: udisksctl unmount -b {device}
text: "{label}"

View file

@ -1,7 +1,7 @@
yambar-modules-river(5)
# NAME
river - This module provide information about the river tags
river - This module provides information about the river tags
# DESCRIPTION
@ -12,21 +12,25 @@ about the river tags.
It has an interface similar to the i3/sway module.
The configuration for the river module specifies one _title_ particle,
which will be instantiated with tags representing the currently active
seat and the currently focused view's title.
which will be instantiated once for each seat, with tags representing
the seats' name, the title of the seats' currently focused view, and
its current river "mode".
It also specifies a _content_ template particle, which is instantiated
once for all 32 river tags. This means you probably want to use a
*map* particle to hide unused river tags.
# TAGS
# TAGS (for the "content" particle)
[[ *Name*
:[ *Type*
:[ *Description*
:< *Description*
| id
: int
: River tag number
| urgent
: bool
: True if the river tag has at least one urgent view.
| visible
: bool
: True if the river tag is focused by at least one output (i.e. visible on at least one monitor).
@ -38,20 +42,35 @@ once for all 32 river tags. This means you probably want to use a
: True if the river tag has views (i.e. windows).
| state
: string
: Set to *focused* if _focused_ is true, *unfocused* if _visible_ is true, but _focused_ is false, or *invisible* if the river tag is not visible on any monitors.
: Set to *urgent* if _urgent_ is true, *focused* if _focused_ is true,
*unfocused* if _visible_ is true, but _focused_ is false, or
*invisible* if the river tag is not visible on any monitors.
# TAGS (for the "title" particle)
[[ *Name*
:[ *Type*
:< *Description*
| seat
: string
: The name of the currently active seat (*title* particle only, see CONFIGURATION)
: The name of the seat.
| title
: string
: The focused view's title (*title* particle only, see CONFIGURATION)
: The seat's focused view's title.
| mode
: string
: The seat's current mode (entered with e.g. *riverctl enter-mode foobar*).
| layout
: string
: Current layout of the output currently focused by the seat.
# CONFIGURATION
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| title
: particle
: no
@ -60,6 +79,12 @@ once for all 32 river tags. This means you probably want to use a
: particle
: yes
: Template particle that will be instantiated once for all of the 32 river tags.
| all-monitors
: bool
: no
: When set to false (the default), tags reflect river tags and seats
for the monitor yambar is on only. When set to true, tags reflect
the union of all monitors.
# EXAMPLES
@ -67,13 +92,12 @@ once for all 32 river tags. This means you probably want to use a
bar:
left:
- river:
title: {string: { text: "{seat} - {title}" }}
title: {string: { text: "{seat} - {title} ({layout}/{mode})" }}
content:
map:
tag: occupied
values:
false: {empty: {}}
true:
conditions:
~occupied: {empty: {}}
occupied:
string:
margin: 5
text: "{id}: {state}"

View file

@ -16,7 +16,7 @@ configurable amount of time.
In continuous mode, the script is executed once. It will typically run
in a loop, sending an updated tag set whenever it needs, or wants
to. The last tag set is used (displayed) by yambar until a new tag set
is received. This mode is intended to be used by scripts that depends
is received. This mode is intended to be used by scripts that depend
on non-polling methods to update their state.
Tag sets, or _transactions_, are separated by an empty line
@ -66,19 +66,21 @@ User defined.
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| path
: string
: yes
: Path to script/binary to execute. Must be an absolute path.
: Path to script/binary to execute. Must either be an absolute path,
or start with *~/*.
| args
: list of strings
: no
: Arguments to pass to the script/binary.
| poll-interval
: integer
: Number of seconds between each script run. If unset, continuous mode
is used.
: no
: Number of milliseconds between each script run. If unset, or set to
0, continuous mode is used.
# EXAMPLES
@ -113,6 +115,36 @@ bar:
content: {string: {text: "{test}"}}
```
Another example use case of this module could be to display currently playing
song or other media from players that support MPRIS (Media Player Remote
Interfacing Specification):
```
bar:
center:
- script:
path: /usr/bin/playerctl
args:
- "--follow"
- "metadata"
- "-f"
- |
status|string|{{status}}
artist|string|{{artist}}
title|string|{{title}}
content:
map:
conditions:
status == Paused: {empty: {}}
status == Playing:
content: {string: {text: "{artist} - {title}"}}
```
The above snippet runs a _playerctl_ utility in _--follow_ mode, reacting to
media updates on DBUS and outputting status, artist and title of media being
played in a format that is recognized by yambar. See _playerctl_ documentation
for more available metadata fields and control over which players get used.
# SEE ALSO
*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5)

View file

@ -16,7 +16,7 @@ instantiated from this template, and represents an input device.
[[ *Name*
:[ *Type*
:[ *Description*
:< *Description*
| id
: string
: Input device identifier
@ -29,7 +29,7 @@ instantiated from this template, and represents an input device.
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| identifiers
: list of strings
: yes

View file

@ -14,7 +14,7 @@ Note: this module is X11 only. It does not work in Wayland.
[[ *Name*
:[ *Type*
:[ *Description*
:< *Description*
| name
: string
: Name of currently selected layout, long version (e.g. "English (US)")

View file

@ -16,7 +16,7 @@ _title_ tags.
[[ *Name*
:[ *Type*
:[ *Description*
:< *Description*
| application
: string
: Name of the application that owns the currently focused window

View file

@ -38,7 +38,7 @@ For example, to render _backlight_ as " 20%", you could use:
```
content:
- string:
font: Font Awesome 5 Free:style=solid:pixelsize=14
font: Font Awesome 6 Free:style=solid:pixelsize=14
text: 
- string:
font: Adobe Helvetica:pixelsize=12
@ -68,20 +68,17 @@ in red.
```
content:
map:
tag: carrier
values:
false: {empty: {}}
true:
conditions:
~carrier: {empty: {}}
carrier:
map:
tag: state
default: {string: {text: , font: *awesome, foreground: ffffff66}}
values:
up:
conditions:
state == up:
map:
tag: ipv4
default: {string: {text: , font: *awesome}}
values:
"": {string: {text: , font: *awesome, foreground: ffffff66}}
conditions:
ipv4 == "": {string: {text: , font: *awesome, foreground: ffffff66}}
```
## Use yaml anchors
@ -94,7 +91,7 @@ In these cases, you can define an anchor point, either at top-level,
or in a module's _anchors_ attribute:
```
awesome: &awesome Font Awesome 5 Free:style=solid:pixelsize=14
awesome: &awesome Font Awesome 6 Free:style=solid:pixelsize=14
```
@ -113,7 +110,7 @@ following attributes are supported by all modules:
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| content
: particle
: yes
@ -145,14 +142,28 @@ Available modules have their own pages:
*yambar-modules-clock*(5)
*yambar-modules-cpu*(5)
*yambar-modules-disk-io*(5)
*yambar-modules-dwl*(5)
*yambar-modules-foreign-toplevel*(5)
*yambar-modules-i3*(5)
*yambar-modules-label*(5)
*yambar-modules-mem*(5)
*yambar-modules-mpd*(5)
*yambar-modules-network*(5)
*yambar-modules-pipewire*(5)
*yambar-modules-pulse*(5)
*yambar-modules-removables*(5)
*yambar-modules-river*(5)
@ -163,6 +174,10 @@ Available modules have their own pages:
*yambar-modules-sway*(5)
*yambar-modules-niri-language*(5)
*yambar-modules-niri-workspaces*(5)
*yambar-modules-xkb*(5)
*yambar-modules-xwindow*(5)

View file

@ -12,7 +12,7 @@ following attributes are supported by all particles:
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| left-margin
: int
: no
@ -31,20 +31,76 @@ following attributes are supported by all particles:
: Font to use. Note that this is an inherited attribute; i.e. you can
set it on e.g. a _list_ particle, and it will apply to all
particles in the list.
| font-shaping
: enum
: no
: font-shaping; one of _full_ or _none_. When set to _full_ (the
default), strings will be "shaped" using HarfBuzz. Requires support
in fcft.
| foreground
: color
: no
: Foreground (text) color. Just like _font_, this is an inherited attribute.
| on-click
: associative array/string
: no
: When set to a string, executes the string as a command when the
particle is left-clicked. Tags can be used. Note that the string is
*not* executed in a shell. Environment variables are not expanded.
*~/* is expanded, but only in the first argument. The same applies
to all attributes associated with it, below.
| on-click.left
: string
: no
: Command to execute when the particle is clicked. Tags can be
used. Note that the string is *not* executed in a shell.
: Command to execute when the particle is left-clicked.
| on-click.right
: string
: no
: Command to execute when the particle is right-clicked.
| on-click.middle
: string
: no
: Command to execute when the particle is middle-clicked.
| on-click.wheel-up
: string
: no
: Command to execute every time a 'wheel-up' event is triggered.
| on-click.wheel-down
: string
: no
: Command to execute every time a 'wheel-down' event is triggered.
| on-click.previous
: string
: no
: Command to execute when the particle is clicked with the 'previous' button.
| on-click.next
: string
: no
: Command to execute when the particle is clicked with the 'next' button.
| deco
: decoration
: no
: Decoration to apply to the particle. See *yambar-decorations*(5)
## EXAMPLES:
*on-click* as a string (handles left click):
```
content:
<particle>:
on-click: command args
```
*on-click* as an associative array (handles other buttons):
```
content:
<particle>:
on-click:
left: command-1
wheel-up: command-3
wheel-down: command-4
```
# STRING
This is the most basic particle. It takes a format string, consisting
@ -59,7 +115,7 @@ of free text mixed with tag specifiers.
| text
: string
: yes
: Format string. Tags are spcified with _{tag_name}_. Some tag types
: Format string. Tags are specified with _{tag_name}_. Some tag types
have suffixes that can be appended (e.g. _{tag_name:suffix}_). See
*yambar-modules*(5)).
| max
@ -67,9 +123,9 @@ of free text mixed with tag specifiers.
: no
: Sets the rendered string's maximum length. If the final string's
length exceeds this, the rendered string will be truncated, and
"..." will be appended. Note that the trailing "..." are
"…" will be appended. Note that the trailing "…" is
*included* in the maximum length. I.e. if you set _max_ to '5', you
will only get *2* characters from the string.
will only get *4* characters from the string.
## EXAMPLES
@ -82,7 +138,7 @@ content:
# EMPTY
This particle is a place-holder. While it does not render any tags,
margins and decortions are rendered.
margins and decorations are rendered.
## CONFIGURATION
@ -99,7 +155,7 @@ content:
This particle is a list (or sequence, if you like) of other
particles. It can be used to render e.g. _string_ particles with
different font and/or color formatting. Or ay other particle
different font and/or color formatting. Or any other particle
combinations.
But note that this means you *cannot* set any attributes on the _list_
@ -110,7 +166,7 @@ particle itself.
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| items
: list
: yes
@ -158,51 +214,165 @@ content:
- string: ...
```
Note that the short form has a hard-coded *right-spacing* of 2. This
cannot be changed. If you want a different spacing, you must use an
explicit list particle (i.e. the long form).
# MAP
This particle maps the values of a specific tag to different
particles. In addition to explicit tag values, you can also specify a
particles based on conditions. A condition takes either the form of:
```
<tag> <operation> <value>
```
Or, for boolean tags:
```
<tag>
```
Where <tag> is the tag you would like to map, <operation> is one of:
[- ==
:- !=
:- >=
:- >
:- <=
:- <
and <value> is the value you would like to compare it to. *If the
value contains any non-alphanumerical characters, you must
surround it with ' \" ' *:
```
"hello world"
"@#$%"
```
Negation is done with a preceding '~':
```
~<tag>
~<condition>
```
To match for empty strings, use ' "" ':
```
<tag> == ""
```
String glob matching
To perform string matching using globbing with "\*" & "?" characters:
\* Match any zero or more characters. ? Match exactly any one
character.
```
<tag> ~~ "hello*"
```
Will match any string starting with "hello", including "hello",
"hello1", "hello123", etc.
```
<tag> ~~ "hello?"
```
Will match any string starting with "hello" followed by any single
character, including "hello1", "hello-", but not "hello".
Furthermore, you may use the boolean operators:
[- &&
:- ||
in order to create more complex conditions:
```
<condition1> && <condition2>
```
You may surround <condition> with parenthesis for clarity or
specifying precedence:
```
(<condition>)
<condition1> && (<condition2> || <condition3>)
```
In addition to explicit tag values, you can also specify a
default/fallback particle.
Note that conditions are evaluated in the order they appear. *If
multiple conditions are true, the first one will be used*. This means
that in a configuration such as:
```
tx-bitrate > 1000:
tx-bitrate > 1000000:
```
the second condition would never run, since whenever the second
condition is true, the first is also true. The correct way of doing
this would be to invert the order of the conditions:
```
tx-bitrate > 1000000:
tx-bitrate > 1000:
```
## CONFIGURATION
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
| tag
: string
: yes
: The tag (name of) which values should be mapped
| values
:< *Description*
| conditions
: associative array
: yes
: An associative array of tag values mapped to particles
: An associative array of conditions (see above) mapped to particles
| default
: particle
: no
: Default particle to use, when tag's value does not match any of the
mapped values.
: Default particle to use, none of the conditions are true
## EXAMPLES
```
content:
map:
tag: tag_name
default:
string:
text: this is the default particle; the tag's value is now {tag_name}
values:
one_value:
conditions:
tag == one_value:
string:
text: tag's value is now one_value
another_value:
tag == another_value:
string:
text: tag's value is now another_value
```
For a boolean tag:
```
content:
map:
conditions:
tag:
string:
text: tag is true
~tag:
string:
text: tag is false
```
# RAMP
This particle uses a range tag to index into an array of
@ -215,7 +385,7 @@ indicator.
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| tag
: string
: yes
@ -226,6 +396,18 @@ indicator.
: List of particles. Note that the tag value is *not* used as-is; its
minimum and maximum values are used to map the tag's range to the
particle list's range.
| min
: int
: no
: If present this will be used as a lower bound instead of the tags
minimum value. Tag values falling outside the defined range will
get clamped to min/max.
| max
: int
: no
: If present this will be used as an upper bound instead of the tags
maximum value. Tag values falling outside the defined range will
get clamped to min/max.
## EXAMPLES
@ -258,7 +440,7 @@ itself when needed.
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| tag
: string
: yes
@ -294,7 +476,7 @@ itself when needed.
```
content:
progres-bar:
progress-bar:
tag: tag_name
length: 20
start: {string: {text: ├}}

View file

@ -11,7 +11,7 @@ their information. Each module defines its own set of tags.
The available tag *types* are:
[[ *Type*
:[ *Description*
:< *Description*
| string
: Value is a string. Rendered as-is by the _string_ particle.
| int
@ -38,9 +38,97 @@ The available tag *types* are:
# FORMATTING
As mentioned above, each tag type has a default representation that is
used when the tag is rendered by a string particle.
A tag may be followed by one or more formatters that alter the tags
rendition.
All integer, floating point and boolean tag types can be modified to
instead be rendered in hexadecimal or octal form, by appending either
the *:hex* or *:oct* suffixes. For example, _\"{tag_name:hex}\"_.
Formatters are added by appending a ':' separated list of formatter
names:
"{tag_name:max:hex}"
In the table below, "kind" describes the type of action performed by
the formatter:
- *format*: changes the representation of the tag's value
- *selector*: changes what to render
In general, formatters of the same kind cannot be combined; if
multiple formatters of the same kind are specified, the last one will
be used.
[[ *Formatter*
:[ *Kind*
:[ *Applies to*
:< *Description*
| [0]<number>[.]
: format
: Numeric tags (integer and floats)
: The width reserved to the field. The leading '0' is optional and
indicates zero padding, as opposed to space padding. The trailing
'.' is also optional
| .<number>
: format
: Float tags
: How many decimals to print
| [0]<N>[.]<M>
: format
: N: numeric tags, M: float tags
: Combined version of the two previous formatters
| hex
: format
: All tag types
: Renders a tag's value in hex
| oct
: format
: All tag types
: Renders a tag's value in octal
| %
: format
: Range tags
: Renders a range tag's value as a percentage value
| /N
: format
: All tag types
: Renders a tag's value (in decimal) divided by N
| kb, mb, gb
: format
: All tag types
: Renders a tag's value (in decimal) divided by 1000, 1000^2 or
1000^3. Note: no unit suffix is appended
| kib, mib, gib
: format
: All tag types
: Same as *kb*, *mb* and *gb*, but divide by 1024^n instead of 1000^n.
| min
: selector
: Range tags
: Renders a range tag's minimum value
| max
: selector
: Range tags
: Renders a range tag's maximum value
| unit
: selector
: Realtime tags
: Renders a realtime tag's unit (e.g. "s", or "ms")
# EXAMPLES
- A numeric (float or int) tag with at least 3 digits, zero-padded if
necessary:
```
{tag:03}
```
- A float tag with 2 decimals:
```
{tag:.2}
```
- A "byte count" tag in gigabytes:
```
{tag:gib}GB
```

View file

@ -25,7 +25,11 @@ yambar - modular status panel for X11 and Wayland
*-p*,*--print-pid*=_FILE_|_FD_
Print PID to this file, or FD, when successfully started. The file
(or FD) is closed immediately after writing the PID. When a _FILE_
as been specified, the file is unlinked exit.
as been specified, the file is unlinked upon exiting.
*-d*,*--log-level*={*info*,*warning*,*error*,*none*}
Log level, used both for log output on stderr as well as
syslog. Default: _warning_.
*-l*,*--log-colorize*=[{*never*,*always*,*auto*}]
Enables or disables colorization of log output on stderr.

View file

@ -12,9 +12,10 @@ and reference them using anchors.
Besides the normal yaml types, there are a couple of yambar specific
types that are frequently used:
- *font*: this is a string in _fontconfig_ format. Example of valid values:
- Font Awesome 5 Brands
- Font Awesome 5 Free:style=solid
- *font*: this is a comma separated list of fonts in _fontconfig_
format. Example of valid values:
- Font Awesome 6 Brands
- Font Awesome 6 Free:style=solid
- Dina:pixelsize=10:slant=italic
- Dina:pixelsize=10:weight=bold
- *color*: an rgba hexstring; _RRGGBBAA_. Examples:
@ -22,12 +23,17 @@ types that are frequently used:
- 000000ff: black, no transparency
- 00ff00ff: green, no transparency
- ff000099: red, semi-transparent
- *environment reference*: a string that contains format ${VAR}. This will be
replaced by the value of the environment variable VAR. Example:
- ${HOME}
- ${HOME}/.config/yambar
- ENV is ${ENV}, ENV2 is ${ENV2}
# FORMAT
[[ *Name*
:[ *Type*
:[ *Req*
:[ *Description*
:< *Description*
| height
: int
: yes
@ -45,6 +51,11 @@ types that are frequently used:
: no
: Monitor to place the bar on. If not specified, the primary monitor will be
used
| layer
: string
: no
: Layer to put bar on. One of _overlay_, _top_, _bottom_ or
_background_. Wayland only. Default: _bottom_.
| left-spacing
: int
: no
@ -73,10 +84,26 @@ types that are frequently used:
: associative array
: no
: Configures the border around the status bar
| border.left-width
: int
: no
: Width of the border on the left side, in pixels
| border.right-width
: int
: no
: Width of the border on the right side, in pixels
| border.top-width
: int
: no
: Width of the border on the top side, in pixels
| border.bottom-width
: int
: no
: Width of the border on the bottom side, in pixels
| border.width
: int
: no
: Width, in pixels, of the border
: Short-hand for setting _border.left/right/top/bottom-width_
| border.color
: color
: no
@ -104,7 +131,17 @@ types that are frequently used:
| font
: font
: no
: Default font to use in modules and particles
: Default font to use in modules and particles. May also be a comma
separated list of several fonts, in which case the first font is
the primary font, and the rest fallback fonts. These are yambar
custom fallback fonts that will be searched before the fontconfig
provided fallback list.
| font-shaping
: enum
: no
: Default setting for font-shaping, for use in particles. One of
_full_ or _none_. When set to _full_ (the default), strings will be
"shaped" using HarfBuzz. Requires support in fcft.
| foreground
: color
: no
@ -141,8 +178,8 @@ bar:
right:
- clock:
content:
- string: {text: "{time}"}
content:
- string: {text: "{time}"}
```
# FILES

View file

@ -4,9 +4,9 @@
# For X11/i3, you'll want to replace calls to swaymsg with i3-msg, and
# the sway-xkb module with the xkb module.
# fonts we'll be re-using here and there
awesome: &awesome Font Awesome 5 Free:style=solid:pixelsize=14
awesome_brands: &awesome_brands Font Awesome 5 Brands:pixelsize=16
# fonts we'll be reusing here and there
awesome: &awesome Font Awesome 6 Free:style=solid:pixelsize=14
awesome_brands: &awesome_brands Font Awesome 6 Brands:pixelsize=16
std_underline: &std_underline {underline: { size: 2, color: ff0000ff}}
@ -46,82 +46,67 @@ bar:
foreground: 000000ff
deco: {stack: [background: {color: bc2b3fff}, <<: *std_underline]}
- map: &i3_mode
tag: mode
default:
- string:
margin: 5
text: "{mode}"
deco: {background: {color: cc421dff}}
- empty: {right-margin: 7}
values:
default: {empty: {}}
conditions:
mode == default: {empty: {}}
content:
"":
map:
tag: state
values:
focused: {string: {<<: [*default, *focused]}}
unfocused: {string: {<<: *default}}
invisible: {string: {<<: [*default, *invisible]}}
urgent: {string: {<<: [*default, *urgent]}}
conditions:
state == focused: {string: {<<: [*default, *focused]}}
state == unfocused: {string: {<<: *default}}
state == invisible: {string: {<<: [*default, *invisible]}}
state == urgent: {string: {<<: [*default, *urgent]}}
main:
map:
tag: state
values:
focused: {string: {<<: [*main, *focused]}}
unfocused: {string: {<<: *main}}
invisible: {string: {<<: [*main, *invisible]}}
urgent: {string: {<<: [*main, *urgent]}}
conditions:
state == focused: {string: {<<: [*main, *focused]}}
state == unfocused: {string: {<<: *main}}
state == invisible: {string: {<<: [*main, *invisible]}}
state == urgent: {string: {<<: [*main, *urgent]}}
surfing:
map:
tag: state
values:
focused: {string: {<<: [*surfing, *focused]}}
unfocused: {string: {<<: *surfing}}
invisible: {string: {<<: [*surfing, *invisible]}}
urgent: {string: {<<: [*surfing, *urgent]}}
conditions:
state == focused: {string: {<<: [*surfing, *focused]}}
state == unfocused: {string: {<<: *surfing}}
state == invisible: {string: {<<: [*surfing, *invisible]}}
state == urgent: {string: {<<: [*surfing, *urgent]}}
misc:
map:
tag: state
values:
focused: {string: {<<: [*misc, *focused]}}
unfocused: {string: {<<: *misc}}
invisible: {string: {<<: [*misc, *invisible]}}
urgent: {string: {<<: [*misc, *urgent]}}
conditions:
state == focused: {string: {<<: [*misc, *focused]}}
state == unfocused: {string: {<<: *misc}}
state == invisible: {string: {<<: [*misc, *invisible]}}
state == urgent: {string: {<<: [*misc, *urgent]}}
mail:
map:
tag: state
values:
focused: {string: {<<: [*mail, *focused]}}
unfocused: {string: {<<: *mail}}
invisible: {string: {<<: [*mail, *invisible]}}
urgent: {string: {<<: [*mail, *urgent]}}
conditions:
state == focused: {string: {<<: [*mail, *focused]}}
state == unfocused: {string: {<<: *mail}}
state == invisible: {string: {<<: [*mail, *invisible]}}
state == urgent: {string: {<<: [*mail, *urgent]}}
music:
map:
tag: state
values:
focused: {string: {<<: [*music, *focused]}}
unfocused: {string: {<<: *music}}
invisible: {string: {<<: [*music, *invisible]}}
urgent: {string: {<<: [*music, *urgent]}}
current:
map:
left-margin: 7
tag: application
values:
"":
- map: {<<: *i3_mode}
- string: {text: "{title}"}
default:
list:
spacing: 0
items:
- map: {<<: *i3_mode}
- string: {text: "{application}", max: 10, foreground: ffa0a0ff}
- string: {text: ": "}
- string: {text: "{title}", max: 35}
conditions:
state == focused: {string: {<<: [*music, *focused]}}
state == unfocused: {string: {<<: *music}}
state == invisible: {string: {<<: [*music, *invisible]}}
state == urgent: {string: {<<: [*music, *urgent]}}
- foreign-toplevel:
content:
map:
conditions:
~activated: {empty: {}}
activated:
- string: {text: "{app-id}", foreground: ffa0a0ff}
- string: {text: ": {title}"}
center:
- mpd:
host: /run/mpd/socket
@ -130,32 +115,28 @@ bar:
spacing: 0
items:
- map:
tag: state
values:
playing: {string: {text: "{artist}"}}
paused: {string: {text: "{artist}", foreground: ffffff66}}
conditions:
state == playing: {string: {text: "{artist}"}}
state == paused: {string: {text: "{artist}", foreground: ffffff66}}
- string: {text: " | ", foreground: ffffff66}
- map:
tag: state
values:
playing: {string: {text: "{album}"}}
paused: {string: {text: "{album}", foreground: ffffff66}}
conditions:
state == playing: {string: {text: "{album}"}}
state == paused: {string: {text: "{album}", foreground: ffffff66}}
- string: {text: " | ", foreground: ffffff66}
- map:
tag: state
values:
playing: {string: {text: "{title}", foreground: ffa0a0ff}}
paused: {string: {text: "{title}", foreground: ffffff66}}
conditions:
state == playing: {string: {text: "{title}", foreground: ffa0a0ff}}
state == paused: {string: {text: "{title}", foreground: ffffff66}}
content:
map:
margin: 10
tag: state
values:
offline: {string: {text: offline, foreground: ff0000ff}}
stopped: {string: {text: stopped}}
paused: {list: *artist_album_title}
playing: {list: *artist_album_title}
conditions:
state == offline: {string: {text: offline, foreground: ff0000ff}}
state == stopped: {string: {text: stopped}}
state == paused: {list: *artist_album_title}
state == playing: {list: *artist_album_title}
right:
- removables:
@ -165,24 +146,21 @@ bar:
spacing: 5
content:
map:
tag: mounted
values:
false:
conditions:
~mounted:
map:
tag: optical
on-click: udisksctl mount -b {device}
values:
false: [{string: *drive}, {string: {text: "{label}"}}]
true: [{string: *optical}, {string: {text: "{label}"}}]
true:
conditions:
~optical: [{string: *drive}, {string: {text: "{label}"}}]
optical: [{string: *optical}, {string: {text: "{label}"}}]
mounted:
map:
tag: optical
on-click: udisksctl unmount -b {device}
values:
false:
conditions:
~optical:
- string: {<<: *drive, deco: *std_underline}
- string: {text: "{label}"}
true:
optical:
- string: {<<: *optical, deco: *std_underline}
- string: {text: "{label}"}
- sway-xkb:
@ -191,66 +169,69 @@ bar:
- string: {text: , font: *awesome}
- string: {text: "{layout}"}
- network:
name: enp1s0
content:
map:
tag: carrier
values:
false: {empty: {}}
true:
default: {empty: {}}
conditions:
name == enp1s0:
map:
tag: state
default: {string: {text: , font: *awesome, foreground: ffffff66}}
values:
up:
conditions:
~carrier: {empty: {}}
carrier:
map:
tag: ipv4
default: {string: {text: , font: *awesome}}
values:
"": {string: {text: , font: *awesome, foreground: ffffff66}}
default: {string: {text: , font: *awesome, foreground: ffffff66}}
conditions:
state == up && ipv4 != "": {string: {text: , font: *awesome}}
- network:
name: wlp2s0
poll-interval: 1000
content:
map:
tag: state
default: {string: {text: , font: *awesome, foreground: ffffff66}}
values:
down: {string: {text: , font: *awesome, foreground: ff0000ff}}
up:
default: {empty: {}}
conditions:
name == wlp2s0:
map:
tag: ipv4
default: {string: {text: , font: *awesome}}
values:
"": {string: {text: , font: *awesome, foreground: ffffff66}}
default: {string: {text: , font: *awesome, foreground: ffffff66}}
conditions:
state == down: {string: {text: , font: *awesome, foreground: ff0000ff}}
state == up:
map:
default:
- string: {text: , font: *awesome}
- string: {text: "{ssid} {dl-speed:mb}/{ul-speed:mb} Mb/s"}
conditions:
ipv4 == "":
- string: {text: , font: *awesome, foreground: ffffff66}
- string: {text: "{ssid} {dl-speed:mb}/{ul-speed:mb} Mb/s", foreground: ffffff66}
- alsa:
card: hw:PCH
mixer: Master
content:
map:
on-click: /bin/sh -c "amixer -q sset Speaker unmute && amixer -q sset Headphone unmute && amixer -q sset Master toggle"
tag: muted
values:
true: {string: {text: , font: *awesome, foreground: ffffff66}}
false:
ramp:
tag: volume
items:
- string: {text: , font: *awesome}
- string: {text: , font: *awesome}
- string: {text: , font: *awesome}
- string: {text: , font: *awesome}
- string: {text: , font: *awesome}
conditions:
~online: {string: {text: , font: *awesome, foreground: ff0000ff}}
online:
map:
on-click: /bin/sh -c "amixer -q sset Speaker unmute && amixer -q sset Headphone unmute && amixer -q sset Master toggle"
conditions:
muted: {string: {text: , font: *awesome, foreground: ffffff66}}
~muted:
ramp:
tag: percent
items:
- string: {text: , font: *awesome}
- string: {text: , font: *awesome}
- string: {text: , font: *awesome}
- backlight:
name: intel_backlight
content: [ string: {text: , font: *awesome}, string: {text: "{percent}%"}]
- battery:
name: BAT0
poll-interval: 30
content:
map:
tag: state
values:
discharging:
poll-interval: 30000
anchors:
discharging: &discharging
list:
items:
- ramp:
tag: capacity
items:
@ -265,13 +246,20 @@ bar:
- string: {text: , font: *awesome}
- string: {text: , foreground: 00ff00ff, font: *awesome}
- string: {text: "{capacity}% {estimate}"}
charging:
content:
map:
conditions:
state == unknown:
<<: *discharging
state == discharging:
<<: *discharging
state == charging:
- string: {text: , foreground: 00ff00ff, font: *awesome}
- string: {text: "{capacity}% {estimate}"}
full:
state == full:
- string: {text: , foreground: 00ff00ff, font: *awesome}
- string: {text: "{capacity}% full"}
not charging:
state == "not charging":
- ramp:
tag: capacity
items:
@ -296,6 +284,6 @@ bar:
- label:
content:
string:
on-click: loginctl poweroff
on-click: systemctl poweroff
text: 
font: *awesome

View file

@ -0,0 +1,58 @@
hack: &hack Hack Nerd Font:pixelsize=13
bg_default: &bg_default {stack: [{background: {color: 81A1C1ff}}, {underline: {size: 4, color: D8DEE9ff}}]}
bar:
height: 40
location: top
font: JuliaMono:pixelsize=10
spacing: 2
margin: 0
layer: bottom
foreground: eeeeeeff
background: 2E3440dd
left:
- river:
anchors:
- base: &river_base
left-margin: 10
right-margin: 13
default: {string: {text: , font: *hack}}
conditions:
id == 1: {string: {text: ﳐ, font: *hack}}
id == 2: {string: {text: , font: *hack}}
id == 3: {string: {text: , font: *hack}}
id == 4: {string: {text: , font: *hack}}
id == 5: {string: {text: , font: *hack}}
id == 10: {string: {text: "scratchpad", font: *hack}}
id == 11: {string: {text: "work", font: *hack}}
content:
map:
on-click:
left: sh -c "riverctl set-focused-tags $((1 << ({id} - 1)))"
right: sh -c "riverctl toggle-focused-tags $((1 << ({id} -1)))"
middle: sh -c "riverctl toggle-view-tags $((1 << ({id} -1)))"
conditions:
state == urgent:
map:
<<: *river_base
deco: {background: {color: D08770ff}}
state == focused:
map:
<<: *river_base
deco: *bg_default
state == visible && ~occupied:
map:
<<: *river_base
state == visible && occupied:
map:
<<: *river_base
deco: *bg_default
state == unfocused:
map:
<<: *river_base
state == invisible && ~occupied: {empty: {}}
state == invisible && occupied:
map:
<<: *river_base
deco: {underline: {size: 3, color: ea6962ff}}

View file

@ -0,0 +1,69 @@
bg_default: &bg_default {stack: [{background: {color: 81A1C1ff}}, {underline: {size: 4, color: D8DEE9ff}}]}
bar:
height: 32
location: top
background: 000000ff
font: NotoSans:pixelsize=16
right:
- clock:
content:
- string: {text: , font: "Font Awesome 6 Free:style=solid:size=12"}
- string: {text: "{date}", right-margin: 5}
- string: {text: , font: "Font Awesome 6 Free:style=solid:size=12"}
- string: {text: "{time} "}
left:
- river:
anchors:
- base: &river_base
left-margin: 10
right-margin: 13
default: {string: {text: }}
conditions:
id == 1: {string: {text: 1}}
id == 2: {string: {text: 2}}
id == 3: {string: {text: 3}}
id == 4: {string: {text: 4}}
id == 5: {string: {text: 5}}
content:
map:
on-click:
left: sh -c "riverctl set-focused-tags $((1 << ({id} - 1)))"
right: sh -c "riverctl toggle-focused-tags $((1 << ({id} -1)))"
middle: sh -c "riverctl toggle-view-tags $((1 << ({id} -1)))"
conditions:
state == urgent:
map:
<<: *river_base
deco: {background: {color: D08770ff}}
state == focused:
map:
<<: *river_base
deco: *bg_default
state == visible && ~occupied:
map:
<<: *river_base
state == visible && occupied:
map:
<<: *river_base
deco: *bg_default
state == unfocused:
map:
<<: *river_base
state == invisible && ~occupied: {empty: {}}
state == invisible && occupied:
map:
<<: *river_base
deco: {underline: {size: 3, color: ea6962ff}}
center:
- foreign-toplevel:
content:
map:
conditions:
~activated: {empty: {}}
activated:
- string: {text: " {app-id}", foreground: ffa0a0ff}
- string: {text: ": {title}"}

View file

@ -19,7 +19,7 @@
#
# Now the fun part
#
# Exemple configuration:
# Example configuration:
#
# - script:
# path: /absolute/path/to/dwl-tags.sh
@ -31,39 +31,33 @@
# content:
# - map:
# margin: 4
# tag: tag_0_occupied
# values:
# true:
# conditions:
# tag_0_occupied:
# map:
# tag: tag_0_focused
# values:
# true: {string: {text: "{tag_0}", <<: *focused}}
# false: {string: {text: "{tag_0}", <<: *occupied}}
# false:
# conditions:
# tag_0_focused: {string: {text: "{tag_0}", <<: *focused}}
# ~tag_0_focused: {string: {text: "{tag_0}", <<: *occupied}}
# ~tag_0_occupied:
# map:
# tag: tag_0_focused
# values:
# true: {string: {text: "{tag_0}", <<: *focused}}
# false: {string: {text: "{tag_0}", <<: *default}}
# conditions:
# tag_0_focused: {string: {text: "{tag_0}", <<: *focused}}
# ~tag_0_focused: {string: {text: "{tag_0}", <<: *default}}
# ...
# ...
# ...
# ...
# - map:
# margin: 4
# tag: tag_8_occupied
# values:
# true:
# conditions:
# tag_8_occupied:
# map:
# tag: tag_8_focused
# values:
# true: {string: {text: "{tag_8}", <<: *focused}}
# false: {string: {text: "{tag_8}", <<: *occupied}}
# false:
# conditions:
# tag_8_focused: {string: {text: "{tag_8}", <<: *focused}}
# ~tag_8_focused: {string: {text: "{tag_8}", <<: *occupied}}
# ~tag_8_occupied:
# map:
# tag: tag_8_focused
# values:
# true: {string: {text: "{tag_8}", <<: *focused}}
# false: {string: {text: "{tag_8}", <<: *default}}
# tag_8_focused: {string: {text: "{tag_8}", <<: *focused}}
# ~tag_8_focused: {string: {text: "{tag_8}", <<: *default}}
# - list:
# spacing: 3
# items:
@ -127,7 +121,7 @@ while true; do
inotifywait -qq --event modify "${fname}"
# Get info from the file
output="$(tail -n4 "${fname}")"
output="$(tail -n6 "${fname}")"
title="$(echo "${output}" | grep title | cut -d ' ' -f 3- )"
#selmon="$(echo "${output}" | grep 'selmon')"
layout="$(echo "${output}" | grep layout | cut -d ' ' -f 3- )"
@ -142,4 +136,3 @@ done
unset -v output title layout activetags selectedtags
unset -v tags name

View file

@ -12,22 +12,21 @@
# {aur} int number of aur packages
# {pkg} int sum of both
#
# Exemples configuration:
# Examples configuration:
# - script:
# path: /absolute/path/to/pacman.sh
# args: []
# args: []
# content: { string: { text: "{pacman} + {aur} = {pkg}" } }
#
# To display a message when there is no update:
# - script:
# path: /absolute/path/to/pacman.sh
# args: []
# args: []
# content:
# map:
# tag: pkg
# default: { string: { text: "{pacman} + {aur} = {pkg}" } }
# values:
# 0: {string: {text: no updates}}
# conditions:
# pkg == 0: {string: {text: no updates}}
declare interval aur_helper pacman_num aur_num pkg_num
@ -48,9 +47,9 @@ while true; do
# Change interval
# NUMBER[SUFFIXE]
# Possible suffix:
# "s" seconds / "m" minutes / "h" hours / "d" days
# "s" seconds / "m" minutes / "h" hours / "d" days
interval="1h"
# Change your aur manager
aur_helper="paru"
@ -63,7 +62,7 @@ while true; do
else
aur_num=$("${aur_helper}" -Qmu | wc -l)
fi
pkg_num=$(( pacman_num + aur_num ))
printf -- '%s\n' "pacman|int|${pacman_num}"
@ -77,4 +76,3 @@ done
unset -v interval aur_helper pacman_num aur_num pkg_num
unset -f _err

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="river_status_unstable_v1">
<copyright>
Copyright 2020 Isaac Freund
Copyright 2020 The River Developers
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
@ -16,7 +16,7 @@
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
</copyright>
<interface name="zriver_status_manager_v1" version="1">
<interface name="zriver_status_manager_v1" version="4">
<description summary="manage river status objects">
A global factory for objects that receive status information specific
to river. It could be used to implement, for example, a status bar.
@ -47,7 +47,7 @@
</request>
</interface>
<interface name="zriver_output_status_v1" version="1">
<interface name="zriver_output_status_v1" version="4">
<description summary="track output tags and focus">
This interface allows clients to receive information about the current
windowing state of an output.
@ -75,12 +75,36 @@
</description>
<arg name="tags" type="array" summary="array of 32-bit bitfields"/>
</event>
<event name="urgent_tags" since="2">
<description summary="tags of the output with an urgent view">
Sent once on binding the interface and again whenever the set of
tags with at least one urgent view changes.
</description>
<arg name="tags" type="uint" summary="32-bit bitfield"/>
</event>
<event name="layout_name" since="4">
<description summary="name of the layout">
Sent once on binding the interface should a layout name exist and again
whenever the name changes.
</description>
<arg name="name" type="string" summary="layout name"/>
</event>
<event name="layout_name_clear" since="4">
<description summary="name of the layout">
Sent when the current layout name has been removed without a new one
being set, for example when the active layout generator disconnects.
</description>
</event>
</interface>
<interface name="zriver_seat_status_v1" version="1">
<interface name="zriver_seat_status_v1" version="3">
<description summary="track seat focus">
This interface allows clients to receive information about the current
focus of a seat.
focus of a seat. Note that (un)focused_output events will only be sent
if the client has bound the relevant wl_output globals.
</description>
<request name="destroy" type="destructor">
@ -112,5 +136,13 @@
</description>
<arg name="title" type="string" summary="title of the focused view"/>
</event>
<event name="mode" since="3">
<description summary="the active mode changed">
Sent once on binding the interface and again whenever a new mode
is entered (e.g. with riverctl enter-mode foobar).
</description>
<arg name="name" type="string" summary="name of the mode"/>
</event>
</interface>
</protocol>

View file

@ -0,0 +1,270 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wlr_foreign_toplevel_management_unstable_v1">
<copyright>
Copyright © 2018 Ilia Bozhinov
Permission to use, copy, modify, distribute, and sell this
software and its documentation for any purpose is hereby granted
without fee, provided that the above copyright notice appear in
all copies and that both that copyright notice and this permission
notice appear in supporting documentation, and that the name of
the copyright holders not be used in advertising or publicity
pertaining to distribution of the software without specific,
written prior permission. The copyright holders make no
representations about the suitability of this software for any
purpose. It is provided "as is" without express or implied
warranty.
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
</copyright>
<interface name="zwlr_foreign_toplevel_manager_v1" version="3">
<description summary="list and control opened apps">
The purpose of this protocol is to enable the creation of taskbars
and docks by providing them with a list of opened applications and
letting them request certain actions on them, like maximizing, etc.
After a client binds the zwlr_foreign_toplevel_manager_v1, each opened
toplevel window will be sent via the toplevel event
</description>
<event name="toplevel">
<description summary="a toplevel has been created">
This event is emitted whenever a new toplevel window is created. It
is emitted for all toplevels, regardless of the app that has created
them.
All initial details of the toplevel(title, app_id, states, etc.) will
be sent immediately after this event via the corresponding events in
zwlr_foreign_toplevel_handle_v1.
</description>
<arg name="toplevel" type="new_id" interface="zwlr_foreign_toplevel_handle_v1"/>
</event>
<request name="stop">
<description summary="stop sending events">
Indicates the client no longer wishes to receive events for new toplevels.
However the compositor may emit further toplevel_created events, until
the finished event is emitted.
The client must not send any more requests after this one.
</description>
</request>
<event name="finished">
<description summary="the compositor has finished with the toplevel manager">
This event indicates that the compositor is done sending events to the
zwlr_foreign_toplevel_manager_v1. The server will destroy the object
immediately after sending this request, so it will become invalid and
the client should free any resources associated with it.
</description>
</event>
</interface>
<interface name="zwlr_foreign_toplevel_handle_v1" version="3">
<description summary="an opened toplevel">
A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel
window. Each app may have multiple opened toplevels.
Each toplevel has a list of outputs it is visible on, conveyed to the
client with the output_enter and output_leave events.
</description>
<event name="title">
<description summary="title change">
This event is emitted whenever the title of the toplevel changes.
</description>
<arg name="title" type="string"/>
</event>
<event name="app_id">
<description summary="app-id change">
This event is emitted whenever the app-id of the toplevel changes.
</description>
<arg name="app_id" type="string"/>
</event>
<event name="output_enter">
<description summary="toplevel entered an output">
This event is emitted whenever the toplevel becomes visible on
the given output. A toplevel may be visible on multiple outputs.
</description>
<arg name="output" type="object" interface="wl_output"/>
</event>
<event name="output_leave">
<description summary="toplevel left an output">
This event is emitted whenever the toplevel stops being visible on
the given output. It is guaranteed that an entered-output event
with the same output has been emitted before this event.
</description>
<arg name="output" type="object" interface="wl_output"/>
</event>
<request name="set_maximized">
<description summary="requests that the toplevel be maximized">
Requests that the toplevel be maximized. If the maximized state actually
changes, this will be indicated by the state event.
</description>
</request>
<request name="unset_maximized">
<description summary="requests that the toplevel be unmaximized">
Requests that the toplevel be unmaximized. If the maximized state actually
changes, this will be indicated by the state event.
</description>
</request>
<request name="set_minimized">
<description summary="requests that the toplevel be minimized">
Requests that the toplevel be minimized. If the minimized state actually
changes, this will be indicated by the state event.
</description>
</request>
<request name="unset_minimized">
<description summary="requests that the toplevel be unminimized">
Requests that the toplevel be unminimized. If the minimized state actually
changes, this will be indicated by the state event.
</description>
</request>
<request name="activate">
<description summary="activate the toplevel">
Request that this toplevel be activated on the given seat.
There is no guarantee the toplevel will be actually activated.
</description>
<arg name="seat" type="object" interface="wl_seat"/>
</request>
<enum name="state">
<description summary="types of states on the toplevel">
The different states that a toplevel can have. These have the same meaning
as the states with the same names defined in xdg-toplevel
</description>
<entry name="maximized" value="0" summary="the toplevel is maximized"/>
<entry name="minimized" value="1" summary="the toplevel is minimized"/>
<entry name="activated" value="2" summary="the toplevel is active"/>
<entry name="fullscreen" value="3" summary="the toplevel is fullscreen" since="2"/>
</enum>
<event name="state">
<description summary="the toplevel state changed">
This event is emitted immediately after the zlw_foreign_toplevel_handle_v1
is created and each time the toplevel state changes, either because of a
compositor action or because of a request in this protocol.
</description>
<arg name="state" type="array"/>
</event>
<event name="done">
<description summary="all information about the toplevel has been sent">
This event is sent after all changes in the toplevel state have been
sent.
This allows changes to the zwlr_foreign_toplevel_handle_v1 properties
to be seen as atomic, even if they happen via multiple events.
</description>
</event>
<request name="close">
<description summary="request that the toplevel be closed">
Send a request to the toplevel to close itself. The compositor would
typically use a shell-specific method to carry out this request, for
example by sending the xdg_toplevel.close event. However, this gives
no guarantees the toplevel will actually be destroyed. If and when
this happens, the zwlr_foreign_toplevel_handle_v1.closed event will
be emitted.
</description>
</request>
<request name="set_rectangle">
<description summary="the rectangle which represents the toplevel">
The rectangle of the surface specified in this request corresponds to
the place where the app using this protocol represents the given toplevel.
It can be used by the compositor as a hint for some operations, e.g
minimizing. The client is however not required to set this, in which
case the compositor is free to decide some default value.
If the client specifies more than one rectangle, only the last one is
considered.
The dimensions are given in surface-local coordinates.
Setting width=height=0 removes the already-set rectangle.
</description>
<arg name="surface" type="object" interface="wl_surface"/>
<arg name="x" type="int"/>
<arg name="y" type="int"/>
<arg name="width" type="int"/>
<arg name="height" type="int"/>
</request>
<enum name="error">
<entry name="invalid_rectangle" value="0"
summary="the provided rectangle is invalid"/>
</enum>
<event name="closed">
<description summary="this toplevel has been destroyed">
This event means the toplevel has been destroyed. It is guaranteed there
won't be any more events for this zwlr_foreign_toplevel_handle_v1. The
toplevel itself becomes inert so any requests will be ignored except the
destroy request.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="destroy the zwlr_foreign_toplevel_handle_v1 object">
Destroys the zwlr_foreign_toplevel_handle_v1 object.
This request should be called either when the client does not want to
use the toplevel anymore or after the closed event to finalize the
destruction of the object.
</description>
</request>
<!-- Version 2 additions -->
<request name="set_fullscreen" since="2">
<description summary="request that the toplevel be fullscreened">
Requests that the toplevel be fullscreened on the given output. If the
fullscreen state and/or the outputs the toplevel is visible on actually
change, this will be indicated by the state and output_enter/leave
events.
The output parameter is only a hint to the compositor. Also, if output
is NULL, the compositor should decide which output the toplevel will be
fullscreened on, if at all.
</description>
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
</request>
<request name="unset_fullscreen" since="2">
<description summary="request that the toplevel be unfullscreened">
Requests that the toplevel be unfullscreened. If the fullscreen state
actually changes, this will be indicated by the state event.
</description>
</request>
<!-- Version 3 additions -->
<event name="parent" since="3">
<description summary="parent change">
This event is emitted whenever the parent of the toplevel changes.
No event is emitted when the parent handle is destroyed by the client.
</description>
<arg name="parent" type="object" interface="zwlr_foreign_toplevel_handle_v1" allow-null="true"/>
</event>
</interface>
</protocol>

7
font-shaping.h Normal file
View file

@ -0,0 +1,7 @@
#pragma once
enum font_shaping {
FONT_SHAPE_NONE,
FONT_SHAPE_GRAPHEMES,
FONT_SHAPE_FULL,
};

View file

@ -13,11 +13,18 @@ out_file=${3}
if [ -d "${src_dir}/.git" ] && command -v git > /dev/null; then
workdir=$(pwd)
cd "${src_dir}"
git_version=$(git describe --always --tags)
if git describe --tags > /dev/null 2>&1; then
git_version=$(git describe --always --tags)
else
# No tags available, happens in e.g. CI builds
git_version="${default_version}"
fi
git_branch=$(git rev-parse --abbrev-ref HEAD)
cd "${workdir}"
new_version="${git_version} ($(env LC_TIME=C date "+%b %d %Y"), branch '${git_branch}')"
new_version="${git_version} ($(date "+%b %d %Y"), branch '${git_branch}')"
else
new_version="${default_version}"
fi

258
log.c
View file

@ -1,41 +1,60 @@
#include "log.h"
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>
#include <assert.h>
#include <errno.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <syslog.h>
#define ALEN(v) (sizeof(v) / sizeof((v)[0]))
#define UNUSED __attribute__((unused))
static bool colorize = false;
static bool do_syslog = true;
static bool do_syslog = false;
static enum log_class log_level = LOG_CLASS_NONE;
static const struct {
const char name[8];
const char log_prefix[7];
uint8_t color;
int syslog_equivalent;
} log_level_map[] = {
[LOG_CLASS_NONE] = {"none", "none", 5, -1},
[LOG_CLASS_ERROR] = {"error", " err", 31, LOG_ERR},
[LOG_CLASS_WARNING] = {"warning", "warn", 33, LOG_WARNING},
[LOG_CLASS_INFO] = {"info", "info", 97, LOG_INFO},
[LOG_CLASS_DEBUG] = {"debug", " dbg", 36, LOG_DEBUG},
};
void
log_init(enum log_colorize _colorize, bool _do_syslog,
enum log_facility syslog_facility, enum log_class syslog_level)
log_init(enum log_colorize _colorize, bool _do_syslog, enum log_facility syslog_facility, enum log_class _log_level)
{
static const int facility_map[] = {
[LOG_FACILITY_USER] = LOG_USER,
[LOG_FACILITY_DAEMON] = LOG_DAEMON,
};
static const int level_map[] = {
[LOG_CLASS_ERROR] = LOG_ERR,
[LOG_CLASS_WARNING] = LOG_WARNING,
[LOG_CLASS_INFO] = LOG_INFO,
[LOG_CLASS_DEBUG] = LOG_DEBUG,
};
/* Don't use colors if NO_COLOR is defined and not empty */
const char *no_color_str = getenv("NO_COLOR");
const bool no_color = no_color_str != NULL && no_color_str[0] != '\0';
colorize = _colorize == LOG_COLORIZE_NEVER ? false : _colorize == LOG_COLORIZE_ALWAYS ? true : isatty(STDERR_FILENO);
colorize = _colorize == LOG_COLORIZE_NEVER
? false
: _colorize == LOG_COLORIZE_ALWAYS
? true
: !no_color && isatty(STDERR_FILENO);
do_syslog = _do_syslog;
log_level = _log_level;
if (do_syslog) {
openlog(NULL, /*LOG_PID*/0, facility_map[syslog_facility]);
setlogmask(LOG_UPTO(level_map[syslog_level]));
int slvl = log_level_map[_log_level].syslog_equivalent;
if (do_syslog && slvl != -1) {
openlog(NULL, /*LOG_PID*/ 0, facility_map[syslog_facility]);
setlogmask(LOG_UPTO(slvl));
}
}
@ -47,120 +66,153 @@ log_deinit(void)
}
static void
_log(enum log_class log_class, const char *module, const char *file, int lineno,
const char *fmt, int sys_errno, va_list va)
_log(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, int sys_errno,
va_list va)
{
const char *class = "abcd";
int class_clr = 0;
switch (log_class) {
case LOG_CLASS_ERROR: class = " err"; class_clr = 31; break;
case LOG_CLASS_WARNING: class = "warn"; class_clr = 33; break;
case LOG_CLASS_INFO: class = "info"; class_clr = 97; break;
case LOG_CLASS_DEBUG: class = " dbg"; class_clr = 36; break;
}
assert(log_class > LOG_CLASS_NONE);
assert(log_class < ALEN(log_level_map));
if (log_class > log_level)
return;
const char *prefix = log_level_map[log_class].log_prefix;
unsigned int class_clr = log_level_map[log_class].color;
char clr[16];
snprintf(clr, sizeof(clr), "\e[%dm", class_clr);
fprintf(stderr, "%s%s%s: ", colorize ? clr : "", class, colorize ? "\e[0m" : "");
snprintf(clr, sizeof(clr), "\033[%um", class_clr);
fprintf(stderr, "%s%s%s: ", colorize ? clr : "", prefix, colorize ? "\033[0m" : "");
if (colorize)
fprintf(stderr, "\e[2m");
fputs("\033[2m", stderr);
fprintf(stderr, "%s:%d: ", file, lineno);
if (colorize)
fprintf(stderr, "\e[0m");
fputs("\033[0m", stderr);
vfprintf(stderr, fmt, va);
if (sys_errno != 0)
fprintf(stderr, ": %s", strerror(sys_errno));
fprintf(stderr, ": %s (%d)", strerror(sys_errno), sys_errno);
fprintf(stderr, "\n");
fputc('\n', stderr);
}
static void
_sys_log(enum log_class log_class, const char *module,
const char *file __attribute__((unused)),
int lineno __attribute__((unused)),
const char *fmt, int sys_errno, va_list va)
_sys_log(enum log_class log_class, const char *module, const char UNUSED *file, int UNUSED lineno, const char *fmt,
int sys_errno, va_list va)
{
assert(log_class > LOG_CLASS_NONE);
assert(log_class < ALEN(log_level_map));
if (!do_syslog)
return;
if (log_class > log_level)
return;
/* Map our log level to syslog's level */
int level = -1;
switch (log_class) {
case LOG_CLASS_ERROR: level = LOG_ERR; break;
case LOG_CLASS_WARNING: level = LOG_WARNING; break;
case LOG_CLASS_INFO: level = LOG_INFO; break;
case LOG_CLASS_DEBUG: level = LOG_DEBUG; break;
}
int level = log_level_map[log_class].syslog_equivalent;
assert(level != -1);
char msg[4096];
int n = vsnprintf(msg, sizeof(msg), fmt, va);
assert(n >= 0);
const char *sys_err = sys_errno != 0 ? strerror(sys_errno) : NULL;
if (sys_errno != 0 && (size_t)n < sizeof(msg))
snprintf(msg + n, sizeof(msg) - n, ": %s", strerror(sys_errno));
va_list va2;
va_copy(va2, va);
/* Calculate required size of buffer holding the entire log message */
int required_len = 0;
required_len += strlen(module) + 2; /* "%s: " */
required_len += vsnprintf(NULL, 0, fmt, va2); va_end(va2);
if (sys_errno != 0)
required_len += strlen(sys_err) + 2; /* ": %s" */
/* Format the msg */
char *msg = malloc(required_len + 1);
int idx = 0;
idx += snprintf(&msg[idx], required_len + 1 - idx, "%s: ", module);
idx += vsnprintf(&msg[idx], required_len + 1 - idx, fmt, va);
if (sys_errno != 0) {
snprintf(
&msg[idx], required_len + 1 - idx, ": %s", strerror(sys_errno));
}
syslog(level, "%s", msg);
free(msg);
syslog(level, "%s: %s", module, msg);
}
void
log_msg(enum log_class log_class, const char *module,
const char *file, int lineno, const char *fmt, ...)
log_msg_va(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, va_list va)
{
va_list ap1, ap2;
va_start(ap1, fmt);
va_copy(ap2, ap1);
_log(log_class, module, file, lineno, fmt, 0, ap1);
_sys_log(log_class, module, file, lineno, fmt, 0, ap2);
va_end(ap1);
va_end(ap2);
va_list va2;
va_copy(va2, va);
_log(log_class, module, file, lineno, fmt, 0, va);
_sys_log(log_class, module, file, lineno, fmt, 0, va2);
va_end(va2);
}
void log_errno(enum log_class log_class, const char *module,
const char *file, int lineno,
const char *fmt, ...)
void
log_msg(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...)
{
va_list ap1, ap2;
va_start(ap1, fmt);
va_copy(ap2, ap1);
_log(log_class, module, file, lineno, fmt, errno, ap1);
_sys_log(log_class, module, file, lineno, fmt, errno, ap2);
va_end(ap1);
va_end(ap2);
va_list va;
va_start(va, fmt);
log_msg_va(log_class, module, file, lineno, fmt, va);
va_end(va);
}
void log_errno_provided(enum log_class log_class, const char *module,
const char *file, int lineno, int _errno,
const char *fmt, ...)
void
log_errno_va(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, va_list va)
{
va_list ap1, ap2;
va_start(ap1, fmt);
va_copy(ap2, ap1);
_log(log_class, module, file, lineno, fmt, _errno, ap1);
_sys_log(log_class, module, file, lineno, fmt, _errno, ap2);
va_end(ap1);
va_end(ap2);
log_errno_provided_va(log_class, module, file, lineno, errno, fmt, va);
}
void
log_errno(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...)
{
va_list va;
va_start(va, fmt);
log_errno_va(log_class, module, file, lineno, fmt, va);
va_end(va);
}
void
log_errno_provided_va(enum log_class log_class, const char *module, const char *file, int lineno, int errno_copy,
const char *fmt, va_list va)
{
va_list va2;
va_copy(va2, va);
_log(log_class, module, file, lineno, fmt, errno_copy, va);
_sys_log(log_class, module, file, lineno, fmt, errno_copy, va2);
va_end(va2);
}
void
log_errno_provided(enum log_class log_class, const char *module, const char *file, int lineno, int errno_copy,
const char *fmt, ...)
{
va_list va;
va_start(va, fmt);
log_errno_provided_va(log_class, module, file, lineno, errno_copy, fmt, va);
va_end(va);
}
static size_t
map_len(void)
{
size_t len = ALEN(log_level_map);
#ifndef _DEBUG
/* Exclude "debug" entry for non-debug builds */
len--;
#endif
return len;
}
int
log_level_from_string(const char *str)
{
if (str[0] == '\0')
return -1;
for (int i = 0, n = map_len(); i < n; i++)
if (strcmp(str, log_level_map[i].name) == 0)
return i;
return -1;
}
const char *
log_level_string_hint(void)
{
static char buf[64];
if (buf[0] != '\0')
return buf;
for (size_t i = 0, pos = 0, n = map_len(); i < n; i++) {
const char *entry = log_level_map[i].name;
const char *delim = (i + 1 < n) ? ", " : "";
pos += snprintf(buf + pos, sizeof(buf) - pos, "'%s'%s", entry, delim);
}
return buf;
}

55
log.h
View file

@ -1,42 +1,43 @@
#pragma once
#include <stdarg.h>
#include <stdbool.h>
enum log_colorize { LOG_COLORIZE_NEVER, LOG_COLORIZE_ALWAYS, LOG_COLORIZE_AUTO };
enum log_facility { LOG_FACILITY_USER, LOG_FACILITY_DAEMON };
enum log_class { LOG_CLASS_ERROR, LOG_CLASS_WARNING, LOG_CLASS_INFO, LOG_CLASS_DEBUG };
void log_init(enum log_colorize colorize, bool do_syslog,
enum log_facility syslog_facility, enum log_class syslog_level);
enum log_class { LOG_CLASS_NONE, LOG_CLASS_ERROR, LOG_CLASS_WARNING, LOG_CLASS_INFO, LOG_CLASS_DEBUG };
void log_init(enum log_colorize colorize, bool do_syslog, enum log_facility syslog_facility, enum log_class log_level);
void log_deinit(void);
void log_msg(enum log_class log_class, const char *module,
const char *file, int lineno,
const char *fmt, ...) __attribute__((format (printf, 5, 6)));
void log_msg(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...)
__attribute__((format(printf, 5, 6)));
void log_errno(enum log_class log_class, const char *module,
const char *file, int lineno,
const char *fmt, ...) __attribute__((format (printf, 5, 6)));
void log_errno(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...)
__attribute__((format(printf, 5, 6)));
void log_errno_provided(
enum log_class log_class, const char *module,
const char *file, int lineno, int _errno,
const char *fmt, ...) __attribute__((format (printf, 6, 7)));
void log_errno_provided(enum log_class log_class, const char *module, const char *file, int lineno, int _errno,
const char *fmt, ...) __attribute__((format(printf, 6, 7)));
#define LOG_ERR(fmt, ...) \
log_msg(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__)
#define LOG_ERRNO(fmt, ...) \
log_errno(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__)
#define LOG_ERRNO_P(fmt, _errno, ...) \
log_errno_provided(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, \
_errno, fmt, ## __VA_ARGS__)
#define LOG_WARN(fmt, ...) \
log_msg(LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__)
#define LOG_INFO(fmt, ...) \
log_msg(LOG_CLASS_INFO, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__)
void log_msg_va(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, va_list va)
__attribute__((format(printf, 5, 0)));
void log_errno_va(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt,
va_list va) __attribute__((format(printf, 5, 0)));
void log_errno_provided_va(enum log_class log_class, const char *module, const char *file, int lineno, int _errno,
const char *fmt, va_list va) __attribute__((format(printf, 6, 0)));
int log_level_from_string(const char *str);
const char *log_level_string_hint(void);
#define LOG_ERR(...) log_msg(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_ERRNO(...) log_errno(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_ERRNO_P(_errno, ...) \
log_errno_provided(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, _errno, __VA_ARGS__)
#define LOG_WARN(...) log_msg(LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_INFO(...) log_msg(LOG_CLASS_INFO, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__)
#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG
#define LOG_DBG(fmt, ...) \
log_msg(LOG_CLASS_DEBUG, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__)
#define LOG_DBG(...) log_msg(LOG_CLASS_DEBUG, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__)
#else
#define LOG_DBG(fmt, ...)
#define LOG_DBG(...)
#endif

85
main.c
View file

@ -1,4 +1,6 @@
#include <assert.h>
#include <errno.h>
#include <getopt.h>
#include <locale.h>
#include <poll.h>
#include <signal.h>
@ -9,14 +11,12 @@
#include <string.h>
#include <threads.h>
#include <unistd.h>
#include <getopt.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/eventfd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pwd.h>
#include <sys/eventfd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include "bar/bar.h"
#include "config.h"
@ -87,7 +87,7 @@ get_config_path(void)
static struct bar *
load_bar(const char *config_path, enum bar_backend backend)
{
FILE *conf_file = fopen(config_path, "r");
FILE *conf_file = fopen(config_path, "re");
if (conf_file == NULL) {
LOG_ERRNO("%s: failed to open", config_path);
return NULL;
@ -127,13 +127,14 @@ print_usage(const char *prog_name)
printf("Usage: %s [OPTION]...\n", prog_name);
printf("\n");
printf("Options:\n");
printf(" -b,--backend={xcb,wayland,auto} backend to use (default: auto)\n"
" -c,--config=FILE alternative configuration file\n"
" -C,--validate verify configuration then quit\n"
" -p,--print-pid=FILE|FD print PID to file or FD\n"
" -l,--log-colorize=[never|always|auto] enable/disable colorization of log output on stderr\n"
" -s,--log-no-syslog disable syslog logging\n"
" -v,--version show the version number and quit\n");
printf(" -b,--backend={xcb,wayland,auto} backend to use (default: auto)\n"
" -c,--config=FILE alternative configuration file\n"
" -C,--validate verify configuration then quit\n"
" -p,--print-pid=FILE|FD print PID to file or FD\n"
" -d,--log-level={info|warning|error|none} log level (warning)\n"
" -l,--log-colorize=[never|always|auto] enable/disable colorization of log output on stderr\n"
" -s,--log-no-syslog disable syslog logging\n"
" -v,--version show the version number and quit\n");
}
static bool
@ -146,9 +147,8 @@ print_pid(const char *pid_file, bool *unlink_at_exit)
int pid_fd = strtoul(pid_file, &end, 10);
if (errno != 0 || *end != '\0') {
if ((pid_fd = open(pid_file,
O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) < 0) {
if ((pid_fd = open(pid_file, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))
< 0) {
LOG_ERRNO("%s: failed to open", pid_file);
return false;
} else
@ -177,15 +177,16 @@ int
main(int argc, char *const *argv)
{
static const struct option longopts[] = {
{"backend", required_argument, 0, 'b'},
{"config", required_argument, 0, 'c'},
{"validate", no_argument, 0, 'C'},
{"print-pid", required_argument, 0, 'p'},
{"log-colorize", optional_argument, 0, 'l'},
{"log-no-syslog", no_argument, 0, 's'},
{"version", no_argument, 0, 'v'},
{"help", no_argument, 0, 'h'},
{NULL, no_argument, 0, 0},
{"backend", required_argument, 0, 'b'},
{"config", required_argument, 0, 'c'},
{"validate", no_argument, 0, 'C'},
{"print-pid", required_argument, 0, 'p'},
{"log-level", required_argument, 0, 'd'},
{"log-colorize", optional_argument, 0, 'l'},
{"log-no-syslog", no_argument, 0, 's'},
{"version", no_argument, 0, 'v'},
{"help", no_argument, 0, 'h'},
{NULL, no_argument, 0, 0},
};
bool unlink_pid_file = false;
@ -195,11 +196,12 @@ main(int argc, char *const *argv)
char *config_path = NULL;
enum bar_backend backend = BAR_BACKEND_AUTO;
enum log_class log_level = LOG_CLASS_WARNING;
enum log_colorize log_colorize = LOG_COLORIZE_AUTO;
bool log_syslog = true;
while (true) {
int c = getopt_long(argc, argv, ":b:c:Cp:l::svh", longopts, NULL);
int c = getopt_long(argc, argv, ":b:c:Cp:d:l::svh", longopts, NULL);
if (c == -1)
break;
@ -220,9 +222,8 @@ main(int argc, char *const *argv)
if (stat(optarg, &st) == -1) {
fprintf(stderr, "%s: invalid configuration file: %s\n", optarg, strerror(errno));
return EXIT_FAILURE;
} else if (!S_ISREG(st.st_mode)) {
fprintf(stderr, "%s: invalid configuration file: not a regular file\n",
optarg);
} else if (!S_ISREG(st.st_mode) && !S_ISFIFO(st.st_mode)) {
fprintf(stderr, "%s: invalid configuration file: neither a regular file nor a pipe or FIFO\n", optarg);
return EXIT_FAILURE;
}
@ -238,6 +239,16 @@ main(int argc, char *const *argv)
pid_file = optarg;
break;
case 'd': {
int lvl = log_level_from_string(optarg);
if (lvl < 0) {
fprintf(stderr, "-d,--log-level: %s: argument must be one of %s\n", optarg, log_level_string_hint());
return EXIT_FAILURE;
}
log_level = lvl;
break;
}
case 'l':
if (optarg == NULL || strcmp(optarg, "auto") == 0)
log_colorize = LOG_COLORIZE_AUTO;
@ -273,14 +284,12 @@ main(int argc, char *const *argv)
}
}
log_init(log_colorize, log_syslog, LOG_FACILITY_DAEMON, LOG_CLASS_INFO);
log_init(log_colorize, log_syslog, LOG_FACILITY_DAEMON, log_level);
_Static_assert(LOG_CLASS_ERROR + 1 == FCFT_LOG_CLASS_ERROR,
"fcft log level enum offset");
_Static_assert((int)LOG_COLORIZE_ALWAYS == (int)FCFT_LOG_COLORIZE_ALWAYS,
"fcft colorize enum mismatch");
fcft_log_init(
(enum fcft_log_colorize)log_colorize, log_syslog, FCFT_LOG_CLASS_INFO);
_Static_assert((int)LOG_CLASS_ERROR == (int)FCFT_LOG_CLASS_ERROR, "fcft log level enum offset");
_Static_assert((int)LOG_COLORIZE_ALWAYS == (int)FCFT_LOG_COLORIZE_ALWAYS, "fcft colorize enum mismatch");
fcft_init((enum fcft_log_colorize)log_colorize, log_syslog, (enum fcft_log_class)log_level);
atexit(&fcft_fini);
const struct sigaction sa = {.sa_handler = &signal_handler};
sigaction(SIGINT, &sa, NULL);
@ -357,7 +366,7 @@ main(int argc, char *const *argv)
}
if (aborted)
LOG_INFO("aborted: %s (%d)", strsignal(aborted), aborted);
LOG_INFO("aborted: %s (%ld)", strsignal(aborted), (long)aborted);
done:
/* Signal abort to other threads */
@ -367,7 +376,7 @@ done:
int res;
int r = thrd_join(bar_thread, &res);
if (r != 0)
LOG_ERRNO_P("failed to join bar thread", r);
LOG_ERRNO_P(r, "failed to join bar thread");
bar->destroy(bar);
close(abort_fd);

View file

@ -1,20 +1,28 @@
project('yambar', 'c',
version: '1.6.2',
version: '1.11.0',
license: 'MIT',
meson_version: '>=0.53.0',
meson_version: '>=0.60.0',
default_options: ['c_std=c18',
'warning_level=1',
'werror=true',
'b_ndebug=if-release'])
is_debug_build = get_option('buildtype').startswith('debug')
plugs_as_libs = get_option('core-plugins-as-shared-libraries')
cc = meson.get_compiler('c')
cc_flags = [
'-Werror=all'
]
if cc.has_function('memfd_create',
args: ['-D_GNU_SOURCE=200809L'],
prefix: '#include <sys/mman.h>')
add_project_arguments('-DMEMFD_CREATE', language: 'c')
endif
# Compute the relative path used by compiler invocations.
source_root = meson.current_source_dir().split('/')
build_root = meson.build_root().split('/')
build_root = meson.global_build_root().split('/')
relative_dir_parts = []
i = 0
in_prefix = true
@ -43,7 +51,9 @@ endif
# Common dependencies
dl = cc.find_library('dl')
m = cc.find_library('m')
threads = dependency('threads')
threads = [dependency('threads'), cc.find_library('stdthreads', required: false)]
libepoll = dependency('epoll-shim', required: false)
libinotify = dependency('libinotify', required: false)
pixman = dependency('pixman-1')
yaml = dependency('yaml-0.1')
@ -65,9 +75,10 @@ backend_wayland = wayland_client.found() and wayland_cursor.found()
# "My" dependencies, fallback to subproject
tllist = dependency('tllist', version: '>=1.0.1', fallback: 'tllist')
fcft = dependency('fcft', version: ['>=2.4.0', '<3.0.0'], fallback: 'fcft')
fcft = dependency('fcft', version: ['>=3.0.0', '<4.0.0'], fallback: 'fcft')
add_project_arguments(
cc_flags +
['-D_GNU_SOURCE'] +
(is_debug_build ? ['-D_DEBUG'] : []) +
(backend_x11 ? ['-DENABLE_X11'] : []) +
@ -84,30 +95,37 @@ if backend_x11
c_args: xcb_errors.found() ? '-DHAVE_XCB_ERRORS' : [],
pic: plugs_as_libs)
xcb_stuff = declare_dependency(link_with: xcb_stuff_lib)
xcb_stuff = declare_dependency(
link_with: xcb_stuff_lib,
dependencies: [xcb_aux, xcb_cursor, xcb_event, xcb_ewmh, xcb_randr,
xcb_render, xcb_errors],
)
install_headers('xcb.h', subdir: 'yambar')
endif
subdir('completions')
subdir('doc')
subdir('bar')
subdir('decorations')
subdir('particles')
subdir('modules')
subdir('doc')
env = find_program('env', native: true)
generate_version_sh = files('generate-version.sh')
version = custom_target(
'generate_version',
build_always_stale: true,
output: 'version.h',
command: [generate_version_sh, meson.project_version(), '@SOURCE_ROOT@', '@OUTPUT@'])
command: [env, 'LC_ALL=C', generate_version_sh, meson.project_version(), '@CURRENT_SOURCE_DIR@', '@OUTPUT@'])
yambar = executable(
'yambar',
'char32.c', 'char32.h',
'color.h',
'config-verify.c', 'config-verify.h',
'config.c', 'config.h',
'decoration.h',
'font-shaping.h',
'log.c', 'log.h',
'main.c',
'module.c', 'module.h',
@ -116,7 +134,7 @@ yambar = executable(
'tag.c', 'tag.h',
'yml.c', 'yml.h',
version,
dependencies: [bar, pixman, yaml, threads, dl, tllist, fcft] +
dependencies: [bar, libepoll, libinotify, pixman, yaml, threads, dl, tllist, fcft] +
decorations + particles + modules,
build_rpath: '$ORIGIN/modules:$ORIGIN/decorations:$ORIGIN/particles',
export_dynamic: true,
@ -152,3 +170,34 @@ summary(
},
bool_yn: true
)
summary(
{
'ALSA': plugin_alsa_enabled,
'Backlight': plugin_backlight_enabled,
'Battery': plugin_battery_enabled,
'Clock': plugin_clock_enabled,
'CPU monitoring': plugin_cpu_enabled,
'Disk I/O monitoring': plugin_disk_io_enabled,
'dwl (dwm for Wayland)': plugin_dwl_enabled,
'Foreign toplevel (window tracking for Wayland)': plugin_foreign_toplevel_enabled,
'Memory monitoring': plugin_mem_enabled,
'Music Player Daemon (MPD)': plugin_mpd_enabled,
'Media Player Remote Interface Specificaion (MPRIS)': plugin_mpris_enabled,
'i3+Sway': plugin_i3_enabled,
'Label': plugin_label_enabled,
'Network monitoring': plugin_network_enabled,
'Pipewire': plugin_pipewire_enabled,
'PulseAudio': plugin_pulse_enabled,
'Removables monitoring': plugin_removables_enabled,
'River': plugin_river_enabled,
'Script': plugin_script_enabled,
'Sway XKB keyboard': plugin_sway_xkb_enabled,
'Niri language': plugin_niri_language_enabled,
'Niri workspaces': plugin_niri_workspaces_enabled,
'XKB keyboard (for X11)': plugin_xkb_enabled,
'XWindow (window tracking for X11)': plugin_xwindow_enabled,
},
section: 'Optional modules',
bool_yn: true
)

View file

@ -5,3 +5,52 @@ option(
option(
'core-plugins-as-shared-libraries', type: 'boolean', value: false,
description: 'Compiles modules, particles and decorations as shared libraries, which are loaded on-demand')
option('plugin-alsa', type: 'feature', value: 'auto',
description: 'ALSA support')
option('plugin-backlight', type: 'feature', value: 'auto',
description: 'Backlight support')
option('plugin-battery', type: 'feature', value: 'auto',
description: 'Battery support')
option('plugin-clock', type: 'feature', value: 'auto',
description: 'Clock support')
option('plugin-cpu', type: 'feature', value: 'auto',
description: 'CPU monitoring support')
option('plugin-disk-io', type: 'feature', value: 'auto',
description: 'Disk I/O support')
option('plugin-dwl', type: 'feature', value: 'auto',
description: 'dwl (dwm for wayland) support')
option('plugin-foreign-toplevel', type: 'feature', value: 'auto',
description: 'Foreign toplevel (window tracking for Wayland) support')
option('plugin-mem', type: 'feature', value: 'auto',
description: 'Memory monitoring support')
option('plugin-mpd', type: 'feature', value: 'auto',
description: 'Music Player Daemon (MPD) support')
option('plugin-mpris', type: 'feature', value: 'enabled',
description: 'Media Player Remote Interface Specificaion (MPRIS) support')
option('plugin-i3', type: 'feature', value: 'auto',
description: 'i3+Sway support')
option('plugin-label', type: 'feature', value: 'auto',
description: 'Label support')
option('plugin-network', type: 'feature', value: 'auto',
description: 'Network monitoring support')
option('plugin-pipewire', type: 'feature', value: 'auto',
description: 'Pipewire support')
option('plugin-pulse', type: 'feature', value: 'auto',
description: 'PulseAudio support')
option('plugin-removables', type: 'feature', value: 'auto',
description: 'Removables (USB sticks, CD-ROM etc) monitoring support')
option('plugin-river', type: 'feature', value: 'auto',
description: 'River support')
option('plugin-script', type: 'feature', value: 'auto',
description: 'Script support')
option('plugin-sway-xkb', type: 'feature', value: 'auto',
description: 'keyboard support for Sway')
option('plugin-niri-language', type: 'feature', value: 'auto',
description: 'language support for Niri')
option('plugin-niri-workspaces', type: 'feature', value: 'auto',
description: 'workspaces support for Niri')
option('plugin-xkb', type: 'feature', value: 'auto',
description: 'keyboard support for X11')
option('plugin-xwindow', type: 'feature', value: 'auto',
description: 'XWindow (window tracking for X11) support')

View file

@ -1,6 +1,6 @@
#include "module.h"
#include <stdlib.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
struct module *

View file

@ -27,7 +27,7 @@ struct module {
* specified number of milliseconds */
bool (*refresh_in)(struct module *mod, long milli_seconds);
const char *(*description)(struct module *mod);
const char *(*description)(const struct module *mod);
};
struct module *module_common_new(void);
@ -35,9 +35,9 @@ void module_default_destroy(struct module *mod);
struct exposable *module_begin_expose(struct module *mod);
/* List of attributes *all* modules implement */
#define MODULE_COMMON_ATTRS \
{"content", true, &conf_verify_particle}, \
{"anchors", false, NULL}, \
{"font", false, &conf_verify_font}, \
{"foreground", false, &conf_verify_color}, \
{NULL, false, NULL}
#define MODULE_COMMON_ATTRS \
{"content", true, &conf_verify_particle}, {"anchors", false, NULL}, {"font", false, &conf_verify_font}, \
{"foreground", false, &conf_verify_color}, \
{ \
NULL, false, NULL \
}

View file

@ -1,6 +1,8 @@
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <sys/inotify.h>
#include <sys/time.h>
#include <alsa/asoundlib.h>
@ -8,42 +10,86 @@
#define LOG_MODULE "alsa"
#define LOG_ENABLE_DBG 0
#include "../log.h"
#include "../bar/bar.h"
#include "../config-verify.h"
#include "../config.h"
#include "../log.h"
#include "../plugin.h"
struct private {
char *card;
char *mixer;
struct particle *label;
enum channel_type { CHANNEL_PLAYBACK, CHANNEL_CAPTURE };
tll(snd_mixer_selem_channel_id_t) channels;
struct channel {
snd_mixer_selem_channel_id_t id;
enum channel_type type;
char *name;
long vol_min;
long vol_max;
bool use_db;
long vol_cur;
long db_cur;
bool muted;
};
struct private
{
char *card;
char *mixer;
char *volume_name;
char *muted_name;
struct particle *label;
tll(struct channel) channels;
bool online;
bool has_playback_volume;
long playback_vol_min;
long playback_vol_max;
bool has_playback_db;
long playback_db_min;
long playback_db_max;
bool has_capture_volume;
long capture_vol_min;
long capture_vol_max;
long has_capture_db;
long capture_db_min;
long capture_db_max;
const struct channel *volume_chan;
const struct channel *muted_chan;
};
static void
channel_free(struct channel *chan)
{
free(chan->name);
}
static void
destroy(struct module *mod)
{
struct private *m = mod->private;
tll_free(m->channels);
tll_foreach(m->channels, it)
{
channel_free(&it->item);
tll_remove(m->channels, it);
}
m->label->destroy(m->label);
free(m->card);
free(m->mixer);
free(m->volume_name);
free(m->muted_name);
free(m);
module_default_destroy(mod);
}
static const char *
description(struct module *mod)
description(const struct module *mod)
{
static char desc[32];
struct private *m = mod->private;
const struct private *m = mod->private;
snprintf(desc, sizeof(desc), "alsa(%s)", m->card);
return desc;
}
@ -53,18 +99,60 @@ content(struct module *mod)
{
struct private *m = mod->private;
int percent = m->vol_max - m->vol_min > 0
? round(100. * m->vol_cur / (m->vol_max - m->vol_min))
: 0;
mtx_lock(&mod->lock);
const struct channel *volume_chan = m->volume_chan;
const struct channel *muted_chan = m->muted_chan;
bool muted = muted_chan != NULL ? muted_chan->muted : false;
long vol_min = 0, vol_max = 0, vol_cur = 0;
long db_min = 0, db_max = 0, db_cur = 0;
bool use_db = false;
if (volume_chan != NULL) {
if (volume_chan->type == CHANNEL_PLAYBACK) {
db_min = m->playback_db_min;
db_max = m->playback_db_max;
vol_min = m->playback_vol_min;
vol_max = m->playback_vol_max;
} else {
db_min = m->capture_db_min;
db_max = m->capture_db_max;
vol_min = m->capture_vol_min;
vol_max = m->capture_vol_max;
}
vol_cur = volume_chan->vol_cur;
db_cur = volume_chan->db_cur;
use_db = volume_chan->use_db;
}
int percent;
if (use_db) {
bool use_linear = db_max - db_min <= 24 * 100;
if (use_linear) {
percent = db_min - db_max > 0 ? round(100. * (db_cur - db_min) / (db_max - db_min)) : 0;
} else {
double normalized = pow(10, (double)(db_cur - db_max) / 6000.);
if (db_min != SND_CTL_TLV_DB_GAIN_MUTE) {
double min_norm = pow(10, (double)(db_min - db_max) / 6000.);
normalized = (normalized - min_norm) / (1. - min_norm);
}
percent = round(100. * normalized);
}
} else {
percent = vol_max - vol_min > 0 ? round(100. * (vol_cur - vol_min) / (vol_max - vol_min)) : 0;
}
struct tag_set tags = {
.tags = (struct tag *[]){
tag_new_int_range(mod, "volume", m->vol_cur, m->vol_min, m->vol_max),
tag_new_bool(mod, "online", m->online),
tag_new_int_range(mod, "volume", vol_cur, vol_min, vol_max),
tag_new_int_range(mod, "dB", db_cur, db_min, db_max),
tag_new_int_range(mod, "percent", percent, 0, 100),
tag_new_bool(mod, "muted", m->muted),
tag_new_bool(mod, "muted", muted),
},
.count = 3,
.count = 5,
};
mtx_unlock(&mod->lock);
@ -79,136 +167,137 @@ update_state(struct module *mod, snd_mixer_elem_t *elem)
{
struct private *m = mod->private;
int idx = 0;
/* Get min/max volume levels */
long min = 0, max = 0;
int r = snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
if (r < 0) {
LOG_DBG("%s,%s: failed to get volume min/max (mixer is digital?)",
m->card, m->mixer);
}
/* Make sure min <= max */
if (min > max) {
LOG_WARN(
"%s,%s: indicated minimum volume is greater than the maximum: "
"%ld > %ld", m->card, m->mixer, min, max);
min = max;
}
long cur[tll_length(m->channels)];
memset(cur, 0, sizeof(cur));
mtx_lock(&mod->lock);
/* If volume level can be changed (i.e. this isn't just a switch;
* e.g. a digital channel), get current level */
if (max > 0) {
tll_foreach(m->channels, it) {
int r = snd_mixer_selem_get_playback_volume(
elem, it->item, &cur[idx]);
* e.g. a digital channel), get current channel levels */
tll_foreach(m->channels, it)
{
struct channel *chan = &it->item;
const bool has_volume = chan->type == CHANNEL_PLAYBACK ? m->has_playback_volume : m->has_capture_volume;
const bool has_db = chan->type == CHANNEL_PLAYBACK ? m->has_playback_db : m->has_capture_db;
if (!has_volume && !has_db)
continue;
if (has_db) {
chan->use_db = true;
const long min = chan->type == CHANNEL_PLAYBACK ? m->playback_db_min : m->capture_db_min;
const long max = chan->type == CHANNEL_PLAYBACK ? m->playback_db_max : m->capture_db_max;
assert(min <= max);
int r = chan->type == CHANNEL_PLAYBACK ? snd_mixer_selem_get_playback_dB(elem, chan->id, &chan->db_cur)
: snd_mixer_selem_get_capture_dB(elem, chan->id, &chan->db_cur);
if (r < 0) {
LOG_WARN("%s,%s: %s: failed to get current volume",
m->card, m->mixer,
snd_mixer_selem_channel_name(it->item));
LOG_ERR("%s,%s: %s: failed to get current dB", m->card, m->mixer, chan->name);
}
LOG_DBG("%s,%s: %s: volume: %ld", m->card, m->mixer,
snd_mixer_selem_channel_name(it->item), cur[idx]);
idx++;
}
}
if (chan->db_cur < min) {
LOG_WARN("%s,%s: %s: current dB is less than the indicated minimum: "
"%ld < %ld",
m->card, m->mixer, chan->name, chan->db_cur, min);
chan->db_cur = min;
}
int unmuted[tll_length(m->channels)];
memset(unmuted, 0, sizeof(unmuted));
if (chan->db_cur > max) {
LOG_WARN("%s,%s: %s: current dB is greater than the indicated maximum: "
"%ld > %ld",
m->card, m->mixer, chan->name, chan->db_cur, max);
chan->db_cur = max;
}
/* Get muted state */
idx = 0;
tll_foreach(m->channels, it) {
int r = snd_mixer_selem_get_playback_switch(
elem, it->item, &unmuted[idx]);
assert(chan->db_cur >= min);
assert(chan->db_cur <= max);
LOG_DBG("%s,%s: %s: dB: %ld", m->card, m->mixer, chan->name, chan->db_cur);
} else
chan->use_db = false;
const long min = chan->type == CHANNEL_PLAYBACK ? m->playback_vol_min : m->capture_vol_min;
const long max = chan->type == CHANNEL_PLAYBACK ? m->playback_vol_max : m->capture_vol_max;
assert(min <= max);
int r = chan->type == CHANNEL_PLAYBACK ? snd_mixer_selem_get_playback_volume(elem, chan->id, &chan->vol_cur)
: snd_mixer_selem_get_capture_volume(elem, chan->id, &chan->vol_cur);
if (r < 0) {
LOG_WARN("%s,%s: %s: failed to get muted state",
m->card, m->mixer, snd_mixer_selem_channel_name(it->item));
unmuted[idx] = 1;
LOG_ERR("%s,%s: %s: failed to get current volume", m->card, m->mixer, chan->name);
}
LOG_DBG("%s,%s: %s: muted: %d", m->card, m->mixer,
snd_mixer_selem_channel_name(it->item), !unmuted[idx]);
idx++;
}
/* Warn if volume level is inconsistent across the channels */
for (size_t i = 1; i < tll_length(m->channels); i++) {
if (cur[i] != cur[i - 1]) {
LOG_WARN("%s,%s: channel volume mismatch, using value from %s",
m->card, m->mixer,
snd_mixer_selem_channel_name(tll_front(m->channels)));
break;
if (chan->vol_cur < min) {
LOG_WARN("%s,%s: %s: current volume is less than the indicated minimum: "
"%ld < %ld",
m->card, m->mixer, chan->name, chan->vol_cur, min);
chan->vol_cur = min;
}
}
/* Warn if muted state is inconsistent across the channels */
for (size_t i = 1; i < tll_length(m->channels); i++) {
if (unmuted[i] != unmuted[i - 1]) {
LOG_WARN("%s,%s: channel muted mismatch, using value from %s",
m->card, m->mixer,
snd_mixer_selem_channel_name(tll_front(m->channels)));
break;
if (chan->vol_cur > max) {
LOG_WARN("%s,%s: %s: current volume is greater than the indicated maximum: "
"%ld > %ld",
m->card, m->mixer, chan->name, chan->vol_cur, max);
chan->vol_cur = max;
}
assert(chan->vol_cur >= min);
assert(chan->vol_cur <= max);
LOG_DBG("%s,%s: %s: volume: %ld", m->card, m->mixer, chan->name, chan->vol_cur);
}
/* Make sure min <= cur <= max */
if (cur[0] < min) {
LOG_WARN(
"%s,%s: current volume is less than the indicated minimum: "
"%ld < %ld", m->card, m->mixer, cur[0], min);
cur[0] = min;
/* Get channels muted state */
tll_foreach(m->channels, it)
{
struct channel *chan = &it->item;
int unmuted;
int r = chan->type == CHANNEL_PLAYBACK ? snd_mixer_selem_get_playback_switch(elem, chan->id, &unmuted)
: snd_mixer_selem_get_capture_switch(elem, chan->id, &unmuted);
if (r < 0) {
LOG_WARN("%s,%s: %s: failed to get muted state", m->card, m->mixer, chan->name);
unmuted = 1;
}
chan->muted = !unmuted;
LOG_DBG("%s,%s: %s: muted: %d", m->card, m->mixer, chan->name, !unmuted);
}
if (cur[0] > max) {
LOG_WARN(
"%s,%s: current volume is greater than the indicated maximum: "
"%ld > %ld", m->card, m->mixer, cur[0], max);
cur[0] = max;
}
m->online = true;
assert(cur[0] >= min);
assert(cur[0] <= max);
LOG_DBG(
"muted=%d, cur=%ld, min=%ld, max=%ld", !unmuted[0], cur[0], min, max);
mtx_lock(&mod->lock);
m->vol_min = min;
m->vol_max = max;
m->vol_cur = cur[0];
m->muted = !unmuted[0];
mtx_unlock(&mod->lock);
mod->bar->refresh(mod->bar);
}
static int
run(struct module *mod)
enum run_state {
RUN_ERROR,
RUN_FAILED_CONNECT,
RUN_DISCONNECTED,
RUN_DONE,
};
static enum run_state
run_while_online(struct module *mod)
{
struct private *m = mod->private;
int ret = 1;
enum run_state ret = RUN_ERROR;
/* Make sure we arent still tracking channels from previous connects */
tll_free(m->channels);
snd_mixer_t *handle;
if (snd_mixer_open(&handle, 0) != 0) {
LOG_ERR("failed to open handle");
return 1;
return ret;
}
if (snd_mixer_attach(handle, m->card) != 0 ||
snd_mixer_selem_register(handle, NULL, NULL) != 0 ||
snd_mixer_load(handle) != 0)
{
if (snd_mixer_attach(handle, m->card) != 0 || snd_mixer_selem_register(handle, NULL, NULL) != 0
|| snd_mixer_load(handle) != 0) {
LOG_ERR("failed to attach to card");
ret = RUN_FAILED_CONNECT;
goto err;
}
@ -217,37 +306,142 @@ run(struct module *mod)
snd_mixer_selem_id_set_index(sid, 0);
snd_mixer_selem_id_set_name(sid, m->mixer);
snd_mixer_elem_t* elem = snd_mixer_find_selem(handle, sid);
snd_mixer_elem_t *elem = snd_mixer_find_selem(handle, sid);
if (elem == NULL) {
LOG_ERR("failed to find mixer");
goto err;
}
/* Get playback volume range */
m->has_playback_volume = snd_mixer_selem_has_playback_volume(elem) > 0;
if (m->has_playback_volume) {
if (snd_mixer_selem_get_playback_volume_range(elem, &m->playback_vol_min, &m->playback_vol_max) < 0) {
LOG_ERR("%s,%s: failed to get playback volume range", m->card, m->mixer);
assert(m->playback_vol_min == 0);
assert(m->playback_vol_max == 0);
}
if (m->playback_vol_min > m->playback_vol_max) {
LOG_WARN("%s,%s: indicated minimum playback volume is greater than the "
"maximum: %ld > %ld",
m->card, m->mixer, m->playback_vol_min, m->playback_vol_max);
m->playback_vol_min = m->playback_vol_max;
}
}
if (snd_mixer_selem_get_playback_dB_range(elem, &m->playback_db_min, &m->playback_db_max) < 0) {
LOG_WARN("%s,%s: failed to get playback dB range, "
"will use raw volume values instead",
m->card, m->mixer);
m->has_playback_db = false;
} else
m->has_playback_db = true;
/* Get capture volume range */
m->has_capture_volume = snd_mixer_selem_has_capture_volume(elem) > 0;
if (m->has_capture_volume) {
if (snd_mixer_selem_get_capture_volume_range(elem, &m->capture_vol_min, &m->capture_vol_max) < 0) {
LOG_ERR("%s,%s: failed to get capture volume range", m->card, m->mixer);
assert(m->capture_vol_min == 0);
assert(m->capture_vol_max == 0);
}
if (m->capture_vol_min > m->capture_vol_max) {
LOG_WARN("%s,%s: indicated minimum capture volume is greater than the "
"maximum: %ld > %ld",
m->card, m->mixer, m->capture_vol_min, m->capture_vol_max);
m->capture_vol_min = m->capture_vol_max;
}
}
if (snd_mixer_selem_get_capture_dB_range(elem, &m->capture_db_min, &m->capture_db_max) < 0) {
LOG_WARN("%s,%s: failed to get capture dB range, "
"will use raw volume values instead",
m->card, m->mixer);
m->has_capture_db = false;
} else
m->has_capture_db = true;
/* Get available channels */
for (size_t i = 0; i < SND_MIXER_SCHN_LAST; i++) {
if (snd_mixer_selem_has_playback_channel(elem, i)) {
tll_push_back(m->channels, i);
bool is_playback = snd_mixer_selem_has_playback_channel(elem, i) == 1;
bool is_capture = snd_mixer_selem_has_capture_channel(elem, i) == 1;
if (is_playback || is_capture) {
struct channel chan = {
.id = i,
.type = is_playback ? CHANNEL_PLAYBACK : CHANNEL_CAPTURE,
.name = strdup(snd_mixer_selem_channel_name(i)),
};
tll_push_back(m->channels, chan);
}
}
if (tll_length(m->channels) == 0) {
LOG_ERR("%s,%s: no channels", m->card, m->mixer);
goto err;
}
char channels_str[1024];
int channels_idx = 0;
tll_foreach(m->channels, it) {
channels_idx += snprintf(
&channels_str[channels_idx], sizeof(channels_str) - channels_idx,
channels_idx == 0 ? "%s" : ", %s",
snd_mixer_selem_channel_name(it->item));
tll_foreach(m->channels, it)
{
const struct channel *chan = &it->item;
channels_idx += snprintf(&channels_str[channels_idx], sizeof(channels_str) - channels_idx,
channels_idx == 0 ? "%s (%s)" : ", %s (%s)", chan->name,
chan->type == CHANNEL_PLAYBACK ? "🔊" : "🎤");
assert(channels_idx <= sizeof(channels_str));
}
LOG_INFO("%s,%s: channels: %s", m->card, m->mixer, channels_str);
/* Verify volume/muted channel names are valid and exists */
bool volume_channel_is_valid = m->volume_name == NULL;
bool muted_channel_is_valid = m->muted_name == NULL;
tll_foreach(m->channels, it)
{
const struct channel *chan = &it->item;
if (m->volume_name != NULL && strcmp(chan->name, m->volume_name) == 0) {
m->volume_chan = chan;
volume_channel_is_valid = true;
}
if (m->muted_name != NULL && strcmp(chan->name, m->muted_name) == 0) {
m->muted_chan = chan;
muted_channel_is_valid = true;
}
}
if (m->volume_name == NULL)
m->volume_chan = &tll_front(m->channels);
if (m->muted_name == NULL)
m->muted_chan = &tll_front(m->channels);
if (!volume_channel_is_valid) {
assert(m->volume_name != NULL);
LOG_ERR("volume: invalid channel name: %s", m->volume_name);
goto err;
}
if (!muted_channel_is_valid) {
assert(m->muted_name != NULL);
LOG_ERR("muted: invalid channel name: %s", m->muted_name);
goto err;
}
/* Initial state */
update_state(mod, elem);
LOG_INFO("%s,%s: volume min=%ld, max=%ld, current=%ld%s",
m->card, m->mixer, m->vol_min, m->vol_max, m->vol_cur,
m->muted ? ", muted" : "");
LOG_INFO(
"%s,%s: %s range=%ld-%ld, current=%ld%s (sources: volume=%s, muted=%s)", m->card, m->mixer,
m->volume_chan->use_db ? "dB" : "volume",
(m->volume_chan->type == CHANNEL_PLAYBACK ? (m->volume_chan->use_db ? m->playback_db_min : m->playback_vol_min)
: (m->volume_chan->use_db ? m->capture_db_min : m->capture_vol_min)),
(m->volume_chan->type == CHANNEL_PLAYBACK ? (m->volume_chan->use_db ? m->playback_db_max : m->playback_vol_max)
: (m->volume_chan->use_db ? m->capture_db_max : m->capture_vol_max)),
m->volume_chan->use_db ? m->volume_chan->db_cur : m->volume_chan->vol_cur,
m->muted_chan->muted ? " (muted)" : "", m->volume_chan->name, m->muted_chan->name);
mod->bar->refresh(mod->bar);
@ -260,36 +454,181 @@ run(struct module *mod)
fds[0] = (struct pollfd){.fd = mod->abort_fd, .events = POLLIN};
snd_mixer_poll_descriptors(handle, &fds[1], fd_count);
poll(fds, fd_count + 1, -1);
int r = poll(fds, fd_count + 1, -1);
if (r < 0) {
if (errno == EINTR)
continue;
if (fds[0].revents & POLLIN)
LOG_ERRNO("failed to poll");
break;
}
if (fds[1].revents & POLLHUP) {
/* Don't know if this can happen */
LOG_ERR("disconnected from alsa");
if (fds[0].revents & POLLIN) {
ret = RUN_DONE;
break;
}
for (size_t i = 0; i < fd_count; i++) {
if (fds[1 + i].revents & (POLLHUP | POLLERR | POLLNVAL)) {
LOG_ERR("disconnected from alsa");
mtx_lock(&mod->lock);
m->online = false;
mtx_unlock(&mod->lock);
mod->bar->refresh(mod->bar);
ret = RUN_DISCONNECTED;
goto err;
}
}
snd_mixer_handle_events(handle);
update_state(mod, elem);
}
ret = 0;
err:
snd_mixer_close(handle);
snd_config_update_free_global();
return ret;
}
static int
run(struct module *mod)
{
int ret = 1;
int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
if (ifd < 0) {
LOG_ERRNO("failed to inotify");
return 1;
}
int wd = inotify_add_watch(ifd, "/dev/snd", IN_CREATE);
if (wd < 0) {
LOG_ERRNO("failed to create inotify watcher for /dev/snd");
close(ifd);
return 1;
}
while (true) {
enum run_state state = run_while_online(mod);
switch (state) {
case RUN_DONE:
ret = 0;
goto out;
case RUN_ERROR:
ret = 1;
goto out;
case RUN_FAILED_CONNECT:
break;
case RUN_DISCONNECTED:
/*
* Weve been connected - drain the watcher
*
* We dont want old, un-releated events (for other
* soundcards, for example) to trigger a storm of
* re-connect attempts.
*/
while (true) {
uint8_t buf[1024];
ssize_t amount = read(ifd, buf, sizeof(buf));
if (amount < 0) {
if (errno == EAGAIN)
break;
LOG_ERRNO("failed to drain inotify watcher");
ret = 1;
goto out;
}
if (amount == 0)
break;
}
break;
}
bool have_create_event = false;
while (!have_create_event) {
struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}, {.fd = ifd, .events = POLLIN}};
int r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1);
if (r < 0) {
if (errno == EINTR)
continue;
LOG_ERRNO("failed to poll");
ret = 1;
goto out;
}
if (fds[0].revents & (POLLIN | POLLHUP)) {
ret = 0;
goto out;
}
if (fds[1].revents & POLLHUP) {
LOG_ERR("inotify socket closed");
ret = 1;
goto out;
}
assert(fds[1].revents & POLLIN);
while (true) {
char buf[1024];
ssize_t len = read(ifd, buf, sizeof(buf));
if (len < 0) {
if (errno == EAGAIN)
break;
LOG_ERRNO("failed to read inotify events");
ret = 1;
goto out;
}
if (len == 0)
break;
/* Consume inotify data */
for (const char *ptr = buf; ptr < buf + len;) {
const struct inotify_event *e = (const struct inotify_event *)ptr;
if (e->mask & IN_CREATE) {
LOG_DBG("inotify: CREATED: /dev/snd/%.*s", e->len, e->name);
have_create_event = true;
}
ptr += sizeof(*e) + e->len;
}
}
}
}
out:
if (wd >= 0)
inotify_rm_watch(ifd, wd);
if (ifd >= 0)
close(ifd);
return ret;
}
static struct module *
alsa_new(const char *card, const char *mixer, struct particle *label)
alsa_new(const char *card, const char *mixer, const char *volume_channel_name, const char *muted_channel_name,
struct particle *label)
{
struct private *priv = calloc(1, sizeof(*priv));
priv->label = label;
priv->card = strdup(card);
priv->mixer = strdup(mixer);
priv->volume_name = volume_channel_name != NULL ? strdup(volume_channel_name) : NULL;
priv->muted_name = muted_channel_name != NULL ? strdup(muted_channel_name) : NULL;
struct module *mod = module_common_new();
mod->private = priv;
@ -305,12 +644,13 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited)
{
const struct yml_node *card = yml_get_value(node, "card");
const struct yml_node *mixer = yml_get_value(node, "mixer");
const struct yml_node *volume = yml_get_value(node, "volume");
const struct yml_node *muted = yml_get_value(node, "muted");
const struct yml_node *content = yml_get_value(node, "content");
return alsa_new(
yml_value_as_string(card),
yml_value_as_string(mixer),
conf_to_particle(content, inherited));
return alsa_new(yml_value_as_string(card), yml_value_as_string(mixer),
volume != NULL ? yml_value_as_string(volume) : NULL,
muted != NULL ? yml_value_as_string(muted) : NULL, conf_to_particle(content, inherited));
}
static bool
@ -319,6 +659,8 @@ verify_conf(keychain_t *chain, const struct yml_node *node)
static const struct attr_info attrs[] = {
{"card", true, &conf_verify_string},
{"mixer", true, &conf_verify_string},
{"volume", false, &conf_verify_string},
{"muted", false, &conf_verify_string},
MODULE_COMMON_ATTRS,
};

View file

@ -1,24 +1,26 @@
#include <assert.h>
#include <errno.h>
#include <math.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <assert.h>
#include <unistd.h>
#include <poll.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <libudev.h>
#define LOG_MODULE "backlight"
#include "../log.h"
#include "../bar/bar.h"
#include "../config.h"
#include "../config-verify.h"
#include "../config.h"
#include "../log.h"
#include "../plugin.h"
struct private {
struct private
{
struct particle *label;
char *device;
@ -39,7 +41,7 @@ destroy(struct module *mod)
}
static const char *
description(struct module *mod)
description(const struct module *mod)
{
return "backlight";
}
@ -110,13 +112,13 @@ readint_from_fd(int fd)
static int
initialize(struct private *m)
{
int backlight_fd = open("/sys/class/backlight", O_RDONLY);
int backlight_fd = open("/sys/class/backlight", O_RDONLY | O_CLOEXEC);
if (backlight_fd == -1) {
LOG_ERRNO("/sys/class/backlight");
return -1;
}
int base_dir_fd = openat(backlight_fd, m->device, O_RDONLY);
int base_dir_fd = openat(backlight_fd, m->device, O_RDONLY | O_CLOEXEC);
close(backlight_fd);
if (base_dir_fd == -1) {
@ -124,7 +126,7 @@ initialize(struct private *m)
return -1;
}
int max_fd = openat(base_dir_fd, "max_brightness", O_RDONLY);
int max_fd = openat(base_dir_fd, "max_brightness", O_RDONLY | O_CLOEXEC);
if (max_fd == -1) {
LOG_ERRNO("/sys/class/backlight/%s/max_brightness", m->device);
close(base_dir_fd);
@ -134,7 +136,7 @@ initialize(struct private *m)
m->max_brightness = readint_from_fd(max_fd);
close(max_fd);
int current_fd = openat(base_dir_fd, "brightness", O_RDONLY);
int current_fd = openat(base_dir_fd, "brightness", O_RDONLY | O_CLOEXEC);
close(base_dir_fd);
if (current_fd == -1) {
@ -144,8 +146,7 @@ initialize(struct private *m)
m->current_brightness = readint_from_fd(current_fd);
LOG_INFO("%s: brightness: %ld (max: %ld)", m->device, m->current_brightness,
m->max_brightness);
LOG_INFO("%s: brightness: %ld (max: %ld)", m->device, m->current_brightness, m->max_brightness);
return current_fd;
}
@ -178,20 +179,31 @@ run(struct module *mod)
bar->refresh(bar);
int ret = 1;
while (true) {
struct pollfd fds[] = {
{.fd = mod->abort_fd, .events = POLLIN},
{.fd = udev_monitor_get_fd(mon), .events = POLLIN},
};
poll(fds, 2, -1);
if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) < 0) {
if (errno == EINTR)
continue;
if (fds[0].revents & POLLIN)
LOG_ERRNO("failed to poll");
break;
}
if (fds[0].revents & POLLIN) {
ret = 0;
break;
}
struct udev_device *dev = udev_monitor_receive_device(mon);
const char *sysname = udev_device_get_sysname(dev);
if (dev == NULL)
continue;
bool is_us = strcmp(sysname, m->device) == 0;
const char *sysname = udev_device_get_sysname(dev);
bool is_us = sysname != NULL && strcmp(sysname, m->device) == 0;
udev_device_unref(dev);
if (!is_us)
@ -207,7 +219,7 @@ run(struct module *mod)
udev_unref(udev);
close(current_fd);
return 0;
return ret;
}
static struct module *
@ -232,8 +244,7 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited)
const struct yml_node *name = yml_get_value(node, "name");
const struct yml_node *c = yml_get_value(node, "content");
return backlight_new(
yml_value_as_string(name), conf_to_particle(c, inherited));
return backlight_new(yml_value_as_string(name), conf_to_particle(c, inherited));
}
static bool

View file

@ -1,31 +1,49 @@
#include <assert.h>
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <assert.h>
#include <errno.h>
#include <poll.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <libudev.h>
#include <tllist.h>
#define LOG_MODULE "battery"
#define LOG_ENABLE_DBG 0
#include "../log.h"
#include "../bar/bar.h"
#include "../config.h"
#include "../config-verify.h"
#include "../config.h"
#include "../log.h"
#include "../plugin.h"
enum state { STATE_FULL, STATE_NOTCHARGING, STATE_CHARGING, STATE_DISCHARGING };
#define max(x, y) ((x) > (y) ? (x) : (y))
struct private {
static const long min_poll_interval = 250;
static const long default_poll_interval = 60 * 1000;
static const long one_sec_in_ns = 1000000000;
enum state { STATE_FULL, STATE_NOTCHARGING, STATE_CHARGING, STATE_DISCHARGING, STATE_UNKNOWN };
struct current_state {
long ema;
long current;
struct timespec time;
};
struct private
{
struct particle *label;
int poll_interval;
long poll_interval;
int battery_scale;
long smoothing_scale;
char *battery;
char *manufacturer;
char *model;
@ -39,10 +57,64 @@ struct private {
long energy;
long power;
long charge;
long current;
struct current_state ema_current;
long time_to_empty;
long time_to_full;
};
static int64_t
difftimespec_ns(const struct timespec after, const struct timespec before)
{
return ((int64_t)after.tv_sec - (int64_t)before.tv_sec) * (int64_t)one_sec_in_ns
+ ((int64_t)after.tv_nsec - (int64_t)before.tv_nsec);
}
// Linear Exponential Moving Average (unevenly spaced time series)
// http://www.eckner.com/papers/Algorithms%20for%20Unevenly%20Spaced%20Time%20Series.pdf
// Adapted from: https://github.com/andreas50/utsAlgorithms/blob/master/ema.c
static void
ema_linear(struct current_state *state, struct current_state curr, long tau)
{
double w, w2, tmp;
if (state->current == -1) {
*state = curr;
return;
}
long time = difftimespec_ns(curr.time, state->time);
tmp = time / (double)tau;
w = exp(-tmp);
if (tmp > 1e-6) {
w2 = (1 - w) / tmp;
} else {
// Use taylor expansion for numerical stability
w2 = 1 - tmp / 2 + tmp * tmp / 6 - tmp * tmp * tmp / 24;
}
double ema = state->ema * w + curr.current * (1 - w2) + state->current * (w2 - w);
state->ema = ema;
state->current = curr.current;
state->time = curr.time;
LOG_DBG("ema current: %ld", (long)ema);
}
static void
timespec_sub(const struct timespec *a, const struct timespec *b, struct timespec *res)
{
res->tv_sec = a->tv_sec - b->tv_sec;
res->tv_nsec = a->tv_nsec - b->tv_nsec;
/* tv_nsec may be negative */
if (res->tv_nsec < 0) {
res->tv_sec--;
res->tv_nsec += one_sec_in_ns;
}
}
static void
destroy(struct module *mod)
{
@ -58,10 +130,10 @@ destroy(struct module *mod)
}
static const char *
description(struct module *mod)
description(const struct module *mod)
{
static char desc[32];
struct private *m = mod->private;
const struct private *m = mod->private;
snprintf(desc, sizeof(desc), "bat(%s)", m->battery);
return desc;
}
@ -73,20 +145,22 @@ content(struct module *mod)
mtx_lock(&mod->lock);
assert(m->state == STATE_FULL ||
m->state == STATE_NOTCHARGING ||
m->state == STATE_CHARGING ||
m->state == STATE_DISCHARGING);
assert(m->state == STATE_FULL || m->state == STATE_NOTCHARGING || m->state == STATE_CHARGING
|| m->state == STATE_DISCHARGING || m->state == STATE_UNKNOWN);
unsigned long hours;
unsigned long minutes;
if (m->time_to_empty >= 0) {
hours = m->time_to_empty / 60;
minutes = m->time_to_empty % 60;
} else if (m->energy_full >= 0 && m->charge && m->power >= 0) {
unsigned long energy = m->state == STATE_CHARGING
? m->energy_full - m->energy : m->energy;
if (m->time_to_empty > 0) {
minutes = m->time_to_empty / 60;
hours = minutes / 60;
minutes = minutes % 60;
} else if (m->time_to_full > 0) {
minutes = m->time_to_full / 60;
hours = minutes / 60;
minutes = minutes % 60;
} else if (m->energy_full >= 0 && m->charge && m->power >= 0) {
unsigned long energy = m->state == STATE_CHARGING ? m->energy_full - m->energy : m->energy;
double hours_as_float;
if (m->state == STATE_FULL || m->state == STATE_NOTCHARGING)
@ -98,15 +172,14 @@ content(struct module *mod)
hours = hours_as_float;
minutes = (hours_as_float - (double)hours) * 60;
} else if (m->charge_full >= 0 && m->charge >= 0 && m->current >= 0) {
unsigned long charge = m->state == STATE_CHARGING
? m->charge_full - m->charge : m->charge;
} else if (m->charge_full >= 0 && m->charge >= 0 && m->ema_current.current >= 0) {
unsigned long charge = m->state == STATE_CHARGING ? m->charge_full - m->charge : m->charge;
double hours_as_float;
if (m->state == STATE_FULL || m->state == STATE_NOTCHARGING)
hours_as_float = 0.0;
else if (m->current > 0)
hours_as_float = (double)charge / m->current;
else if (m->ema_current.current > 0)
hours_as_float = (double)charge / m->ema_current.current;
else
hours_as_float = 99.0;
@ -146,20 +219,18 @@ content(struct module *mod)
}
static const char *
readline_from_fd(int fd)
readline_from_fd(int fd, size_t sz, char buf[static sz])
{
static char buf[4096];
ssize_t sz = read(fd, buf, sizeof(buf) - 1);
ssize_t bytes = read(fd, buf, sz - 1);
lseek(fd, 0, SEEK_SET);
if (sz < 0) {
if (bytes < 0) {
LOG_WARN("failed to read from FD=%d", fd);
return NULL;
}
buf[sz] = '\0';
for (ssize_t i = sz - 1; i >= 0 && buf[i] == '\n'; sz--)
buf[bytes] = '\0';
for (ssize_t i = bytes - 1; i >= 0 && buf[i] == '\n'; bytes--)
buf[i] = '\0';
return buf;
@ -168,7 +239,8 @@ readline_from_fd(int fd)
static long
readint_from_fd(int fd)
{
const char *s = readline_from_fd(fd);
char buf[512];
const char *s = readline_from_fd(fd, sizeof(buf), buf);
if (s == NULL)
return 0;
@ -185,13 +257,15 @@ readint_from_fd(int fd)
static bool
initialize(struct private *m)
{
int pw_fd = open("/sys/class/power_supply", O_RDONLY);
char line_buf[512];
int pw_fd = open("/sys/class/power_supply", O_RDONLY | O_CLOEXEC);
if (pw_fd < 0) {
LOG_ERRNO("/sys/class/power_supply");
return false;
}
int base_dir_fd = openat(pw_fd, m->battery, O_RDONLY);
int base_dir_fd = openat(pw_fd, m->battery, O_RDONLY | O_CLOEXEC);
close(pw_fd);
if (base_dir_fd < 0) {
@ -200,34 +274,31 @@ initialize(struct private *m)
}
{
int fd = openat(base_dir_fd, "manufacturer", O_RDONLY);
int fd = openat(base_dir_fd, "manufacturer", O_RDONLY | O_CLOEXEC);
if (fd == -1) {
LOG_WARN("/sys/class/power_supply/%s/manufacturer: %s",
m->battery, strerror(errno));
LOG_WARN("/sys/class/power_supply/%s/manufacturer: %s", m->battery, strerror(errno));
m->manufacturer = NULL;
} else {
m->manufacturer = strdup(readline_from_fd(fd));
m->manufacturer = strdup(readline_from_fd(fd, sizeof(line_buf), line_buf));
close(fd);
}
}
{
int fd = openat(base_dir_fd, "model_name", O_RDONLY);
int fd = openat(base_dir_fd, "model_name", O_RDONLY | O_CLOEXEC);
if (fd == -1) {
LOG_WARN("/sys/class/power_supply/%s/model_name: %s",
m->battery, strerror(errno));
LOG_WARN("/sys/class/power_supply/%s/model_name: %s", m->battery, strerror(errno));
m->model = NULL;
} else {
m->model = strdup(readline_from_fd(fd));
m->model = strdup(readline_from_fd(fd, sizeof(line_buf), line_buf));
close(fd);
}
}
if (faccessat(base_dir_fd, "energy_full_design", O_RDONLY, 0) == 0 &&
faccessat(base_dir_fd, "energy_full", O_RDONLY, 0) == 0)
{
if (faccessat(base_dir_fd, "energy_full_design", O_RDONLY, 0) == 0
&& faccessat(base_dir_fd, "energy_full", O_RDONLY, 0) == 0) {
{
int fd = openat(base_dir_fd, "energy_full_design", O_RDONLY);
int fd = openat(base_dir_fd, "energy_full_design", O_RDONLY | O_CLOEXEC);
if (fd == -1) {
LOG_ERRNO("/sys/class/power_supply/%s/energy_full_design", m->battery);
goto err;
@ -238,7 +309,7 @@ initialize(struct private *m)
}
{
int fd = openat(base_dir_fd, "energy_full", O_RDONLY);
int fd = openat(base_dir_fd, "energy_full", O_RDONLY | O_CLOEXEC);
if (fd == -1) {
LOG_ERRNO("/sys/class/power_supply/%s/energy_full", m->battery);
goto err;
@ -251,28 +322,27 @@ initialize(struct private *m)
m->energy_full = m->energy_full_design = -1;
}
if (faccessat(base_dir_fd, "charge_full_design", O_RDONLY, 0) == 0 &&
faccessat(base_dir_fd, "charge_full", O_RDONLY, 0) == 0)
{
if (faccessat(base_dir_fd, "charge_full_design", O_RDONLY, 0) == 0
&& faccessat(base_dir_fd, "charge_full", O_RDONLY, 0) == 0) {
{
int fd = openat(base_dir_fd, "charge_full_design", O_RDONLY);
int fd = openat(base_dir_fd, "charge_full_design", O_RDONLY | O_CLOEXEC);
if (fd == -1) {
LOG_ERRNO("/sys/class/power_supply/%s/charge_full_design", m->battery);
goto err;
}
m->charge_full_design = readint_from_fd(fd);
m->charge_full_design = readint_from_fd(fd) / m->battery_scale;
close(fd);
}
{
int fd = openat(base_dir_fd, "charge_full", O_RDONLY);
int fd = openat(base_dir_fd, "charge_full", O_RDONLY | O_CLOEXEC);
if (fd == -1) {
LOG_ERRNO("/sys/class/power_supply/%s/charge_full", m->battery);
goto err;
}
m->charge_full = readint_from_fd(fd);
m->charge_full = readint_from_fd(fd) / m->battery_scale;
close(fd);
}
} else {
@ -292,13 +362,13 @@ update_status(struct module *mod)
{
struct private *m = mod->private;
int pw_fd = open("/sys/class/power_supply", O_RDONLY);
int pw_fd = open("/sys/class/power_supply", O_RDONLY | O_CLOEXEC);
if (pw_fd < 0) {
LOG_ERRNO("/sys/class/power_supply");
return false;
}
int base_dir_fd = openat(pw_fd, m->battery, O_RDONLY);
int base_dir_fd = openat(pw_fd, m->battery, O_RDONLY | O_CLOEXEC);
close(pw_fd);
if (base_dir_fd < 0) {
@ -306,14 +376,14 @@ update_status(struct module *mod)
return false;
}
int status_fd = openat(base_dir_fd, "status", O_RDONLY);
int status_fd = openat(base_dir_fd, "status", O_RDONLY | O_CLOEXEC);
if (status_fd < 0) {
LOG_ERRNO("/sys/class/power_supply/%s/status", m->battery);
close(base_dir_fd);
return false;
}
int capacity_fd = openat(base_dir_fd, "capacity", O_RDONLY);
int capacity_fd = openat(base_dir_fd, "capacity", O_RDONLY | O_CLOEXEC);
if (capacity_fd < 0) {
LOG_ERRNO("/sys/class/power_supply/%s/capacity", m->battery);
close(status_fd);
@ -321,11 +391,12 @@ update_status(struct module *mod)
return false;
}
int energy_fd = openat(base_dir_fd, "energy_now", O_RDONLY);
int power_fd = openat(base_dir_fd, "power_now", O_RDONLY);
int charge_fd = openat(base_dir_fd, "charge_now", O_RDONLY);
int current_fd = openat(base_dir_fd, "current_now", O_RDONLY);
int time_to_empty_fd = openat(base_dir_fd, "time_to_empty_now", O_RDONLY);
int energy_fd = openat(base_dir_fd, "energy_now", O_RDONLY | O_CLOEXEC);
int power_fd = openat(base_dir_fd, "power_now", O_RDONLY | O_CLOEXEC);
int charge_fd = openat(base_dir_fd, "charge_now", O_RDONLY | O_CLOEXEC);
int current_fd = openat(base_dir_fd, "current_now", O_RDONLY | O_CLOEXEC);
int time_to_empty_fd = openat(base_dir_fd, "time_to_empty_now", O_RDONLY | O_CLOEXEC);
int time_to_full_fd = openat(base_dir_fd, "time_to_full_now", O_RDONLY | O_CLOEXEC);
long capacity = readint_from_fd(capacity_fd);
long energy = energy_fd >= 0 ? readint_from_fd(energy_fd) : -1;
@ -333,8 +404,14 @@ update_status(struct module *mod)
long charge = charge_fd >= 0 ? readint_from_fd(charge_fd) : -1;
long current = current_fd >= 0 ? readint_from_fd(current_fd) : -1;
long time_to_empty = time_to_empty_fd >= 0 ? readint_from_fd(time_to_empty_fd) : -1;
long time_to_full = time_to_full_fd >= 0 ? readint_from_fd(time_to_full_fd) : -1;
const char *status = readline_from_fd(status_fd);
if (charge >= -1) {
charge /= m->battery_scale;
}
char buf[512];
const char *status = readline_from_fd(status_fd, sizeof(buf), buf);
if (status_fd >= 0)
close(status_fd);
@ -350,6 +427,8 @@ update_status(struct module *mod)
close(current_fd);
if (time_to_empty_fd >= 0)
close(time_to_empty_fd);
if (time_to_full_fd >= 0)
close(time_to_full_fd);
if (base_dir_fd >= 0)
close(base_dir_fd);
@ -357,7 +436,7 @@ update_status(struct module *mod)
if (status == NULL) {
LOG_WARN("failed to read battery state");
state = STATE_DISCHARGING;
state = STATE_UNKNOWN;
} else if (strcmp(status, "Full") == 0)
state = STATE_FULL;
else if (strcmp(status, "Not charging") == 0)
@ -367,24 +446,32 @@ update_status(struct module *mod)
else if (strcmp(status, "Discharging") == 0)
state = STATE_DISCHARGING;
else if (strcmp(status, "Unknown") == 0)
state = STATE_DISCHARGING;
state = STATE_UNKNOWN;
else {
LOG_ERR("unrecognized battery state: %s", status);
state = STATE_DISCHARGING;
state = STATE_UNKNOWN;
}
LOG_DBG("capacity: %ld, energy: %ld, power: %ld, charge=%ld, current=%ld, "
"time-to-empty: %ld", capacity, energy, power, charge, current,
time_to_empty);
"time-to-empty: %ld, time-to-full: %ld",
capacity, energy, power, charge, current, time_to_empty, time_to_full);
mtx_lock(&mod->lock);
if (m->state != state) {
m->ema_current = (struct current_state){-1, 0, (struct timespec){0, 0}};
}
m->state = state;
m->capacity = capacity;
m->energy = energy;
m->power = power;
m->charge = charge;
m->current = current;
if (current != -1) {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
ema_linear(&m->ema_current, (struct current_state){current, current, t}, m->smoothing_scale);
}
m->time_to_empty = time_to_empty;
m->time_to_full = time_to_full;
mtx_unlock(&mod->lock);
return true;
}
@ -398,13 +485,10 @@ run(struct module *mod)
if (!initialize(m))
return -1;
LOG_INFO("%s: %s %s (at %.1f%% of original capacity)",
m->battery, m->manufacturer, m->model,
(m->energy_full > 0
? 100.0 * m->energy_full / m->energy_full_design
: m->charge_full > 0
? 100.0 * m->charge_full / m->charge_full_design
: 0.0));
LOG_INFO("%s: %s %s (at %.1f%% of original capacity)", m->battery, m->manufacturer, m->model,
(m->energy_full > 0 ? 100.0 * m->energy_full / m->energy_full_design
: m->charge_full > 0 ? 100.0 * m->charge_full / m->charge_full_design
: 0.0));
int ret = 1;
@ -422,31 +506,82 @@ run(struct module *mod)
bar->refresh(bar);
int timeout_left_ms = m->poll_interval;
while (true) {
struct pollfd fds[] = {
{.fd = mod->abort_fd, .events = POLLIN},
{.fd = udev_monitor_get_fd(mon), .events = POLLIN},
};
poll(fds, 2, m->poll_interval > 0 ? m->poll_interval * 1000 : -1);
int timeout = m->poll_interval > 0 ? timeout_left_ms : -1;
struct timespec time_before_poll;
if (clock_gettime(CLOCK_BOOTTIME, &time_before_poll) < 0) {
LOG_ERRNO("failed to get current time");
break;
}
const int poll_ret = poll(fds, sizeof(fds) / sizeof(fds[0]), timeout);
if (poll_ret < 0) {
if (errno == EINTR)
continue;
LOG_ERRNO("failed to poll");
break;
}
if (fds[0].revents & POLLIN) {
ret = 0;
break;
}
bool udev_for_us = false;
if (fds[1].revents & POLLIN) {
struct udev_device *dev = udev_monitor_receive_device(mon);
const char *sysname = udev_device_get_sysname(dev);
if (dev != NULL) {
const char *sysname = udev_device_get_sysname(dev);
udev_for_us = sysname != NULL && strcmp(sysname, m->battery) == 0;
bool is_us = sysname != NULL && strcmp(sysname, m->battery) == 0;
udev_device_unref(dev);
if (!udev_for_us) {
LOG_DBG("udev notification not for us (%s != %s)", m->battery,
sysname != sysname ? sysname : "NULL");
} else
LOG_DBG("triggering update due to udev notification");
if (!is_us)
continue;
udev_device_unref(dev);
}
}
if (update_status(mod))
bar->refresh(bar);
if (udev_for_us || poll_ret == 0) {
if (update_status(mod))
bar->refresh(bar);
}
if (poll_ret == 0 || udev_for_us) {
LOG_DBG("resetting timeout-left to %ldms", m->poll_interval);
timeout_left_ms = m->poll_interval;
} else {
struct timespec time_after_poll;
if (clock_gettime(CLOCK_BOOTTIME, &time_after_poll) < 0) {
LOG_ERRNO("failed to get current time");
break;
}
struct timespec timeout_consumed;
timespec_sub(&time_after_poll, &time_before_poll, &timeout_consumed);
const int timeout_consumed_ms = timeout_consumed.tv_sec * 1000 + timeout_consumed.tv_nsec / 1000000;
LOG_DBG("timeout-left before: %dms, consumed: %dms, updated: %dms", timeout_left_ms, timeout_consumed_ms,
max(timeout_left_ms - timeout_consumed_ms, 0));
timeout_left_ms -= timeout_consumed_ms;
if (timeout_left_ms < 0)
timeout_left_ms = 0;
}
}
out:
@ -458,13 +593,17 @@ out:
}
static struct module *
battery_new(const char *battery, struct particle *label, int poll_interval_secs)
battery_new(const char *battery, struct particle *label, long poll_interval_msecs, int battery_scale,
long smoothing_secs)
{
struct private *m = calloc(1, sizeof(*m));
m->label = label;
m->poll_interval = poll_interval_secs;
m->poll_interval = poll_interval_msecs;
m->battery_scale = battery_scale;
m->smoothing_scale = smoothing_secs * one_sec_in_ns;
m->battery = strdup(battery);
m->state = STATE_DISCHARGING;
m->state = STATE_UNKNOWN;
m->ema_current = (struct current_state){-1, 0, (struct timespec){0, 0}};
struct module *mod = module_common_new();
mod->private = m;
@ -481,11 +620,29 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited)
const struct yml_node *c = yml_get_value(node, "content");
const struct yml_node *name = yml_get_value(node, "name");
const struct yml_node *poll_interval = yml_get_value(node, "poll-interval");
const struct yml_node *battery_scale = yml_get_value(node, "battery-scale");
const struct yml_node *smoothing_secs = yml_get_value(node, "smoothing-secs");
return battery_new(
yml_value_as_string(name),
conf_to_particle(c, inherited),
poll_interval != NULL ? yml_value_as_int(poll_interval) : 60);
return battery_new(yml_value_as_string(name), conf_to_particle(c, inherited),
(poll_interval != NULL ? yml_value_as_int(poll_interval) : default_poll_interval),
(battery_scale != NULL ? yml_value_as_int(battery_scale) : 1),
(smoothing_secs != NULL ? yml_value_as_int(smoothing_secs) : 100));
}
static bool
conf_verify_poll_interval(keychain_t *chain, const struct yml_node *node)
{
if (!conf_verify_unsigned(chain, node))
return false;
const long value = yml_value_as_int(node);
if (value != 0 && value < min_poll_interval) {
LOG_ERR("%s: interval value cannot be less than %ldms", conf_err_prefix(chain, node), min_poll_interval);
return false;
}
return true;
}
static bool
@ -493,7 +650,9 @@ verify_conf(keychain_t *chain, const struct yml_node *node)
{
static const struct attr_info attrs[] = {
{"name", true, &conf_verify_string},
{"poll-interval", false, &conf_verify_int},
{"poll-interval", false, &conf_verify_poll_interval},
{"battery-scale", false, &conf_verify_unsigned},
{"smoothing-secs", false, &conf_verify_unsigned},
MODULE_COMMON_ATTRS,
};

View file

@ -1,20 +1,22 @@
#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <assert.h>
#include <poll.h>
#include <sys/time.h>
#define LOG_MODULE "clock"
#define LOG_ENABLE_DBG 0
#include "../log.h"
#include "../bar/bar.h"
#include "../config.h"
#include "../config-verify.h"
#include "../config.h"
#include "../log.h"
#include "../plugin.h"
struct private {
struct private
{
struct particle *label;
enum {
UPDATE_GRANULARITY_SECONDS,
@ -22,6 +24,7 @@ struct private {
} update_granularity;
char *date_format;
char *time_format;
bool utc;
};
static void
@ -36,7 +39,7 @@ destroy(struct module *mod)
}
static const char *
description(struct module *mod)
description(const struct module *mod)
{
return "clock";
}
@ -46,7 +49,7 @@ content(struct module *mod)
{
const struct private *m = mod->private;
time_t t = time(NULL);
struct tm *tm = localtime(&t);
struct tm *tm = m->utc ? gmtime(&t) : localtime(&t);
char date_str[1024];
strftime(date_str, sizeof(date_str), m->date_format, tm);
@ -55,8 +58,7 @@ content(struct module *mod)
strftime(time_str, sizeof(time_str), m->time_format, tm);
struct tag_set tags = {
.tags = (struct tag *[]){tag_new_string(mod, "time", time_str),
tag_new_string(mod, "date", date_str)},
.tags = (struct tag *[]){tag_new_string(mod, "time", time_str), tag_new_string(mod, "date", date_str)},
.count = 2,
};
@ -66,7 +68,6 @@ content(struct module *mod)
return exposable;
}
#include <pthread.h>
static int
run(struct module *mod)
{
@ -74,6 +75,8 @@ run(struct module *mod)
const struct bar *bar = mod->bar;
bar->refresh(bar);
int ret = 1;
while (true) {
struct timespec _now;
clock_gettime(CLOCK_REALTIME, &_now);
@ -87,15 +90,12 @@ run(struct module *mod)
switch (m->update_granularity) {
case UPDATE_GRANULARITY_SECONDS: {
const struct timeval next_second = {
.tv_sec = now.tv_sec + 1,
.tv_usec = 0};
const struct timeval next_second = {.tv_sec = now.tv_sec + 1, .tv_usec = 0};
struct timeval _timeout;
timersub(&next_second, &now, &_timeout);
assert(_timeout.tv_sec == 0 ||
(_timeout.tv_sec == 1 && _timeout.tv_usec == 0));
assert(_timeout.tv_sec == 0 || (_timeout.tv_sec == 1 && _timeout.tv_usec == 0));
timeout_ms = _timeout.tv_usec / 1000;
break;
}
@ -115,44 +115,44 @@ run(struct module *mod)
/* Add 1ms to account for rounding errors */
timeout_ms++;
LOG_DBG("now: %lds %ldµs -> timeout: %dms",
now.tv_sec, now.tv_usec, timeout_ms);
LOG_DBG("now: %lds %ldµs -> timeout: %dms", now.tv_sec, now.tv_usec, timeout_ms);
struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}};
poll(fds, 1, timeout_ms);
if (poll(fds, 1, timeout_ms) < 0) {
if (errno == EINTR)
continue;
if (fds[0].revents & POLLIN)
LOG_ERRNO("failed to poll");
break;
}
if (fds[0].revents & POLLIN) {
ret = 0;
break;
}
bar->refresh(bar);
}
return 0;
return ret;
}
static struct module *
clock_new(struct particle *label, const char *date_format, const char *time_format)
clock_new(struct particle *label, const char *date_format, const char *time_format, bool utc)
{
struct private *m = calloc(1, sizeof(*m));
m->label = label;
m->date_format = strdup(date_format);
m->time_format = strdup(time_format);
m->utc = utc;
static const char *const seconds_formatters[] = {
"%c",
"%s",
"%S",
"%T",
"%r",
"%X",
"%c", "%s", "%S", "%T", "%r", "%X",
};
m->update_granularity = UPDATE_GRANULARITY_MINUTES;
for (size_t i = 0;
i < sizeof(seconds_formatters) / sizeof(seconds_formatters[0]);
i++)
{
for (size_t i = 0; i < sizeof(seconds_formatters) / sizeof(seconds_formatters[0]); i++) {
if (strstr(time_format, seconds_formatters[i]) != NULL) {
m->update_granularity = UPDATE_GRANULARITY_SECONDS;
break;
@ -160,8 +160,7 @@ clock_new(struct particle *label, const char *date_format, const char *time_form
}
LOG_DBG("using %s update granularity",
(m->update_granularity == UPDATE_GRANULARITY_MINUTES
? "minutes" : "seconds"));
(m->update_granularity == UPDATE_GRANULARITY_MINUTES ? "minutes" : "seconds"));
struct module *mod = module_common_new();
mod->private = m;
@ -178,11 +177,11 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited)
const struct yml_node *c = yml_get_value(node, "content");
const struct yml_node *date_format = yml_get_value(node, "date-format");
const struct yml_node *time_format = yml_get_value(node, "time-format");
const struct yml_node *utc = yml_get_value(node, "utc");
return clock_new(
conf_to_particle(c, inherited),
date_format != NULL ? yml_value_as_string(date_format) : "%x",
time_format != NULL ? yml_value_as_string(time_format) : "%H:%M");
return clock_new(conf_to_particle(c, inherited), date_format != NULL ? yml_value_as_string(date_format) : "%x",
time_format != NULL ? yml_value_as_string(time_format) : "%H:%M",
utc != NULL ? yml_value_as_bool(utc) : false);
}
static bool
@ -191,6 +190,7 @@ verify_conf(keychain_t *chain, const struct yml_node *node)
static const struct attr_info attrs[] = {
{"date-format", false, &conf_verify_string},
{"time-format", false, &conf_verify_string},
{"utc", false, &conf_verify_bool},
MODULE_COMMON_ATTRS,
};

297
modules/cpu.c Normal file
View file

@ -0,0 +1,297 @@
#include <assert.h>
#include <errno.h>
#include <inttypes.h>
#include <math.h>
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#define LOG_MODULE "cpu"
#define LOG_ENABLE_DBG 0
#include "../log.h"
#include "../bar/bar.h"
#include "../config-verify.h"
#include "../config.h"
#include "../particles/dynlist.h"
#include "../plugin.h"
static const long min_poll_interval = 250;
struct cpu_stats {
uint32_t *prev_cores_idle;
uint32_t *prev_cores_nidle;
uint32_t *cur_cores_idle;
uint32_t *cur_cores_nidle;
};
struct private
{
struct particle *template;
uint16_t interval;
size_t core_count;
struct cpu_stats cpu_stats;
};
static void
destroy(struct module *mod)
{
struct private *m = mod->private;
m->template->destroy(m->template);
free(m->cpu_stats.prev_cores_idle);
free(m->cpu_stats.prev_cores_nidle);
free(m->cpu_stats.cur_cores_idle);
free(m->cpu_stats.cur_cores_nidle);
free(m);
module_default_destroy(mod);
}
static const char *
description(const struct module *mod)
{
return "cpu";
}
static uint32_t
get_cpu_nb_cores()
{
int nb_cores = sysconf(_SC_NPROCESSORS_ONLN);
LOG_DBG("CPU count: %d", nb_cores);
return nb_cores;
}
static bool
parse_proc_stat_line(const char *line, uint32_t *user, uint32_t *nice, uint32_t *system, uint32_t *idle,
uint32_t *iowait, uint32_t *irq, uint32_t *softirq, uint32_t *steal, uint32_t *guest,
uint32_t *guestnice)
{
int32_t core_id;
if (line[sizeof("cpu") - 1] == ' ') {
int read = sscanf(line,
"cpu %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32
" %" SCNu32 " %" SCNu32 " %" SCNu32,
user, nice, system, idle, iowait, irq, softirq, steal, guest, guestnice);
return read == 10;
} else {
int read = sscanf(line,
"cpu%" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32
" %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32,
&core_id, user, nice, system, idle, iowait, irq, softirq, steal, guest, guestnice);
return read == 11;
}
}
static uint8_t
get_cpu_usage_percent(const struct cpu_stats *cpu_stats, int8_t core_idx)
{
uint32_t prev_total = cpu_stats->prev_cores_idle[core_idx + 1] + cpu_stats->prev_cores_nidle[core_idx + 1];
uint32_t cur_total = cpu_stats->cur_cores_idle[core_idx + 1] + cpu_stats->cur_cores_nidle[core_idx + 1];
double totald = cur_total - prev_total;
double nidled = cpu_stats->cur_cores_nidle[core_idx + 1] - cpu_stats->prev_cores_nidle[core_idx + 1];
double percent = (nidled * 100) / (totald + 1);
return round(percent);
}
static void
refresh_cpu_stats(struct cpu_stats *cpu_stats, size_t core_count)
{
int32_t core = 0;
uint32_t user = 0;
uint32_t nice = 0;
uint32_t system = 0;
uint32_t idle = 0;
uint32_t iowait = 0;
uint32_t irq = 0;
uint32_t softirq = 0;
uint32_t steal = 0;
uint32_t guest = 0;
uint32_t guestnice = 0;
FILE *fp = NULL;
char *line = NULL;
size_t len = 0;
ssize_t read;
fp = fopen("/proc/stat", "re");
if (NULL == fp) {
LOG_ERRNO("unable to open /proc/stat");
return;
}
while ((read = getline(&line, &len, fp)) != -1 && core <= core_count) {
if (strncmp(line, "cpu", sizeof("cpu") - 1) == 0) {
if (!parse_proc_stat_line(line, &user, &nice, &system, &idle, &iowait, &irq, &softirq, &steal, &guest,
&guestnice)) {
LOG_ERR("unable to parse /proc/stat line");
goto exit;
}
cpu_stats->prev_cores_idle[core] = cpu_stats->cur_cores_idle[core];
cpu_stats->prev_cores_nidle[core] = cpu_stats->cur_cores_nidle[core];
cpu_stats->cur_cores_idle[core] = idle + iowait;
cpu_stats->cur_cores_nidle[core] = user + nice + system + irq + softirq + steal;
core++;
}
}
exit:
fclose(fp);
free(line);
}
static struct exposable *
content(struct module *mod)
{
const struct private *m = mod->private;
mtx_lock(&mod->lock);
const size_t list_count = m->core_count + 1;
struct exposable *parts[list_count];
{
uint8_t total_usage = get_cpu_usage_percent(&m->cpu_stats, -1);
struct tag_set tags = {
.tags = (struct tag *[]){
tag_new_int(mod, "id", -1),
tag_new_int_range(mod, "cpu", total_usage, 0, 100),
},
.count = 2,
};
parts[0] = m->template->instantiate(m->template, &tags);
tag_set_destroy(&tags);
}
for (size_t i = 0; i < m->core_count; i++) {
uint8_t core_usage = get_cpu_usage_percent(&m->cpu_stats, i);
struct tag_set tags = {
.tags = (struct tag *[]){
tag_new_int(mod, "id", i),
tag_new_int_range(mod, "cpu", core_usage, 0, 100),
},
.count = 2,
};
parts[i + 1] = m->template->instantiate(m->template, &tags);
tag_set_destroy(&tags);
}
mtx_unlock(&mod->lock);
return dynlist_exposable_new(parts, list_count, 0, 0);
}
static int
run(struct module *mod)
{
const struct bar *bar = mod->bar;
bar->refresh(bar);
struct private *p = mod->private;
while (true) {
struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}};
int res = poll(fds, sizeof(fds) / sizeof(*fds), p->interval);
if (res < 0) {
if (EINTR == errno)
continue;
LOG_ERRNO("unable to poll abort fd");
return -1;
}
if (fds[0].revents & POLLIN)
break;
mtx_lock(&mod->lock);
refresh_cpu_stats(&p->cpu_stats, p->core_count);
mtx_unlock(&mod->lock);
bar->refresh(bar);
}
return 0;
}
static struct module *
cpu_new(uint16_t interval, struct particle *template)
{
uint32_t nb_cores = get_cpu_nb_cores();
struct private *p = calloc(1, sizeof(*p));
p->template = template;
p->interval = interval;
p->core_count = nb_cores;
p->cpu_stats.prev_cores_nidle = calloc(nb_cores + 1, sizeof(*p->cpu_stats.prev_cores_nidle));
p->cpu_stats.prev_cores_idle = calloc(nb_cores + 1, sizeof(*p->cpu_stats.prev_cores_idle));
p->cpu_stats.cur_cores_nidle = calloc(nb_cores + 1, sizeof(*p->cpu_stats.cur_cores_nidle));
p->cpu_stats.cur_cores_idle = calloc(nb_cores + 1, sizeof(*p->cpu_stats.cur_cores_idle));
struct module *mod = module_common_new();
mod->private = p;
mod->run = &run;
mod->destroy = &destroy;
mod->content = &content;
mod->description = &description;
return mod;
}
static struct module *
from_conf(const struct yml_node *node, struct conf_inherit inherited)
{
const struct yml_node *interval = yml_get_value(node, "poll-interval");
const struct yml_node *c = yml_get_value(node, "content");
return cpu_new(interval == NULL ? min_poll_interval : yml_value_as_int(interval), conf_to_particle(c, inherited));
}
static bool
conf_verify_poll_interval(keychain_t *chain, const struct yml_node *node)
{
if (!conf_verify_unsigned(chain, node))
return false;
if (yml_value_as_int(node) < min_poll_interval) {
LOG_ERR("%s: interval value cannot be less than %ldms", conf_err_prefix(chain, node), min_poll_interval);
return false;
}
return true;
}
static bool
verify_conf(keychain_t *chain, const struct yml_node *node)
{
static const struct attr_info attrs[] = {
{"poll-interval", false, &conf_verify_poll_interval},
MODULE_COMMON_ATTRS,
};
return conf_verify_dict(chain, node, attrs);
}
const struct module_iface module_cpu_iface = {
.verify_conf = &verify_conf,
.from_conf = &from_conf,
};
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
extern const struct module_iface iface __attribute__((weak, alias("module_cpu_iface")));
#endif

13
modules/dbus.h Normal file
View file

@ -0,0 +1,13 @@
#pragma once
// This header provides an generic interface for different versions of
// systemd-sdbus.
#if defined(HAVE_LIBSYSTEMD)
#include <systemd/sd-bus.h>
#elif defined(HAVE_LIBELOGIND)
#include <elogind/sd-bus.h>
#elif defined(HAVE_BASU)
#include <basu/sd-bus.h>
#endif

350
modules/disk-io.c Normal file
View file

@ -0,0 +1,350 @@
#include <dirent.h>
#include <errno.h>
#include <inttypes.h>
#include <poll.h>
#include <stdbool.h>
#include <string.h>
#include <tllist.h>
#define LOG_MODULE "disk-io"
#define LOG_ENABLE_DBG 0
#include "../log.h"
#include "../bar/bar.h"
#include "../config-verify.h"
#include "../config.h"
#include "../particles/dynlist.h"
#include "../plugin.h"
static const long min_poll_interval = 250;
struct device_stats {
char *name;
bool is_disk;
uint64_t prev_sectors_read;
uint64_t cur_sectors_read;
uint64_t prev_sectors_written;
uint64_t cur_sectors_written;
uint32_t ios_in_progress;
bool exists;
};
struct private
{
struct particle *label;
uint16_t interval;
tll(struct device_stats *) devices;
};
static bool
is_disk(char const *name)
{
DIR *dir = opendir("/sys/block");
if (dir == NULL) {
LOG_ERRNO("failed to read /sys/block directory");
return false;
}
struct dirent *entry;
bool found = false;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(name, entry->d_name) == 0) {
found = true;
break;
}
}
closedir(dir);
return found;
}
static struct device_stats *
new_device_stats(char const *name)
{
struct device_stats *dev = malloc(sizeof(*dev));
dev->name = strdup(name);
dev->is_disk = is_disk(name);
return dev;
}
static void
free_device_stats(struct device_stats *dev)
{
free(dev->name);
free(dev);
}
static void
destroy(struct module *mod)
{
struct private *m = mod->private;
m->label->destroy(m->label);
tll_foreach(m->devices, it) { free_device_stats(it->item); }
tll_free(m->devices);
free(m);
module_default_destroy(mod);
}
static const char *
description(const struct module *mod)
{
return "disk-io";
}
static void
refresh_device_stats(struct private *m)
{
FILE *fp = NULL;
char *line = NULL;
size_t len = 0;
ssize_t read;
fp = fopen("/proc/diskstats", "re");
if (NULL == fp) {
LOG_ERRNO("unable to open /proc/diskstats");
return;
}
/*
* Devices may be added or removed during the bar's lifetime, as external
* block devices are connected or disconnected from the machine. /proc/diskstats
* reports data only for the devices that are currently connected.
*
* This means that if we have a device that ISN'T in /proc/diskstats, it was
* disconnected, and we need to remove it from the list.
*
* On the other hand, if a device IS in /proc/diskstats, but not in our list, we
* must create a new device_stats struct and add it to the list.
*
* The 'exists' variable is what keep tracks of whether or not /proc/diskstats
* is still reporting the device (i.e., it is still connected).
*/
tll_foreach(m->devices, it) { it->item->exists = false; }
while ((read = getline(&line, &len, fp)) != -1) {
/*
* For an explanation of the fields below, see
* https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats
*/
uint8_t major_number = 0;
uint8_t minor_number = 0;
char *device_name = NULL;
uint32_t completed_reads = 0;
uint32_t merged_reads = 0;
uint64_t sectors_read = 0;
uint32_t reading_time = 0;
uint32_t completed_writes = 0;
uint32_t merged_writes = 0;
uint64_t sectors_written = 0;
uint32_t writting_time = 0;
uint32_t ios_in_progress = 0;
uint32_t io_time = 0;
uint32_t io_weighted_time = 0;
uint32_t completed_discards = 0;
uint32_t merged_discards = 0;
uint32_t sectors_discarded = 0;
uint32_t discarding_time = 0;
uint32_t completed_flushes = 0;
uint32_t flushing_time = 0;
if (!sscanf(line,
" %" SCNu8 " %" SCNu8 " %ms %" SCNu32 " %" SCNu32 " %" SCNu64 " %" SCNu32 " %" SCNu32 " %" SCNu32
" %" SCNu64 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32
" %" SCNu32 " %" SCNu32 " %" SCNu32,
&major_number, &minor_number, &device_name, &completed_reads, &merged_reads, &sectors_read,
&reading_time, &completed_writes, &merged_writes, &sectors_written, &writting_time,
&ios_in_progress, &io_time, &io_weighted_time, &completed_discards, &merged_discards,
&sectors_discarded, &discarding_time, &completed_flushes, &flushing_time)) {
LOG_ERR("unable to parse /proc/diskstats line");
free(device_name);
goto exit;
}
bool found = false;
tll_foreach(m->devices, it)
{
struct device_stats *dev = it->item;
if (strcmp(dev->name, device_name) == 0) {
dev->prev_sectors_read = dev->cur_sectors_read;
dev->prev_sectors_written = dev->cur_sectors_written;
dev->ios_in_progress = ios_in_progress;
dev->cur_sectors_read = sectors_read;
dev->cur_sectors_written = sectors_written;
dev->exists = true;
found = true;
break;
}
}
if (!found) {
struct device_stats *new_dev = new_device_stats(device_name);
new_dev->ios_in_progress = ios_in_progress;
new_dev->prev_sectors_read = sectors_read;
new_dev->cur_sectors_read = sectors_read;
new_dev->prev_sectors_written = sectors_written;
new_dev->cur_sectors_written = sectors_written;
new_dev->exists = true;
tll_push_back(m->devices, new_dev);
}
free(device_name);
}
tll_foreach(m->devices, it)
{
if (!it->item->exists) {
free_device_stats(it->item);
tll_remove(m->devices, it);
}
}
exit:
fclose(fp);
free(line);
}
static struct exposable *
content(struct module *mod)
{
const struct private *p = mod->private;
uint64_t total_bytes_read = 0;
uint64_t total_bytes_written = 0;
uint32_t total_ios_in_progress = 0;
mtx_lock(&mod->lock);
struct exposable *tag_parts[p->devices.length + 1];
int i = 0;
tll_foreach(p->devices, it)
{
struct device_stats *dev = it->item;
uint64_t bytes_read = (dev->cur_sectors_read - dev->prev_sectors_read) * 512;
uint64_t bytes_written = (dev->cur_sectors_written - dev->prev_sectors_written) * 512;
if (dev->is_disk) {
total_bytes_read += bytes_read;
total_bytes_written += bytes_written;
total_ios_in_progress += dev->ios_in_progress;
}
struct tag_set tags = {
.tags = (struct tag *[]) {
tag_new_string(mod, "device", dev->name),
tag_new_bool(mod, "is_disk", dev->is_disk),
tag_new_int(mod, "read_speed", (bytes_read * 1000) / p->interval),
tag_new_int(mod, "write_speed", (bytes_written * 1000) / p->interval),
tag_new_int(mod, "ios_in_progress", dev->ios_in_progress),
},
.count = 5,
};
tag_parts[i++] = p->label->instantiate(p->label, &tags);
tag_set_destroy(&tags);
}
struct tag_set tags = {
.tags = (struct tag *[]) {
tag_new_string(mod, "device", "Total"),
tag_new_bool(mod, "is_disk", true),
tag_new_int(mod, "read_speed", (total_bytes_read * 1000) / p->interval),
tag_new_int(mod, "write_speed", (total_bytes_written * 1000) / p->interval),
tag_new_int(mod, "ios_in_progress", total_ios_in_progress),
},
.count = 5,
};
tag_parts[i] = p->label->instantiate(p->label, &tags);
tag_set_destroy(&tags);
mtx_unlock(&mod->lock);
return dynlist_exposable_new(tag_parts, p->devices.length + 1, 0, 0);
}
static int
run(struct module *mod)
{
const struct bar *bar = mod->bar;
bar->refresh(bar);
struct private *p = mod->private;
while (true) {
struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}};
int res = poll(fds, sizeof(fds) / sizeof(*fds), p->interval);
if (res < 0) {
if (EINTR == errno)
continue;
LOG_ERRNO("unable to poll abort fd");
return -1;
}
if (fds[0].revents & POLLIN)
break;
mtx_lock(&mod->lock);
refresh_device_stats(p);
mtx_unlock(&mod->lock);
bar->refresh(bar);
}
return 0;
}
static struct module *
disk_io_new(uint16_t interval, struct particle *label)
{
struct private *p = calloc(1, sizeof(*p));
p->label = label;
p->interval = interval;
struct module *mod = module_common_new();
mod->private = p;
mod->run = &run;
mod->destroy = &destroy;
mod->content = &content;
mod->description = &description;
return mod;
}
static struct module *
from_conf(const struct yml_node *node, struct conf_inherit inherited)
{
const struct yml_node *interval = yml_get_value(node, "poll-interval");
const struct yml_node *c = yml_get_value(node, "content");
return disk_io_new(interval == NULL ? min_poll_interval : yml_value_as_int(interval),
conf_to_particle(c, inherited));
}
static bool
conf_verify_poll_interval(keychain_t *chain, const struct yml_node *node)
{
if (!conf_verify_unsigned(chain, node))
return false;
if (yml_value_as_int(node) < min_poll_interval) {
LOG_ERR("%s: poll-interval value cannot be less than %ldms", conf_err_prefix(chain, node), min_poll_interval);
return false;
}
return true;
}
static bool
verify_conf(keychain_t *chain, const struct yml_node *node)
{
static const struct attr_info attrs[] = {
{"poll-interval", false, &conf_verify_poll_interval},
MODULE_COMMON_ATTRS,
};
return conf_verify_dict(chain, node, attrs);
}
const struct module_iface module_disk_io_iface = {
.verify_conf = &verify_conf,
.from_conf = &from_conf,
};
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
extern const struct module_iface iface __attribute__((weak, alias("module_disk_io_iface")));
#endif

550
modules/dwl.c Normal file
View file

@ -0,0 +1,550 @@
#include <errno.h>
#include <poll.h>
#include <string.h>
#include <sys/inotify.h>
#include <unistd.h>
#define ARR_LEN(x) (sizeof((x)) / sizeof((x)[0]))
#include "../config-verify.h"
#include "../config.h"
#include "../log.h"
#include "../module.h"
#include "../particles/dynlist.h"
#include "../plugin.h"
#define LOG_MODULE "dwl"
#define LOG_ENABLE_DBG 0
struct dwl_tag {
int id;
char *name;
bool selected;
bool empty;
bool urgent;
};
struct private
{
struct particle *label;
char const *monitor;
unsigned int number_of_tags;
char *dwl_info_filename;
/* dwl data */
char *title;
char *appid;
bool fullscreen;
bool floating;
bool selmon;
tll(struct dwl_tag *) tags;
char *layout;
};
enum LINE_MODE {
LINE_MODE_0,
LINE_MODE_TITLE,
LINE_MODE_APPID,
LINE_MODE_FULLSCREEN,
LINE_MODE_FLOATING,
LINE_MODE_SELMON,
LINE_MODE_TAGS,
LINE_MODE_LAYOUT,
};
static void
free_dwl_tag(struct dwl_tag *tag)
{
free(tag->name);
free(tag);
}
static void
destroy(struct module *module)
{
struct private *private = module->private;
private->label->destroy(private->label);
tll_free_and_free(private->tags, free_dwl_tag);
free(private->dwl_info_filename);
free(private->title);
free(private->layout);
free(private);
module_default_destroy(module);
}
static char const *
description(const struct module *module)
{
return "dwl";
}
static struct exposable *
content(struct module *module)
{
struct private const *private = module->private;
mtx_lock(&module->lock);
size_t i = 0;
/* + 1 for `default` tag */
struct exposable *exposable[tll_length(private->tags) + 1];
tll_foreach(private->tags, it)
{
struct tag_set tags = {
.tags = (struct tag*[]){
tag_new_string(module, "title", private->title),
tag_new_string(module, "appid", private->appid),
tag_new_bool(module, "fullscreen", private->fullscreen),
tag_new_bool(module, "floating", private->floating),
tag_new_bool(module, "selmon", private->selmon),
tag_new_string(module, "layout", private->layout),
tag_new_int(module, "id", it->item->id),
tag_new_string(module, "name", it->item->name),
tag_new_bool(module, "selected", it->item->selected),
tag_new_bool(module, "empty", it->item->empty),
tag_new_bool(module, "urgent", it->item->urgent),
},
.count = 11,
};
exposable[i++] = private->label->instantiate(private->label, &tags);
tag_set_destroy(&tags);
}
/* default tag (used for title, layout, etc) */
struct tag_set tags = {
.tags = (struct tag*[]){
tag_new_string(module, "title", private->title),
tag_new_string(module, "appid", private->appid),
tag_new_bool(module, "fullscreen", private->fullscreen),
tag_new_bool(module, "floating", private->floating),
tag_new_bool(module, "selmon", private->selmon),
tag_new_string(module, "layout", private->layout),
tag_new_int(module, "id", 0),
tag_new_string(module, "name", "0"),
tag_new_bool(module, "selected", false),
tag_new_bool(module, "empty", true),
tag_new_bool(module, "urgent", false),
},
.count = 11,
};
exposable[i++] = private->label->instantiate(private->label, &tags);
tag_set_destroy(&tags);
mtx_unlock(&module->lock);
return dynlist_exposable_new(exposable, i, 0, 0);
}
static struct dwl_tag *
dwl_tag_from_id(struct private *private, uint32_t id)
{
tll_foreach(private->tags, it)
{
if (it->item->id == id)
return it->item;
}
assert(false); /* unreachable */
return NULL;
}
static void
process_line(char *line, struct module *module)
{
struct private *private = module->private;
enum LINE_MODE line_mode = LINE_MODE_0;
/* Remove \n */
line[strcspn(line, "\n")] = '\0';
/* Split line by space */
size_t index = 1;
char *save_pointer = NULL;
char *string = strtok_r(line, " ", &save_pointer);
while (string != NULL) {
/* dwl logs are formatted like this
* $1 -> monitor
* $2 -> action
* $3 -> arg1
* $4 -> arg2
* ... */
/* monitor */
if (index == 1) {
/* Not our monitor */
if (strcmp(string, private->monitor) != 0)
break;
}
/* action */
else if (index == 2) {
if (strcmp(string, "title") == 0) {
line_mode = LINE_MODE_TITLE;
/* Update the title here, to avoid allocate and free memory on
* every iteration (the line is separated by spaces, then we
* join it again) a bit suboptimal, isn't it?) */
free(private->title);
private->title = strdup(save_pointer);
break;
} else if (strcmp(string, "appid") == 0) {
line_mode = LINE_MODE_APPID;
/* Update the appid here, same as the title. */
free(private->appid);
private->appid = strdup(save_pointer);
break;
} else if (strcmp(string, "fullscreen") == 0)
line_mode = LINE_MODE_FULLSCREEN;
else if (strcmp(string, "floating") == 0)
line_mode = LINE_MODE_FLOATING;
else if (strcmp(string, "selmon") == 0)
line_mode = LINE_MODE_SELMON;
else if (strcmp(string, "tags") == 0)
line_mode = LINE_MODE_TAGS;
else if (strcmp(string, "layout") == 0)
line_mode = LINE_MODE_LAYOUT;
else {
LOG_WARN("UNKNOWN action, please open an issue on https://codeberg.org/dnkl/yambar");
return;
}
}
/* args */
else {
if (line_mode == LINE_MODE_TAGS) {
static uint32_t occupied, selected, client_tags, urgent;
static uint32_t *target = NULL;
/* dwl tags action log are formatted like this
* $3 -> occupied
* $4 -> tags
* $5 -> clientTags (not needed)
* $6 -> urgent */
if (index == 3)
target = &occupied;
else if (index == 4)
target = &selected;
else if (index == 5)
target = &client_tags;
else if (index == 6)
target = &urgent;
/* No need to check error IMHO */
*target = strtoul(string, NULL, 10);
/* Populate information */
if (index == 6) {
for (size_t id = 1; id <= private->number_of_tags; ++id) {
uint32_t mask = 1 << (id - 1);
struct dwl_tag *dwl_tag = dwl_tag_from_id(private, id);
dwl_tag->selected = mask & selected;
dwl_tag->empty = !(mask & occupied);
dwl_tag->urgent = mask & urgent;
}
}
} else
switch (line_mode) {
case LINE_MODE_TITLE:
case LINE_MODE_APPID:
assert(false); /* unreachable */
break;
case LINE_MODE_FULLSCREEN:
private
->fullscreen = (strcmp(string, "0") != 0);
break;
case LINE_MODE_FLOATING:
private
->floating = (strcmp(string, "0") != 0);
break;
case LINE_MODE_SELMON:
private
->selmon = (strcmp(string, "0") != 0);
break;
case LINE_MODE_LAYOUT:
free(private->layout);
private->layout = strdup(string);
break;
default:;
assert(false); /* unreachable */
}
}
string = strtok_r(NULL, " ", &save_pointer);
++index;
}
}
static int
file_read_content(FILE *file, struct module *module)
{
static char buffer[1024];
errno = 0;
while (fgets(buffer, ARR_LEN(buffer), file) != NULL)
process_line(buffer, module);
fseek(file, 0, SEEK_END);
/* Check whether error has been */
if (ferror(file) != 0) {
LOG_ERRNO("unable to read file's content.");
return 1;
}
return 0;
}
static void
file_seek_to_last_n_lines(FILE *file, int number_of_lines)
{
if (number_of_lines == 0 || file == NULL)
return;
fseek(file, 0, SEEK_END);
long position = ftell(file);
while (position > 0) {
/* Cannot go less than position 0 */
if (fseek(file, --position, SEEK_SET) == EINVAL)
break;
if (fgetc(file) == '\n')
if (number_of_lines-- == 0)
break;
}
}
static int
run_init(int *inotify_fd, int *inotify_wd, FILE **file, char *dwl_info_filename)
{
*inotify_fd = inotify_init();
if (*inotify_fd == -1) {
LOG_ERRNO("unable to create inotify fd.");
return -1;
}
*inotify_wd = inotify_add_watch(*inotify_fd, dwl_info_filename, IN_MODIFY);
if (*inotify_wd == -1) {
close(*inotify_fd);
LOG_ERRNO("unable to add watch to inotify fd.");
return 1;
}
*file = fopen(dwl_info_filename, "re");
if (*file == NULL) {
inotify_rm_watch(*inotify_fd, *inotify_wd);
close(*inotify_fd);
LOG_ERRNO("unable to open file.");
return 1;
}
return 0;
}
static int
run_clean(int inotify_fd, int inotify_wd, FILE *file)
{
if (inotify_fd != -1) {
if (inotify_wd != -1)
inotify_rm_watch(inotify_fd, inotify_wd);
close(inotify_fd);
}
if (file != NULL) {
if (fclose(file) == EOF) {
LOG_ERRNO("unable to close file.");
return 1;
}
}
return 0;
};
static int
run(struct module *module)
{
struct private *private = module->private;
/* Ugly, but I didn't find better way for waiting
* the monitor's name to be set */
do {
private->monitor = module->bar->output_name(module->bar);
usleep(50);
} while (private->monitor == NULL);
int inotify_fd = -1, inotify_wd = -1;
FILE *file = NULL;
if (run_init(&inotify_fd, &inotify_wd, &file, private->dwl_info_filename) != 0)
return 1;
/* Dwl output is 6 lines per monitor, so let's assume that nobody has
* more than 5 monitors (6 * 5 = 30) */
mtx_lock(&module->lock);
file_seek_to_last_n_lines(file, 30);
if (file_read_content(file, module) != 0) {
mtx_unlock(&module->lock);
return run_clean(inotify_fd, inotify_wd, file);
}
mtx_unlock(&module->lock);
module->bar->refresh(module->bar);
while (true) {
struct pollfd fds[] = {
(struct pollfd){.fd = module->abort_fd, .events = POLLIN},
(struct pollfd){.fd = inotify_fd, .events = POLLIN},
};
if (poll(fds, ARR_LEN(fds), -1) == -1) {
if (errno == EINTR)
continue;
LOG_ERRNO("unable to poll.");
break;
}
if (fds[0].revents & POLLIN)
break;
/* fds[1] (inotify_fd) must be POLLIN otherwise issue happen'd */
if (!(fds[1].revents & POLLIN)) {
LOG_ERR("expected POLLIN revent");
break;
}
/* Block until event */
static char buffer[1024];
ssize_t length = read(inotify_fd, buffer, ARR_LEN(buffer));
if (length == 0)
break;
if (length == -1) {
if (errno == EAGAIN)
continue;
LOG_ERRNO("unable to read %s", private->dwl_info_filename);
break;
}
mtx_lock(&module->lock);
if (file_read_content(file, module) != 0) {
mtx_unlock(&module->lock);
break;
}
mtx_unlock(&module->lock);
module->bar->refresh(module->bar);
}
return run_clean(inotify_fd, inotify_wd, file);
}
static struct module *
dwl_new(struct particle *label, int number_of_tags, struct yml_node const *name_of_tags, char const *dwl_info_filename)
{
struct private *private = calloc(1, sizeof(struct private));
private->label = label;
private->number_of_tags = number_of_tags;
private->dwl_info_filename = strdup(dwl_info_filename);
struct yml_list_iter list = {0};
if (name_of_tags)
list = yml_list_iter(name_of_tags);
for (int i = 1; i <= number_of_tags; i++) {
struct dwl_tag *dwl_tag = calloc(1, sizeof(struct dwl_tag));
dwl_tag->id = i;
if (list.node) {
dwl_tag->name = strdup(yml_value_as_string(list.node));
yml_list_next(&list);
} else if (asprintf(&dwl_tag->name, "%d", i) < 0) {
LOG_ERRNO("asprintf");
}
tll_push_back(private->tags, dwl_tag);
}
struct module *module = module_common_new();
module->private = private;
module->run = &run;
module->destroy = &destroy;
module->content = &content;
module->description = &description;
return module;
}
static struct module *
from_conf(struct yml_node const *node, struct conf_inherit inherited)
{
struct yml_node const *content = yml_get_value(node, "content");
struct yml_node const *number_of_tags = yml_get_value(node, "number-of-tags");
struct yml_node const *name_of_tags = yml_get_value(node, "name-of-tags");
struct yml_node const *dwl_info_filename = yml_get_value(node, "dwl-info-filename");
return dwl_new(conf_to_particle(content, inherited), yml_value_as_int(number_of_tags), name_of_tags,
yml_value_as_string(dwl_info_filename));
}
static bool
verify_names(keychain_t *keychain, const struct yml_node *node)
{
if (!yml_is_list(node)) {
LOG_ERR("%s: %s is not a list", conf_err_prefix(keychain, node), yml_value_as_string(node));
return false;
}
return conf_verify_list(keychain, node, &conf_verify_string);
}
static bool
verify_conf(keychain_t *keychain, struct yml_node const *node)
{
static struct attr_info const attrs[] = {
{"number-of-tags", true, &conf_verify_unsigned},
{"name-of-tags", false, &verify_names},
{"dwl-info-filename", true, &conf_verify_string},
MODULE_COMMON_ATTRS,
};
if (!conf_verify_dict(keychain, node, attrs))
return false;
/* No need to check whether is `number_of_tags` is a int
* because `conf_verify_unsigned` already did it */
struct yml_node const *ntags_key = yml_get_key(node, "number-of-tags");
struct yml_node const *value = yml_get_value(node, "number-of-tags");
int number_of_tags = yml_value_as_int(value);
if (number_of_tags == 0) {
LOG_ERR("%s: %s must not be 0", conf_err_prefix(keychain, ntags_key), yml_value_as_string(ntags_key));
return false;
}
struct yml_node const *key = yml_get_key(node, "name-of-tags");
value = yml_get_value(node, "name-of-tags");
if (value && yml_list_length(value) != number_of_tags) {
LOG_ERR("%s: %s must have the same number of elements that %s", conf_err_prefix(keychain, key),
yml_value_as_string(key), yml_value_as_string(ntags_key));
return false;
}
/* No need to check whether is `dwl_info_filename` is a string
* because `conf_verify_string` already did it */
key = yml_get_key(node, "dwl-info-filename");
value = yml_get_value(node, "dwl-info-filename");
if (strlen(yml_value_as_string(value)) == 0) {
LOG_ERR("%s: %s must not be empty", conf_err_prefix(keychain, key), yml_value_as_string(key));
return false;
}
return true;
}
struct module_iface const module_dwl_iface = {
.verify_conf = &verify_conf,
.from_conf = &from_conf,
};
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
extern struct module_iface const iface __attribute__((weak, alias("module_dwl_iface")));
#endif

666
modules/foreign-toplevel.c Normal file
View file

@ -0,0 +1,666 @@
#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <tllist.h>
#include <wayland-client.h>
#define LOG_MODULE "foreign-toplevel"
#define LOG_ENABLE_DBG 0
#include "../log.h"
#include "../particles/dynlist.h"
#include "../plugin.h"
#include "wlr-foreign-toplevel-management-unstable-v1.h"
#include "xdg-output-unstable-v1.h"
#define min(x, y) ((x) < (y) ? (x) : (y))
static const int required_manager_interface_version = 2;
struct output {
struct module *mod;
uint32_t wl_name;
struct wl_output *wl_output;
struct zxdg_output_v1 *xdg_output;
char *name;
};
struct toplevel {
struct module *mod;
struct zwlr_foreign_toplevel_handle_v1 *handle;
char *app_id;
char *title;
bool maximized;
bool minimized;
bool activated;
bool fullscreen;
tll(const struct output *) outputs;
};
struct private
{
struct particle *template;
uint32_t manager_wl_name;
struct zwlr_foreign_toplevel_manager_v1 *manager;
struct zxdg_output_manager_v1 *xdg_output_manager;
bool all_monitors;
tll(struct toplevel) toplevels;
tll(struct output) outputs;
};
static void
output_free(struct output *output)
{
free(output->name);
if (output->xdg_output != NULL)
zxdg_output_v1_destroy(output->xdg_output);
if (output->wl_output != NULL)
wl_output_release(output->wl_output);
}
static void
toplevel_free(struct toplevel *top)
{
if (top->handle != NULL)
zwlr_foreign_toplevel_handle_v1_destroy(top->handle);
free(top->app_id);
free(top->title);
tll_free(top->outputs);
}
static void
destroy(struct module *mod)
{
struct private *m = mod->private;
m->template->destroy(m->template);
assert(tll_length(m->toplevels) == 0);
assert(tll_length(m->outputs) == 0);
free(m);
module_default_destroy(mod);
}
static const char *
description(const struct module *mod)
{
return "toplevel";
}
static struct exposable *
content(struct module *mod)
{
const struct private *m = mod->private;
mtx_lock(&mod->lock);
const size_t toplevel_count = tll_length(m->toplevels);
size_t show_count = 0;
struct exposable *toplevels[toplevel_count];
const char *current_output = mod->bar->output_name(mod->bar);
tll_foreach(m->toplevels, it)
{
const struct toplevel *top = &it->item;
bool show = false;
if (m->all_monitors)
show = true;
else if (current_output != NULL) {
tll_foreach(top->outputs, it2)
{
const struct output *output = it2->item;
if (output->name != NULL && strcmp(output->name, current_output) == 0) {
show = true;
break;
}
}
}
if (!show)
continue;
struct tag_set tags = {
.tags = (struct tag *[]){
tag_new_string(mod, "app-id", it->item.app_id),
tag_new_string(mod, "title", it->item.title),
tag_new_bool(mod, "maximized", it->item.maximized),
tag_new_bool(mod, "minimized", it->item.minimized),
tag_new_bool(mod, "activated", it->item.activated),
tag_new_bool(mod, "fullscreen", it->item.fullscreen),
},
.count = 6,
};
toplevels[show_count++] = m->template->instantiate(m->template, &tags);
tag_set_destroy(&tags);
}
mtx_unlock(&mod->lock);
return dynlist_exposable_new(toplevels, show_count, 0, 0);
}
static bool
verify_iface_version(const char *iface, uint32_t version, uint32_t wanted)
{
if (version >= wanted)
return true;
LOG_ERR("%s: need interface version %u, but compositor only implements %u", iface, wanted, version);
return false;
}
static void
xdg_output_handle_logical_position(void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y)
{
}
static void
xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height)
{
}
static void
xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output)
{
}
static void
xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name)
{
struct output *output = data;
struct module *mod = output->mod;
mtx_lock(&mod->lock);
{
free(output->name);
output->name = name != NULL ? strdup(name) : NULL;
}
mtx_unlock(&mod->lock);
}
static void
xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description)
{
}
static struct zxdg_output_v1_listener xdg_output_listener = {
.logical_position = xdg_output_handle_logical_position,
.logical_size = xdg_output_handle_logical_size,
.done = xdg_output_handle_done,
.name = xdg_output_handle_name,
.description = xdg_output_handle_description,
};
static void
title(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, const char *title)
{
struct toplevel *top = data;
mtx_lock(&top->mod->lock);
{
free(top->title);
top->title = title != NULL ? strdup(title) : NULL;
}
mtx_unlock(&top->mod->lock);
}
static void
app_id(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, const char *app_id)
{
struct toplevel *top = data;
mtx_lock(&top->mod->lock);
{
free(top->app_id);
top->app_id = app_id != NULL ? strdup(app_id) : NULL;
}
mtx_unlock(&top->mod->lock);
}
static void
output_enter(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_output *wl_output)
{
struct toplevel *top = data;
struct module *mod = top->mod;
struct private *m = mod->private;
mtx_lock(&mod->lock);
const struct output *output = NULL;
tll_foreach(m->outputs, it)
{
if (it->item.wl_output == wl_output) {
output = &it->item;
break;
}
}
if (output == NULL) {
LOG_ERR("output-enter event on untracked output");
goto out;
}
tll_foreach(top->outputs, it)
{
if (it->item == output) {
LOG_ERR("output-enter event on output we're already on");
goto out;
}
}
LOG_DBG("mapped: %s:%s on %s", top->app_id, top->title, output->name);
tll_push_back(top->outputs, output);
out:
mtx_unlock(&mod->lock);
}
static void
output_leave(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_output *wl_output)
{
struct toplevel *top = data;
struct module *mod = top->mod;
struct private *m = mod->private;
mtx_lock(&mod->lock);
const struct output *output = NULL;
tll_foreach(m->outputs, it)
{
if (it->item.wl_output == wl_output) {
output = &it->item;
break;
}
}
if (output == NULL) {
LOG_ERR("output-leave event on untracked output");
goto out;
}
bool output_removed = false;
tll_foreach(top->outputs, it)
{
if (it->item == output) {
LOG_DBG("unmapped: %s:%s from %s", top->app_id, top->title, output->name);
tll_remove(top->outputs, it);
output_removed = true;
break;
}
}
if (!output_removed) {
LOG_ERR("output-leave event on an output we're not on");
goto out;
}
out:
mtx_unlock(&mod->lock);
}
static void
state(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_array *states)
{
struct toplevel *top = data;
bool maximized = false;
bool minimized = false;
bool activated = false;
bool fullscreen = false;
enum zwlr_foreign_toplevel_handle_v1_state *state;
wl_array_for_each(state, states)
{
switch (*state) {
case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED:
maximized = true;
break;
case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED:
minimized = true;
break;
case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED:
activated = true;
break;
case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN:
fullscreen = true;
break;
}
}
mtx_lock(&top->mod->lock);
{
top->maximized = maximized;
top->minimized = minimized;
top->activated = activated;
top->fullscreen = fullscreen;
}
mtx_unlock(&top->mod->lock);
}
static void
done(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle)
{
struct toplevel *top = data;
const struct bar *bar = top->mod->bar;
bar->refresh(bar);
}
static void
closed(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle)
{
struct toplevel *top = data;
struct module *mod = top->mod;
struct private *m = mod->private;
mtx_lock(&mod->lock);
tll_foreach(m->toplevels, it)
{
if (it->item.handle == handle) {
toplevel_free(top);
tll_remove(m->toplevels, it);
break;
}
}
mtx_unlock(&mod->lock);
const struct bar *bar = mod->bar;
bar->refresh(bar);
}
static void
parent(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct zwlr_foreign_toplevel_handle_v1 *parent)
{
}
static const struct zwlr_foreign_toplevel_handle_v1_listener toplevel_listener = {
.title = &title,
.app_id = &app_id,
.output_enter = &output_enter,
.output_leave = &output_leave,
.state = &state,
.done = &done,
.closed = &closed,
.parent = &parent,
};
static void
toplevel(void *data, struct zwlr_foreign_toplevel_manager_v1 *manager, struct zwlr_foreign_toplevel_handle_v1 *handle)
{
struct module *mod = data;
struct private *m = mod->private;
struct toplevel toplevel = {
.mod = mod,
.handle = handle,
};
mtx_lock(&mod->lock);
{
tll_push_back(m->toplevels, toplevel);
zwlr_foreign_toplevel_handle_v1_add_listener(handle, &toplevel_listener, &tll_back(m->toplevels));
}
mtx_unlock(&mod->lock);
}
static void
finished(void *data, struct zwlr_foreign_toplevel_manager_v1 *manager)
{
struct module *mod = data;
struct private *m = mod->private;
assert(m->manager == manager);
zwlr_foreign_toplevel_manager_v1_destroy(m->manager);
m->manager = NULL;
}
static const struct zwlr_foreign_toplevel_manager_v1_listener manager_listener = {
.toplevel = &toplevel,
.finished = &finished,
};
static void
output_xdg_output(struct output *output)
{
struct private *m = output->mod->private;
if (m->xdg_output_manager == NULL)
return;
if (output->xdg_output != NULL)
return;
output->xdg_output = zxdg_output_manager_v1_get_xdg_output(m->xdg_output_manager, output->wl_output);
zxdg_output_v1_add_listener(output->xdg_output, &xdg_output_listener, output);
}
static void
handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version)
{
struct module *mod = data;
struct private *m = mod->private;
if (strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0) {
if (!verify_iface_version(interface, version, required_manager_interface_version))
return;
m->manager_wl_name = name;
}
else if (strcmp(interface, wl_output_interface.name) == 0) {
const uint32_t required = 3;
if (!verify_iface_version(interface, version, required))
return;
struct output output = {
.mod = mod,
.wl_name = name,
.wl_output = wl_registry_bind(registry, name, &wl_output_interface, required),
};
mtx_lock(&mod->lock);
tll_push_back(m->outputs, output);
output_xdg_output(&tll_back(m->outputs));
mtx_unlock(&mod->lock);
}
else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) {
const uint32_t required = 2;
if (!verify_iface_version(interface, version, required))
return;
m->xdg_output_manager = wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, required);
mtx_lock(&mod->lock);
tll_foreach(m->outputs, it) output_xdg_output(&it->item);
mtx_unlock(&mod->lock);
}
}
static void
handle_global_remove(void *data, struct wl_registry *registry, uint32_t name)
{
struct module *mod = data;
struct private *m = mod->private;
mtx_lock(&mod->lock);
tll_foreach(m->outputs, it)
{
const struct output *output = &it->item;
if (output->wl_name == name) {
/* Loop all toplevels */
tll_foreach(m->toplevels, it2)
{
/* And remove this output from their list of tracked
* outputs */
tll_foreach(it2->item.outputs, it3)
{
if (it3->item == output) {
tll_remove(it2->item.outputs, it3);
break;
}
}
}
tll_remove(m->outputs, it);
goto out;
}
}
out:
mtx_unlock(&mod->lock);
}
static const struct wl_registry_listener registry_listener = {
.global = &handle_global,
.global_remove = &handle_global_remove,
};
static int
run(struct module *mod)
{
struct private *m = mod->private;
int ret = -1;
struct wl_display *display = NULL;
struct wl_registry *registry = NULL;
if ((display = wl_display_connect(NULL)) == NULL) {
LOG_ERR("no Wayland compositor running");
goto out;
}
if ((registry = wl_display_get_registry(display)) == NULL
|| wl_registry_add_listener(registry, &registry_listener, mod) != 0) {
LOG_ERR("failed to get Wayland registry");
goto out;
}
wl_display_roundtrip(display);
if (m->manager_wl_name == 0) {
LOG_ERR("compositor does not implement the foreign-toplevel-manager interface");
goto out;
}
m->manager = wl_registry_bind(registry, m->manager_wl_name, &zwlr_foreign_toplevel_manager_v1_interface,
required_manager_interface_version);
zwlr_foreign_toplevel_manager_v1_add_listener(m->manager, &manager_listener, mod);
while (true) {
wl_display_flush(display);
struct pollfd fds[] = {
{.fd = mod->abort_fd, .events = POLLIN},
{.fd = wl_display_get_fd(display), .events = POLLIN},
};
int r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1);
if (r < 0) {
if (errno == EINTR)
continue;
LOG_ERRNO("failed to poll");
break;
}
if (fds[0].revents & (POLLIN | POLLHUP)) {
ret = 0;
break;
}
if (fds[1].revents & POLLHUP) {
LOG_ERR("disconnected from the Wayland compositor");
break;
}
assert(fds[1].revents & POLLIN);
wl_display_dispatch(display);
}
out:
tll_foreach(m->toplevels, it)
{
toplevel_free(&it->item);
tll_remove(m->toplevels, it);
}
tll_foreach(m->outputs, it)
{
output_free(&it->item);
tll_remove(m->outputs, it);
}
if (m->xdg_output_manager != NULL)
zxdg_output_manager_v1_destroy(m->xdg_output_manager);
if (m->manager != NULL)
zwlr_foreign_toplevel_manager_v1_destroy(m->manager);
if (registry != NULL)
wl_registry_destroy(registry);
if (display != NULL)
wl_display_disconnect(display);
return ret;
}
static struct module *
ftop_new(struct particle *label, bool all_monitors)
{
struct private *m = calloc(1, sizeof(*m));
m->template = label;
m->all_monitors = all_monitors;
struct module *mod = module_common_new();
mod->private = m;
mod->run = &run;
mod->destroy = &destroy;
mod->content = &content;
mod->description = &description;
return mod;
}
static struct module *
from_conf(const struct yml_node *node, struct conf_inherit inherited)
{
const struct yml_node *c = yml_get_value(node, "content");
const struct yml_node *all_monitors = yml_get_value(node, "all-monitors");
return ftop_new(conf_to_particle(c, inherited), all_monitors != NULL ? yml_value_as_bool(all_monitors) : false);
}
static bool
verify_conf(keychain_t *chain, const struct yml_node *node)
{
static const struct attr_info attrs[] = {
{"all-monitors", false, &conf_verify_bool},
MODULE_COMMON_ATTRS,
};
return conf_verify_dict(chain, node, attrs);
}
const struct module_iface module_foreign_toplevel_iface = {
.verify_conf = &verify_conf,
.from_conf = &from_conf,
};
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
extern const struct module_iface iface __attribute__((weak, alias("module_foreign_toplevel_iface")));
#endif

View file

@ -1,15 +1,15 @@
#include "i3-common.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <poll.h>
#if defined(ENABLE_X11)
#include <xcb/xcb.h>
#include <xcb/xcb_aux.h>
#include <xcb/xcb.h>
#include <xcb/xcb_aux.h>
#endif
#include <json-c/json_tokener.h>
@ -19,7 +19,7 @@
#include "../log.h"
#if defined(ENABLE_X11)
#include "../xcb.h"
#include "../xcb.h"
#endif
#include "i3-ipc.h"
@ -41,14 +41,11 @@ get_socket_address_x11(struct sockaddr_un *addr)
xcb_atom_t atom = get_atom(conn, "I3_SOCKET_PATH");
assert(atom != XCB_ATOM_NONE);
xcb_get_property_cookie_t cookie
= xcb_get_property_unchecked(
conn, false, screen->root, atom,
XCB_GET_PROPERTY_TYPE_ANY, 0, sizeof(addr->sun_path));
xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(conn, false, screen->root, atom,
XCB_GET_PROPERTY_TYPE_ANY, 0, sizeof(addr->sun_path));
xcb_generic_error_t *err = NULL;
xcb_get_property_reply_t *reply =
xcb_get_property_reply(conn, cookie, &err);
xcb_get_property_reply_t *reply = xcb_get_property_reply(conn, cookie, &err);
bool ret = false;
if (err != NULL) {
@ -102,11 +99,7 @@ bool
i3_send_pkg(int sock, int cmd, char *data)
{
const size_t size = data != NULL ? strlen(data) : 0;
const i3_ipc_header_t hdr = {
.magic = I3_IPC_MAGIC,
.size = size,
.type = cmd
};
const i3_ipc_header_t hdr = {.magic = I3_IPC_MAGIC, .size = size, .type = cmd};
if (write(sock, &hdr, sizeof(hdr)) != (ssize_t)sizeof(hdr))
return false;
@ -120,8 +113,7 @@ i3_send_pkg(int sock, int cmd, char *data)
}
bool
i3_receive_loop(int abort_fd, int sock,
const struct i3_ipc_callbacks *cbs, void *data)
i3_receive_loop(int abort_fd, int sock, const struct i3_ipc_callbacks *cbs, void *data)
{
/* Initial reply typically requires a couple of KB. But we often
* need more later. For example, switching workspaces can result
@ -133,10 +125,7 @@ i3_receive_loop(int abort_fd, int sock,
bool err = false;
while (!err) {
struct pollfd fds[] = {
{.fd = abort_fd, .events = POLLIN},
{.fd = sock, .events = POLLIN}
};
struct pollfd fds[] = {{.fd = abort_fd, .events = POLLIN}, {.fd = sock, .events = POLLIN}};
int res = poll(fds, 2, -1);
if (res <= 0) {
@ -159,13 +148,11 @@ i3_receive_loop(int abort_fd, int sock,
/* Grow receive buffer, if necessary */
if (buf_idx == reply_buf_size) {
LOG_DBG("growing reply buffer: %zu -> %zu",
reply_buf_size, reply_buf_size * 2);
LOG_DBG("growing reply buffer: %zu -> %zu", reply_buf_size, reply_buf_size * 2);
char *new_buf = realloc(buf, reply_buf_size * 2);
if (new_buf == NULL) {
LOG_ERR("failed to grow reply buffer from %zu to %zu bytes",
reply_buf_size, reply_buf_size * 2);
LOG_ERR("failed to grow reply buffer from %zu to %zu bytes", reply_buf_size, reply_buf_size * 2);
err = true;
break;
}
@ -188,10 +175,8 @@ i3_receive_loop(int abort_fd, int sock,
while (!err && buf_idx >= sizeof(i3_ipc_header_t)) {
const i3_ipc_header_t *hdr = (const i3_ipc_header_t *)buf;
if (strncmp(hdr->magic, I3_IPC_MAGIC, sizeof(hdr->magic)) != 0) {
LOG_ERR(
"i3 IPC header magic mismatch: expected \"%.*s\", got \"%.*s\"",
(int)sizeof(hdr->magic), I3_IPC_MAGIC,
(int)sizeof(hdr->magic), hdr->magic);
LOG_ERR("i3 IPC header magic mismatch: expected \"%.*s\", got \"%.*s\"", (int)sizeof(hdr->magic),
I3_IPC_MAGIC, (int)sizeof(hdr->magic), hdr->magic);
err = true;
break;
@ -210,10 +195,10 @@ i3_receive_loop(int abort_fd, int sock,
char json_str[hdr->size + 1];
memcpy(json_str, &buf[sizeof(*hdr)], hdr->size);
json_str[hdr->size] = '\0';
//printf("raw: %s\n", json_str);
// printf("raw: %s\n", json_str);
LOG_DBG("raw: %s\n", json_str);
//json_tokener *tokener = json_tokener_new();
// json_tokener *tokener = json_tokener_new();
struct json_object *json = json_tokener_parse(json_str);
if (json == NULL) {
LOG_ERR("failed to parse json");
@ -262,13 +247,13 @@ i3_receive_loop(int abort_fd, int sock,
break;
#endif
/* Sway extensions */
case 100: /* IPC_GET_INPUTS */
case 100: /* IPC_GET_INPUTS */
pkt_handler = cbs->reply_inputs;
break;
/*
* Events
*/
/*
* Events
*/
case I3_IPC_EVENT_WORKSPACE:
pkt_handler = cbs->event_workspace;
@ -295,7 +280,7 @@ i3_receive_loop(int abort_fd, int sock,
pkt_handler = cbs->event_tick;
break;
/* Sway extensions */
/* Sway extensions */
#define SWAY_IPC_EVENT_INPUT ((1u << 31) | 21)
case SWAY_IPC_EVENT_INPUT:
pkt_handler = cbs->event_input;
@ -309,7 +294,7 @@ i3_receive_loop(int abort_fd, int sock,
}
if (pkt_handler != NULL)
err = !pkt_handler(hdr->type, json, data);
err = !pkt_handler(sock, hdr->type, json, data);
else
LOG_DBG("no handler for reply/event %d; ignoring", hdr->type);

View file

@ -2,8 +2,8 @@
#include <stdbool.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <json-c/json_util.h>
@ -11,7 +11,7 @@
bool i3_get_socket_address(struct sockaddr_un *addr);
bool i3_send_pkg(int sock, int cmd, char *data);
typedef bool (*i3_ipc_callback_t)(int type, const struct json_object *json, void *data);
typedef bool (*i3_ipc_callback_t)(int sock, int type, const struct json_object *json, void *data);
struct i3_ipc_callbacks {
void (*burst_done)(void *data);
@ -43,6 +43,4 @@ struct i3_ipc_callbacks {
i3_ipc_callback_t event_input;
};
bool i3_receive_loop(
int abort_fd, int sock,
const struct i3_ipc_callbacks *callbacks, void *data);
bool i3_receive_loop(int abort_fd, int sock, const struct i3_ipc_callbacks *callbacks, void *data);

View file

@ -1,27 +1,27 @@
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <threads.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/types.h>
#include <tllist.h>
#define LOG_MODULE "i3"
#define LOG_ENABLE_DBG 0
#include "../log.h"
#include "../bar/bar.h"
#include "../config.h"
#include "../config-verify.h"
#include "../config.h"
#include "../log.h"
#include "../particles/dynlist.h"
#include "../plugin.h"
#include "i3-ipc.h"
#include "i3-common.h"
#include "i3-ipc.h"
enum sort_mode {SORT_NONE, SORT_ASCENDING, SORT_DESCENDING};
enum sort_mode { SORT_NONE, SORT_NATIVE, SORT_ASCENDING, SORT_DESCENDING };
struct ws_content {
char *name;
@ -29,13 +29,16 @@ struct ws_content {
};
struct workspace {
int id;
char *name;
int name_as_int; /* -1 is name is not a decimal number */
int name_as_int; /* -1 if name is not a decimal number */
bool persistent;
char *output;
bool visible;
bool focused;
bool urgent;
bool empty;
struct {
unsigned id;
@ -45,7 +48,8 @@ struct workspace {
} window;
};
struct private {
struct private
{
int left_spacing;
int right_spacing;
@ -58,22 +62,61 @@ struct private {
size_t count;
} ws_content;
bool strip_workspace_numbers;
enum sort_mode sort_mode;
tll(struct workspace) workspaces;
size_t persistent_count;
char **persistent_workspaces;
};
static int
workspace_name_as_int(const char *name)
{
int name_as_int = 0;
/* First check for N:name pattern (set $ws1 “1:foobar”) */
const char *colon = strchr(name, ':');
if (colon != NULL) {
for (const char *p = name; p < colon; p++) {
if (!(*p >= '0' && *p < '9'))
return -1;
name_as_int *= 10;
name_as_int += *p - '0';
}
return name_as_int;
}
/* Then, if the name is a number *only* (set $ws1 1) */
for (const char *p = name; *p != '\0'; p++) {
if (!(*p >= '0' && *p <= '9'))
return -1;
name_as_int *= 10;
name_as_int += *p - '0';
}
return name_as_int;
}
static bool
workspace_from_json(const struct json_object *json, struct workspace *ws)
{
/* Always present */
struct json_object *name, *output;
if (!json_object_object_get_ex(json, "name", &name) ||
!json_object_object_get_ex(json, "output", &output))
{
LOG_ERR("workspace reply/event without 'name' and/or 'output' property");
struct json_object *id, *name, *output;
if (!json_object_object_get_ex(json, "id", &id) || !json_object_object_get_ex(json, "name", &name)
|| !json_object_object_get_ex(json, "output", &output)) {
LOG_ERR("workspace reply/event without 'name' and/or 'output' "
"properties");
return false;
}
/* Sway only */
struct json_object *focus = NULL;
json_object_object_get_ex(json, "focus", &focus);
/* Optional */
struct json_object *visible = NULL, *focused = NULL, *urgent = NULL;
json_object_object_get_ex(json, "visible", &visible);
@ -82,24 +125,21 @@ workspace_from_json(const struct json_object *json, struct workspace *ws)
const char *name_as_string = json_object_get_string(name);
int name_as_int = 0;
for (const char *p = name_as_string; *p != '\0'; p++) {
if (!(*p >= '0' && *p <= '9')) {
name_as_int = -1;
break;
}
const size_t node_count = focus != NULL ? json_object_array_length(focus) : 0;
name_as_int *= 10;
name_as_int += *p - '0';
}
const bool is_empty = node_count == 0;
int name_as_int = workspace_name_as_int(name_as_string);
*ws = (struct workspace) {
*ws = (struct workspace){
.id = json_object_get_int(id),
.name = strdup(name_as_string),
.name_as_int = name_as_int,
.persistent = false,
.output = strdup(json_object_get_string(output)),
.visible = json_object_get_boolean(visible),
.focused = json_object_get_boolean(focused),
.urgent = json_object_get_boolean(urgent),
.empty = is_empty && json_object_get_boolean(focused),
.window = {.title = NULL, .pid = -1},
};
@ -107,22 +147,36 @@ workspace_from_json(const struct json_object *json, struct workspace *ws)
}
static void
workspace_free(struct workspace *ws)
workspace_free_persistent(struct workspace *ws)
{
free(ws->name);
free(ws->output);
ws->output = NULL;
free(ws->window.title);
ws->window.title = NULL;
free(ws->window.application);
ws->window.application = NULL;
ws->id = -1;
}
static void
workspaces_free(struct private *m)
workspace_free(struct workspace *ws)
{
tll_foreach(m->workspaces, it)
workspace_free(&it->item);
tll_free(m->workspaces);
workspace_free_persistent(ws);
free(ws->name);
ws->name = NULL;
}
static void
workspaces_free(struct private *m, bool free_persistent)
{
tll_foreach(m->workspaces, it)
{
if (free_persistent || !it->item.persistent) {
workspace_free(&it->item);
tll_remove(m->workspaces, it);
}
}
}
static void
workspace_add(struct private *m, struct workspace ws)
@ -132,9 +186,26 @@ workspace_add(struct private *m, struct workspace ws)
tll_push_back(m->workspaces, ws);
return;
case SORT_NATIVE:
if (ws.name_as_int >= 0) {
tll_foreach(m->workspaces, it)
{
if (it->item.name_as_int < 0)
continue;
if (it->item.name_as_int > ws.name_as_int) {
tll_insert_before(m->workspaces, it, ws);
return;
}
}
};
tll_push_back(m->workspaces, ws);
return;
case SORT_ASCENDING:
if (ws.name_as_int >= 0) {
tll_foreach(m->workspaces, it) {
tll_foreach(m->workspaces, it)
{
if (it->item.name_as_int < 0)
continue;
if (it->item.name_as_int > ws.name_as_int) {
@ -143,10 +214,9 @@ workspace_add(struct private *m, struct workspace ws)
}
}
} else {
tll_foreach(m->workspaces, it) {
if (strcoll(it->item.name, ws.name) > 0 ||
it->item.name_as_int >= 0)
{
tll_foreach(m->workspaces, it)
{
if (strcoll(it->item.name, ws.name) > 0 || it->item.name_as_int >= 0) {
tll_insert_before(m->workspaces, it, ws);
return;
}
@ -157,14 +227,16 @@ workspace_add(struct private *m, struct workspace ws)
case SORT_DESCENDING:
if (ws.name_as_int >= 0) {
tll_foreach(m->workspaces, it) {
tll_foreach(m->workspaces, it)
{
if (it->item.name_as_int < ws.name_as_int) {
tll_insert_before(m->workspaces, it, ws);
return;
}
}
} else {
tll_foreach(m->workspaces, it) {
tll_foreach(m->workspaces, it)
{
if (it->item.name_as_int >= 0)
continue;
if (strcoll(it->item.name, ws.name) < 0) {
@ -179,12 +251,13 @@ workspace_add(struct private *m, struct workspace ws)
}
static void
workspace_del(struct private *m, const char *name)
workspace_del(struct private *m, int id)
{
tll_foreach(m->workspaces, it) {
tll_foreach(m->workspaces, it)
{
struct workspace *ws = &it->item;
if (strcmp(ws->name, name) != 0)
if (ws->id != id)
continue;
workspace_free(ws);
@ -194,9 +267,22 @@ workspace_del(struct private *m, const char *name)
}
static struct workspace *
workspace_lookup(struct private *m, const char *name)
workspace_lookup(struct private *m, int id)
{
tll_foreach(m->workspaces, it) {
tll_foreach(m->workspaces, it)
{
struct workspace *ws = &it->item;
if (ws->id == id)
return ws;
}
return NULL;
}
static struct workspace *
workspace_lookup_by_name(struct private *m, const char *name)
{
tll_foreach(m->workspaces, it)
{
struct workspace *ws = &it->item;
if (strcmp(ws->name, name) == 0)
return ws;
@ -205,7 +291,7 @@ workspace_lookup(struct private *m, const char *name)
}
static bool
handle_get_version_reply(int type, const struct json_object *json, void *_m)
handle_get_version_reply(int sock, int type, const struct json_object *json, void *_m)
{
struct json_object *version;
if (!json_object_object_get_ex(json, "human_readable", &version)) {
@ -218,7 +304,7 @@ handle_get_version_reply(int type, const struct json_object *json, void *_m)
}
static bool
handle_subscribe_reply(int type, const struct json_object *json, void *_m)
handle_subscribe_reply(int sock, int type, const struct json_object *json, void *_m)
{
struct json_object *success;
if (!json_object_object_get_ex(json, "success", &success)) {
@ -235,36 +321,88 @@ handle_subscribe_reply(int type, const struct json_object *json, void *_m)
}
static bool
handle_get_workspaces_reply(int type, const struct json_object *json, void *_mod)
workspace_update_or_add(struct private *m, const struct json_object *ws_json)
{
struct json_object *_id;
if (!json_object_object_get_ex(ws_json, "id", &_id))
return false;
const int id = json_object_get_int(_id);
struct workspace *already_exists = workspace_lookup(m, id);
if (already_exists == NULL) {
/*
* No workspace with this ID.
*
* Try looking it up again, but this time using the name. If
* we get a match, check if its an empty, persistent
* workspace, and if so, use it.
*
* This is necessary, since empty, persistent workspaces dont
* exist in the i3/Sway server, and thus we dont _have_ an
* ID.
*/
struct json_object *_name;
if (json_object_object_get_ex(ws_json, "name", &_name)) {
const char *name = json_object_get_string(_name);
if (name != NULL) {
struct workspace *maybe_persistent = workspace_lookup_by_name(m, name);
if (maybe_persistent != NULL && maybe_persistent->persistent && maybe_persistent->id < 0) {
already_exists = maybe_persistent;
}
}
}
}
if (already_exists != NULL) {
bool persistent = already_exists->persistent;
assert(persistent);
workspace_free(already_exists);
if (!workspace_from_json(ws_json, already_exists))
return false;
already_exists->persistent = persistent;
} else {
struct workspace ws;
if (!workspace_from_json(ws_json, &ws))
return false;
workspace_add(m, ws);
}
return true;
}
static bool
handle_get_workspaces_reply(int sock, int type, const struct json_object *json, void *_mod)
{
struct module *mod = _mod;
struct private *m = mod->private;
mtx_lock(&mod->lock);
workspaces_free(m);
workspaces_free(m, false);
m->dirty = true;
size_t count = json_object_array_length(json);
for (size_t i = 0; i < count; i++) {
struct workspace ws = {};
if (!workspace_from_json(json_object_array_get_idx(json, i), &ws)) {
workspaces_free(m);
mtx_unlock(&mod->lock);
return false;
}
LOG_DBG("#%zu: %s", i, m->workspaces.v[i].name);
workspace_add(m, ws);
if (!workspace_update_or_add(m, json_object_array_get_idx(json, i)))
goto err;
}
mtx_unlock(&mod->lock);
return true;
err:
workspaces_free(m, false);
mtx_unlock(&mod->lock);
return false;
}
static bool
handle_workspace_event(int type, const struct json_object *json, void *_mod)
handle_workspace_event(int sock, int type, const struct json_object *json, void *_mod)
{
struct module *mod = _mod;
struct private *m = mod->private;
@ -280,67 +418,59 @@ handle_workspace_event(int type, const struct json_object *json, void *_mod)
bool is_init = strcmp(change_str, "init") == 0;
bool is_empty = strcmp(change_str, "empty") == 0;
bool is_focused = strcmp(change_str, "focus") == 0;
bool is_rename = strcmp(change_str, "rename") == 0;
bool is_move = strcmp(change_str, "move") == 0;
bool is_urgent = strcmp(change_str, "urgent") == 0;
bool is_reload = strcmp(change_str, "reload") == 0;
if (is_reload) {
LOG_WARN("unimplemented: 'reload' event");
return true;
}
struct json_object *current, *_current_name;
if (!json_object_object_get_ex(json, "current", &current) ||
!json_object_object_get_ex(current, "name", &_current_name))
{
LOG_ERR("workspace event without 'current' and/or 'name' properties");
struct json_object *current, *_current_id;
if ((!json_object_object_get_ex(json, "current", &current)
|| !json_object_object_get_ex(current, "id", &_current_id))
&& !is_reload) {
LOG_ERR("workspace event without 'current' and/or 'id' properties");
return false;
}
const char *current_name = json_object_get_string(_current_name);
int current_id = json_object_get_int(_current_id);
mtx_lock(&mod->lock);
if (is_init) {
struct workspace *already_exists = workspace_lookup(m, current_name);
if (already_exists != NULL) {
LOG_WARN("workspace 'init' event for already existing workspace: %s", current_name);
workspace_free(already_exists);
if (!workspace_from_json(current, already_exists))
goto err;
} else {
struct workspace ws;
if (!workspace_from_json(current, &ws))
goto err;
workspace_add(m, ws);
}
if (!workspace_update_or_add(m, current))
goto err;
}
else if (is_empty) {
assert(workspace_lookup(m, current_name) != NULL);
workspace_del(m, current_name);
struct workspace *ws = workspace_lookup(m, current_id);
assert(ws != NULL);
if (!ws->persistent)
workspace_del(m, current_id);
else {
workspace_free_persistent(ws);
ws->empty = true;
}
}
else if (is_focused) {
struct json_object *old, *_old_name, *urgent;
if (!json_object_object_get_ex(json, "old", &old) ||
!json_object_object_get_ex(old, "name", &_old_name) ||
!json_object_object_get_ex(current, "urgent", &urgent))
{
struct json_object *old, *_old_id, *urgent;
if (!json_object_object_get_ex(json, "old", &old) || !json_object_object_get_ex(old, "id", &_old_id)
|| !json_object_object_get_ex(current, "urgent", &urgent)) {
LOG_ERR("workspace 'focused' event without 'old', 'name' and/or 'urgent' property");
mtx_unlock(&mod->lock);
return false;
}
struct workspace *w = workspace_lookup(m, current_name);
struct workspace *w = workspace_lookup(m, current_id);
assert(w != NULL);
LOG_DBG("w: %s", w->name);
/* Mark all workspaces on current's output invisible */
tll_foreach(m->workspaces, it) {
tll_foreach(m->workspaces, it)
{
struct workspace *ws = &it->item;
if (strcmp(ws->output, w->output) == 0)
if (ws->output != NULL && strcmp(ws->output, w->output) == 0)
ws->visible = false;
}
@ -349,12 +479,67 @@ handle_workspace_event(int type, const struct json_object *json, void *_mod)
w->visible = true;
/* Old workspace is no longer focused */
const char *old_name = json_object_get_string(_old_name);
struct workspace *old_w = workspace_lookup(m, old_name);
int old_id = json_object_get_int(_old_id);
struct workspace *old_w = workspace_lookup(m, old_id);
if (old_w != NULL)
old_w->focused = false;
}
else if (is_rename) {
struct workspace *w = workspace_lookup(m, current_id);
assert(w != NULL);
struct json_object *_current_name;
if (!json_object_object_get_ex(current, "name", &_current_name)) {
LOG_ERR("workspace 'rename' event without 'name' property");
mtx_unlock(&mod->lock);
return false;
}
free(w->name);
w->name = strdup(json_object_get_string(_current_name));
w->name_as_int = workspace_name_as_int(w->name);
/* Re-add the workspace to ensure correct sorting */
struct workspace ws = *w;
tll_foreach(m->workspaces, it)
{
if (it->item.id == current_id) {
tll_remove(m->workspaces, it);
break;
}
}
workspace_add(m, ws);
}
else if (is_move) {
struct workspace *w = workspace_lookup(m, current_id);
struct json_object *_current_output;
if (!json_object_object_get_ex(current, "output", &_current_output)) {
LOG_ERR("workspace 'move' event without 'output' property");
mtx_unlock(&mod->lock);
return false;
}
const char *current_output_string = json_object_get_string(_current_output);
/* Ignore fallback_output ("For when there's no connected outputs") */
if (strcmp(current_output_string, "FALLBACK") != 0) {
assert(w != NULL);
free(w->output);
w->output = strdup(current_output_string);
/*
* If the moved workspace was focused, schedule a full update because
* visibility for other workspaces may have changed.
*/
if (w->focused) {
i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_GET_WORKSPACES, NULL);
}
}
}
else if (is_urgent) {
struct json_object *urgent;
if (!json_object_object_get_ex(current, "urgent", &urgent)) {
@ -363,10 +548,20 @@ handle_workspace_event(int type, const struct json_object *json, void *_mod)
return false;
}
struct workspace *w = workspace_lookup(m, current_name);
struct workspace *w = workspace_lookup(m, current_id);
w->urgent = json_object_get_boolean(urgent);
}
else if (is_reload) {
/* Schedule full update to check if anything was changed
* during reload */
i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_GET_WORKSPACES, NULL);
}
else {
LOG_WARN("unimplemented workspace event '%s'", change_str);
}
m->dirty = true;
mtx_unlock(&mod->lock);
return true;
@ -377,7 +572,7 @@ err:
}
static bool
handle_window_event(int type, const struct json_object *json, void *_mod)
handle_window_event(int sock, int type, const struct json_object *json, void *_mod)
{
struct module *mod = _mod;
struct private *m = mod->private;
@ -399,8 +594,9 @@ handle_window_event(int type, const struct json_object *json, void *_mod)
mtx_lock(&mod->lock);
struct workspace *ws = NULL;
size_t focused = 0;
tll_foreach(m->workspaces, it) {
__attribute__((unused)) size_t focused = 0;
tll_foreach(m->workspaces, it)
{
if (it->item.focused) {
ws = &it->item;
focused++;
@ -410,6 +606,20 @@ handle_window_event(int type, const struct json_object *json, void *_mod)
assert(focused == 1);
assert(ws != NULL);
struct json_object *container, *id, *name;
if (!json_object_object_get_ex(json, "container", &container) || !json_object_object_get_ex(container, "id", &id)
|| !json_object_object_get_ex(container, "name", &name)) {
mtx_unlock(&mod->lock);
LOG_ERR("window event without 'container' with 'id' and 'name'");
return false;
}
if ((is_close || is_title) && ws->window.id != json_object_get_int(id)) {
/* Ignore close event and title changed event if it's not current window */
mtx_unlock(&mod->lock);
return true;
}
if (is_close) {
free(ws->window.title);
free(ws->window.application);
@ -421,23 +631,6 @@ handle_window_event(int type, const struct json_object *json, void *_mod)
m->dirty = true;
mtx_unlock(&mod->lock);
return true;
}
struct json_object *container, *id, *name;
if (!json_object_object_get_ex(json, "container", &container) ||
!json_object_object_get_ex(container, "id", &id) ||
!json_object_object_get_ex(container, "name", &name))
{
mtx_unlock(&mod->lock);
LOG_ERR("window event without 'container' with 'id' and 'name'");
return false;
}
if (is_title && ws->window.id != json_object_get_int(id)) {
/* Ignore title changed event if it's not current window */
mtx_unlock(&mod->lock);
return true;
}
free(ws->window.title);
@ -458,27 +651,24 @@ handle_window_event(int type, const struct json_object *json, void *_mod)
struct json_object *app_id;
struct json_object *pid;
if (json_object_object_get_ex(container, "app_id", &app_id) &&
json_object_get_string(app_id) != NULL)
{
if (json_object_object_get_ex(container, "app_id", &app_id) && json_object_get_string(app_id) != NULL) {
free(ws->window.application);
ws->window.application = strdup(json_object_get_string(app_id));
LOG_DBG("application: \"%s\", via 'app_id'", ws->window.application);
}
/* If PID has changed, update application name from /proc/<pid>/comm */
else if (json_object_object_get_ex(container, "pid", &pid) &&
ws->window.pid != json_object_get_int(pid))
{
else if (json_object_object_get_ex(container, "pid", &pid) && ws->window.pid != json_object_get_int(pid)) {
ws->window.pid = json_object_get_int(pid);
char path[64];
snprintf(path, sizeof(path), "/proc/%u/comm", ws->window.pid);
int fd = open(path, O_RDONLY);
int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd == -1) {
/* Application may simply have terminated */
free(ws->window.application); ws->window.application = NULL;
free(ws->window.application);
ws->window.application = NULL;
ws->window.pid = -1;
m->dirty = true;
@ -503,7 +693,7 @@ handle_window_event(int type, const struct json_object *json, void *_mod)
}
static bool
handle_mode_event(int type, const struct json_object *json, void *_mod)
handle_mode_event(int sock, int type, const struct json_object *json, void *_mod)
{
struct module *mod = _mod;
struct private *m = mod->private;
@ -545,7 +735,7 @@ run(struct module *mod)
if (!i3_get_socket_address(&addr))
return 1;
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (sock == -1) {
LOG_ERRNO("failed to create UNIX socket");
return 1;
@ -558,6 +748,27 @@ run(struct module *mod)
return 1;
}
struct private *m = mod->private;
for (size_t i = 0; i < m->persistent_count; i++) {
const char *name_as_string = m->persistent_workspaces[i];
int name_as_int = workspace_name_as_int(name_as_string);
if (m->strip_workspace_numbers) {
const char *colon = strchr(name_as_string, ':');
if (colon != NULL)
name_as_string = colon++;
}
struct workspace ws = {
.id = -1,
.name = strdup(name_as_string),
.name_as_int = name_as_int,
.persistent = true,
.empty = true,
};
workspace_add(m, ws);
}
i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_GET_VERSION, NULL);
i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_SUBSCRIBE, "[\"workspace\", \"window\", \"mode\"]");
i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_GET_WORKSPACES, NULL);
@ -589,7 +800,11 @@ destroy(struct module *mod)
}
free(m->ws_content.v);
workspaces_free(m);
workspaces_free(m, true);
for (size_t i = 0; i < m->persistent_count; i++)
free(m->persistent_workspaces[i]);
free(m->persistent_workspaces);
free(m->mode);
free(m);
@ -609,7 +824,7 @@ ws_content_for_name(struct private *m, const char *name)
}
static const char *
description(struct module *mod)
description(const struct module *mod)
{
return "i3/sway";
}
@ -625,30 +840,47 @@ content(struct module *mod)
struct exposable *particles[tll_length(m->workspaces) + 1];
struct exposable *current = NULL;
tll_foreach(m->workspaces, it) {
tll_foreach(m->workspaces, it)
{
struct workspace *ws = &it->item;
const struct ws_content *template = NULL;
/* Lookup content template for workspace. Fall back to default
* template if this workspace doesn't have a specific
* template */
if (ws->name == NULL) {
LOG_ERR("%d %d", ws->name_as_int, ws->id);
}
template = ws_content_for_name(m, ws->name);
if (template == NULL) {
LOG_DBG("no ws template for %s, using default template", ws->name);
template = ws_content_for_name(m, "");
}
const char *state =
ws->urgent ? "urgent" :
ws->visible ? ws->focused ? "focused" : "unfocused" :
"invisible";
const char *state = ws->urgent ? "urgent" : ws->visible ? ws->focused ? "focused" : "unfocused" : "invisible";
LOG_DBG("name=%s (name-as-int=%d): visible=%s, focused=%s, urgent=%s, empty=%s, state=%s, "
"application=%s, title=%s, mode=%s",
ws->name, ws->name_as_int, ws->visible ? "yes" : "no", ws->focused ? "yes" : "no",
ws->urgent ? "yes" : "no", ws->empty ? "yes" : "no", state, ws->window.application, ws->window.title,
m->mode);
const char *name = ws->name;
if (m->strip_workspace_numbers) {
const char *colon = strchr(name, ':');
if (colon != NULL)
name = colon + 1;
}
struct tag_set tags = {
.tags = (struct tag *[]){
tag_new_string(mod, "name", ws->name),
tag_new_string(mod, "name", name),
tag_new_string(mod, "output", ws->output),
tag_new_bool(mod, "visible", ws->visible),
tag_new_bool(mod, "focused", ws->focused),
tag_new_bool(mod, "urgent", ws->urgent),
tag_new_bool(mod, "empty", ws->empty),
tag_new_string(mod, "state", state),
tag_new_string(mod, "application", ws->window.application),
@ -656,7 +888,7 @@ content(struct module *mod)
tag_new_string(mod, "mode", m->mode),
},
.count = 8,
.count = 10,
};
if (ws->focused) {
@ -666,12 +898,9 @@ content(struct module *mod)
}
if (template == NULL) {
LOG_WARN(
"no ws template for %s, and no default template available",
ws->name);
LOG_WARN("no ws template for %s, and no default template available", ws->name);
} else {
particles[particle_count++] = template->content->instantiate(
template->content, &tags);
particles[particle_count++] = template->content->instantiate(template->content, &tags);
}
tag_set_destroy(&tags);
@ -681,8 +910,7 @@ content(struct module *mod)
particles[particle_count++] = current;
mtx_unlock(&mod->lock);
return dynlist_exposable_new(
particles, particle_count, m->left_spacing, m->right_spacing);
return dynlist_exposable_new(particles, particle_count, m->left_spacing, m->right_spacing);
}
/* Maps workspace name to a content particle. */
@ -692,8 +920,9 @@ struct i3_workspaces {
};
static struct module *
i3_new(struct i3_workspaces workspaces[], size_t workspace_count,
int left_spacing, int right_spacing, enum sort_mode sort_mode)
i3_new(struct i3_workspaces workspaces[], size_t workspace_count, int left_spacing, int right_spacing,
enum sort_mode sort_mode, size_t persistent_count, const char *persistent_workspaces[static persistent_count],
bool strip_workspace_numbers)
{
struct private *m = calloc(1, sizeof(*m));
@ -709,8 +938,15 @@ i3_new(struct i3_workspaces workspaces[], size_t workspace_count,
m->ws_content.v[i].content = workspaces[i].content;
}
m->strip_workspace_numbers = strip_workspace_numbers;
m->sort_mode = sort_mode;
m->persistent_count = persistent_count;
m->persistent_workspaces = calloc(persistent_count, sizeof(m->persistent_workspaces[0]));
for (size_t i = 0; i < persistent_count; i++)
m->persistent_workspaces[i] = strdup(persistent_workspaces[i]);
struct module *mod = module_common_new();
mod->private = m;
mod->run = &run;
@ -728,50 +964,55 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited)
const struct yml_node *left_spacing = yml_get_value(node, "left-spacing");
const struct yml_node *right_spacing = yml_get_value(node, "right-spacing");
const struct yml_node *sort = yml_get_value(node, "sort");
const struct yml_node *persistent = yml_get_value(node, "persistent");
const struct yml_node *strip_workspace_number = yml_get_value(node, "strip-workspace-numbers");
int left = spacing != NULL ? yml_value_as_int(spacing) :
left_spacing != NULL ? yml_value_as_int(left_spacing) : 0;
int right = spacing != NULL ? yml_value_as_int(spacing) :
right_spacing != NULL ? yml_value_as_int(right_spacing) : 0;
int left = spacing != NULL ? yml_value_as_int(spacing) : left_spacing != NULL ? yml_value_as_int(left_spacing) : 0;
int right = spacing != NULL ? yml_value_as_int(spacing)
: right_spacing != NULL ? yml_value_as_int(right_spacing)
: 0;
const char *sort_value = sort != NULL ? yml_value_as_string(sort) : NULL;
enum sort_mode sort_mode =
sort_value == NULL ? SORT_NONE :
strcmp(sort_value, "none") == 0 ? SORT_NONE :
strcmp(sort_value, "ascending") == 0 ? SORT_ASCENDING : SORT_DESCENDING;
enum sort_mode sort_mode = sort_value == NULL ? SORT_NONE
: strcmp(sort_value, "none") == 0 ? SORT_NONE
: strcmp(sort_value, "native") == 0 ? SORT_NATIVE
: strcmp(sort_value, "ascending") == 0 ? SORT_ASCENDING
: SORT_DESCENDING;
const size_t persistent_count = persistent != NULL ? yml_list_length(persistent) : 0;
const char *persistent_workspaces[persistent_count];
if (persistent != NULL) {
size_t idx = 0;
for (struct yml_list_iter it = yml_list_iter(persistent); it.node != NULL; yml_list_next(&it), idx++) {
persistent_workspaces[idx] = yml_value_as_string(it.node);
}
}
struct i3_workspaces workspaces[yml_dict_length(c)];
size_t idx = 0;
for (struct yml_dict_iter it = yml_dict_iter(c);
it.key != NULL;
yml_dict_next(&it), idx++)
{
for (struct yml_dict_iter it = yml_dict_iter(c); it.key != NULL; yml_dict_next(&it), idx++) {
workspaces[idx].name = yml_value_as_string(it.key);
workspaces[idx].content = conf_to_particle(it.value, inherited);
}
return i3_new(workspaces, yml_dict_length(c), left, right, sort_mode);
return i3_new(workspaces, yml_dict_length(c), left, right, sort_mode, persistent_count, persistent_workspaces,
(strip_workspace_number != NULL ? yml_value_as_bool(strip_workspace_number) : false));
}
static bool
verify_content(keychain_t *chain, const struct yml_node *node)
{
if (!yml_is_dict(node)) {
LOG_ERR(
"%s: must be a dictionary of workspace-name: particle mappings",
conf_err_prefix(chain, node));
LOG_ERR("%s: must be a dictionary of workspace-name: particle mappings", conf_err_prefix(chain, node));
return false;
}
for (struct yml_dict_iter it = yml_dict_iter(node);
it.key != NULL;
yml_dict_next(&it))
{
for (struct yml_dict_iter it = yml_dict_iter(node); it.key != NULL; yml_dict_next(&it)) {
const char *key = yml_value_as_string(it.key);
if (key == NULL) {
LOG_ERR("%s: key must be a string (a i3 workspace name)",
conf_err_prefix(chain, it.key));
LOG_ERR("%s: key must be a string (a i3 workspace name)", conf_err_prefix(chain, it.key));
return false;
}
@ -787,18 +1028,25 @@ verify_content(keychain_t *chain, const struct yml_node *node)
static bool
verify_sort(keychain_t *chain, const struct yml_node *node)
{
return conf_verify_enum(
chain, node, (const char *[]){"none", "ascending", "descending"}, 3);
return conf_verify_enum(chain, node, (const char *[]){"none", "native", "ascending", "descending"}, 4);
}
static bool
verify_persistent(keychain_t *chain, const struct yml_node *node)
{
return conf_verify_list(chain, node, &conf_verify_string);
}
static bool
verify_conf(keychain_t *chain, const struct yml_node *node)
{
static const struct attr_info attrs[] = {
{"spacing", false, &conf_verify_int},
{"left-spacing", false, &conf_verify_int},
{"right-spacing", false, &conf_verify_int},
{"spacing", false, &conf_verify_unsigned},
{"left-spacing", false, &conf_verify_unsigned},
{"right-spacing", false, &conf_verify_unsigned},
{"sort", false, &verify_sort},
{"persistent", false, &verify_persistent},
{"strip-workspace-numbers", false, &conf_verify_bool},
{"content", true, &verify_content},
{"anchors", false, NULL},
{NULL, false, NULL},
@ -813,5 +1061,5 @@ const struct module_iface module_i3_iface = {
};
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
extern const struct module_iface iface __attribute__((weak, alias("module_i3_iface"))) ;
extern const struct module_iface iface __attribute__((weak, alias("module_i3_iface")));
#endif

View file

@ -1,16 +1,14 @@
#include <stdlib.h>
#include <assert.h>
#include <stdlib.h>
#include <poll.h>
#include "../config.h"
#include "../config-verify.h"
#include "../config.h"
#include "../module.h"
#include "../plugin.h"
struct private {
struct particle *label;
};
struct private { struct particle *label; };
static void
destroy(struct module *mod)
@ -22,7 +20,7 @@ destroy(struct module *mod)
}
static const char *
description(struct module *mod)
description(const struct module *mod)
{
return "label";
}

200
modules/mem.c Normal file
View file

@ -0,0 +1,200 @@
#include <assert.h>
#include <errno.h>
#include <inttypes.h>
#include <math.h>
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#define LOG_MODULE "mem"
#define LOG_ENABLE_DBG 0
#include "../bar/bar.h"
#include "../config-verify.h"
#include "../config.h"
#include "../log.h"
#include "../plugin.h"
static const long min_poll_interval = 250;
struct private
{
struct particle *label;
uint16_t interval;
uint64_t mem_free;
uint64_t mem_total;
};
static void
destroy(struct module *mod)
{
struct private *m = mod->private;
m->label->destroy(m->label);
free(m);
module_default_destroy(mod);
}
static const char *
description(const struct module *mod)
{
return "mem";
}
static bool
get_mem_stats(uint64_t *mem_free, uint64_t *mem_total)
{
bool mem_total_found = false;
bool mem_free_found = false;
FILE *fp = NULL;
char *line = NULL;
size_t len = 0;
ssize_t read = 0;
fp = fopen("/proc/meminfo", "re");
if (NULL == fp) {
LOG_ERRNO("unable to open /proc/meminfo");
return false;
}
while ((read = getline(&line, &len, fp)) != -1) {
if (strncmp(line, "MemTotal:", sizeof("MemTotal:") - 1) == 0) {
read = sscanf(line + sizeof("MemTotal:") - 1, "%" SCNu64, mem_total);
mem_total_found = (read == 1);
}
if (strncmp(line, "MemAvailable:", sizeof("MemAvailable:") - 1) == 0) {
read = sscanf(line + sizeof("MemAvailable:"), "%" SCNu64, mem_free);
mem_free_found = (read == 1);
}
}
free(line);
fclose(fp);
return mem_free_found && mem_total_found;
}
static struct exposable *
content(struct module *mod)
{
const struct private *p = mod->private;
mtx_lock(&mod->lock);
const uint64_t mem_free = p->mem_free;
const uint64_t mem_total = p->mem_total;
const uint64_t mem_used = mem_total - mem_free;
double percent_used = ((double)mem_used * 100) / (mem_total + 1);
double percent_free = ((double)mem_free * 100) / (mem_total + 1);
struct tag_set tags = {
.tags = (struct tag *[]){tag_new_int(mod, "free", mem_free * 1024), tag_new_int(mod, "used", mem_used * 1024),
tag_new_int(mod, "total", mem_total * 1024),
tag_new_int_range(mod, "percent_free", round(percent_free), 0, 100),
tag_new_int_range(mod, "percent_used", round(percent_used), 0, 100)},
.count = 5,
};
struct exposable *exposable = p->label->instantiate(p->label, &tags);
tag_set_destroy(&tags);
mtx_unlock(&mod->lock);
return exposable;
}
static int
run(struct module *mod)
{
const struct bar *bar = mod->bar;
bar->refresh(bar);
struct private *p = mod->private;
while (true) {
struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}};
int res = poll(fds, 1, p->interval);
if (res < 0) {
if (EINTR == errno) {
continue;
}
LOG_ERRNO("unable to poll abort fd");
return -1;
}
if (fds[0].revents & POLLIN)
break;
mtx_lock(&mod->lock);
p->mem_free = 0;
p->mem_total = 0;
if (!get_mem_stats(&p->mem_free, &p->mem_total)) {
LOG_ERR("unable to retrieve the memory stats");
}
mtx_unlock(&mod->lock);
bar->refresh(bar);
}
return 0;
}
static struct module *
mem_new(uint16_t interval, struct particle *label)
{
struct private *p = calloc(1, sizeof(*p));
p->label = label;
p->interval = interval;
struct module *mod = module_common_new();
mod->private = p;
mod->run = &run;
mod->destroy = &destroy;
mod->content = &content;
mod->description = &description;
return mod;
}
static struct module *
from_conf(const struct yml_node *node, struct conf_inherit inherited)
{
const struct yml_node *interval = yml_get_value(node, "poll-interval");
const struct yml_node *c = yml_get_value(node, "content");
return mem_new(interval == NULL ? min_poll_interval : yml_value_as_int(interval), conf_to_particle(c, inherited));
}
static bool
conf_verify_poll_interval(keychain_t *chain, const struct yml_node *node)
{
if (!conf_verify_unsigned(chain, node))
return false;
if (yml_value_as_int(node) < min_poll_interval) {
LOG_ERR("%s: interval value cannot be less than %ldms", conf_err_prefix(chain, node), min_poll_interval);
return false;
}
return true;
}
static bool
verify_conf(keychain_t *chain, const struct yml_node *node)
{
static const struct attr_info attrs[] = {
{"poll-interval", false, &conf_verify_poll_interval},
MODULE_COMMON_ATTRS,
};
return conf_verify_dict(chain, node, attrs);
}
const struct module_iface module_mem_iface = {
.verify_conf = &verify_conf,
.from_conf = &from_conf,
};
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
extern const struct module_iface iface __attribute__((weak, alias("module_mem_iface")));
#endif

View file

@ -2,35 +2,157 @@ module_sdk = declare_dependency(dependencies: [pixman, threads, tllist, fcft])
modules = []
alsa = dependency('alsa')
udev = dependency('libudev')
json = dependency('json-c')
mpd = dependency('libmpdclient')
xcb_xkb = dependency('xcb-xkb', required: get_option('backend-x11'))
# Optional deps
alsa = dependency('alsa', required: get_option('plugin-alsa'))
plugin_alsa_enabled = alsa.found()
udev_backlight = dependency('libudev', required: get_option('plugin-backlight'))
plugin_backlight_enabled = udev_backlight.found()
udev_battery = dependency('libudev', required: get_option('plugin-battery'))
plugin_battery_enabled = udev_battery.found()
plugin_clock_enabled = get_option('plugin-clock').allowed()
plugin_cpu_enabled = get_option('plugin-cpu').allowed()
plugin_disk_io_enabled = get_option('plugin-disk-io').allowed()
plugin_dwl_enabled = get_option('plugin-dwl').allowed()
plugin_foreign_toplevel_enabled = backend_wayland and get_option('plugin-foreign-toplevel').allowed()
plugin_mem_enabled = get_option('plugin-mem').allowed()
mpd = dependency('libmpdclient', required: get_option('plugin-mpd'))
plugin_mpd_enabled = mpd.found()
# DBus dependency. Used by 'mpris'
sdbus_library = dependency('libsystemd', 'libelogind', 'basu', required: get_option('plugin-mpris'))
plugin_mpris_enabled = sdbus_library.found()
json_i3 = dependency('json-c', required: get_option('plugin-i3'))
plugin_i3_enabled = json_i3.found()
plugin_label_enabled = get_option('plugin-label').allowed()
plugin_network_enabled = get_option('plugin-network').allowed()
pipewire = dependency('libpipewire-0.3', required: get_option('plugin-pipewire'))
json_pipewire = dependency('json-c', required: get_option('plugin-pipewire'))
plugin_pipewire_enabled = pipewire.found() and json_pipewire.found()
pulse = dependency('libpulse', required: get_option('plugin-pulse'))
plugin_pulse_enabled = pulse.found()
udev_removables = dependency('libudev', required: get_option('plugin-removables'))
plugin_removables_enabled = udev_removables.found()
plugin_river_enabled = backend_wayland and get_option('plugin-river').allowed()
plugin_script_enabled = get_option('plugin-script').allowed()
json_sway_xkb = dependency('json-c', required: get_option('plugin-sway-xkb'))
plugin_sway_xkb_enabled = json_sway_xkb.found()
json_niri_language = dependency('json-c', required: get_option('plugin-niri-language'))
plugin_niri_language_enabled = json_niri_language.found()
json_niri_workspaces = dependency('json-c', required: get_option('plugin-niri-workspaces'))
plugin_niri_workspaces_enabled = json_niri_workspaces.found()
xcb_xkb = dependency('xcb-xkb', required: get_option('plugin-xkb'))
plugin_xkb_enabled = backend_x11 and xcb_xkb.found()
plugin_xwindow_enabled = backend_x11 and get_option('plugin-xwindow').allowed()
# Module name -> (source-list, dep-list)
mod_data = {
'alsa': [[], [m, alsa]],
'backlight': [[], [m, udev]],
'battery': [[], [udev]],
'clock': [[], []],
'i3': [['i3-common.c', 'i3-common.h'], [dynlist, json]],
'label': [[], []],
'mpd': [[], [mpd]],
'network': [[], []],
'removables': [[], [dynlist, udev]],
'script': [[], []],
'sway-xkb': [['i3-common.c', 'i3-common.h'], [dynlist, json]],
}
mod_data = {}
if backend_x11
mod_data += {
'xkb': [[], [xcb_stuff, xcb_xkb]],
'xwindow': [[], [xcb_stuff]],
}
if plugin_alsa_enabled
mod_data += {'alsa': [[], [m, alsa]]}
endif
if backend_wayland
if plugin_backlight_enabled
mod_data += {'backlight': [[], [m, udev_backlight]]}
endif
if plugin_battery_enabled
mod_data += {'battery': [[], [udev_battery]]}
endif
if plugin_clock_enabled
mod_data += {'clock': [[], []]}
endif
if plugin_cpu_enabled
mod_data += {'cpu': [[], [m, dynlist]]}
endif
if plugin_disk_io_enabled
mod_data += {'disk-io': [[], [dynlist]]}
endif
if plugin_dwl_enabled
mod_data += {'dwl': [[], [dynlist]]}
endif
if plugin_mem_enabled
mod_data += {'mem': [[], [m]]}
endif
if plugin_mpd_enabled
mod_data += {'mpd': [[], [mpd]]}
endif
if plugin_mpris_enabled
sdbus = declare_dependency(compile_args: ['-DHAVE_' + sdbus_library.name().to_upper()], dependencies:[sdbus_library])
mod_data += {'mpris': [[], [sdbus]]}
endif
if plugin_i3_enabled
mod_data += {'i3': [['i3-common.c', 'i3-common.h'], [dynlist, json_i3]]}
endif
if plugin_label_enabled
mod_data += {'label': [[], []]}
endif
if plugin_network_enabled
mod_data += {'network': [[], [dynlist]]}
endif
if plugin_pipewire_enabled
mod_data += {'pipewire': [[], [m, pipewire, dynlist, json_pipewire]]}
endif
if plugin_pulse_enabled
mod_data += {'pulse': [[], [m, pulse]]}
endif
if plugin_removables_enabled
mod_data += {'removables': [[], [dynlist, udev_removables]]}
endif
if plugin_script_enabled
mod_data += {'script': [[], []]}
endif
if plugin_sway_xkb_enabled
mod_data += {'sway-xkb': [['i3-common.c', 'i3-common.h'], [dynlist, json_sway_xkb]]}
endif
if plugin_niri_language_enabled
mod_data += {'niri-language': [['niri-common.c', 'niri-common.h'], [dynlist, json_niri_language]]}
endif
if plugin_niri_workspaces_enabled
mod_data += {'niri-workspaces': [['niri-common.c', 'niri-common.h'], [dynlist, json_niri_workspaces]]}
endif
if plugin_xkb_enabled
mod_data += {'xkb': [[], [xcb_stuff, xcb_xkb]]}
endif
if plugin_xwindow_enabled
mod_data += {'xwindow': [[], [xcb_stuff]]}
endif
if plugin_river_enabled
river_proto_headers = []
river_proto_src = []
@ -49,9 +171,29 @@ if backend_wayland
command: [wscanner_prog, 'private-code', '@INPUT@', '@OUTPUT@'])
endforeach
mod_data += {
'river': [[wl_proto_src + wl_proto_headers + river_proto_src + river_proto_headers], [dynlist]],
}
mod_data += {'river': [[wl_proto_src + wl_proto_headers + river_proto_src + river_proto_headers], [dynlist, wayland_client]]}
endif
if plugin_foreign_toplevel_enabled
ftop_proto_headers = []
ftop_proto_src = []
foreach prot : ['../external/wlr-foreign-toplevel-management-unstable-v1.xml']
ftop_proto_headers += custom_target(
prot.underscorify() + '-client-header',
output: '@BASENAME@.h',
input: prot,
command: [wscanner_prog, 'client-header', '@INPUT@', '@OUTPUT@'])
ftop_proto_src += custom_target(
prot.underscorify() + '-private-code',
output: '@BASENAME@.c',
input: prot,
command: [wscanner_prog, 'private-code', '@INPUT@', '@OUTPUT@'])
endforeach
mod_data += {'foreign-toplevel': [[wl_proto_src + wl_proto_headers + ftop_proto_headers + ftop_proto_src], [m, dynlist, wayland_client]]}
endif
foreach mod, data : mod_data

View file

@ -1,33 +1,34 @@
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include <time.h>
#include <threads.h>
#include <unistd.h>
#include <assert.h>
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <threads.h>
#include <time.h>
#include <unistd.h>
#include <poll.h>
#include <libgen.h>
#include <poll.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/eventfd.h>
#include <sys/inotify.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <mpd/client.h>
#define LOG_MODULE "mpd"
#define LOG_ENABLE_DBG 0
#include "../log.h"
#include "../bar/bar.h"
#include "../config.h"
#include "../config-verify.h"
#include "../config.h"
#include "../log.h"
#include "../plugin.h"
struct private {
struct private
{
char *host;
uint16_t port;
struct particle *label;
@ -38,10 +39,12 @@ struct private {
bool repeat;
bool random;
bool consume;
int volume;
bool single;
int volume;
char *album;
char *artist;
char *title;
char *file;
struct {
uint64_t value;
@ -59,11 +62,9 @@ destroy(struct module *mod)
struct private *m = mod->private;
if (m->refresh_thread_id != 0) {
assert(m->refresh_abort_fd != -1);
if (write(m->refresh_abort_fd, &(uint64_t){1}, sizeof(uint64_t))
!= sizeof(uint64_t))
{
if (write(m->refresh_abort_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) {
LOG_ERRNO("failed to signal abort to refresher thread");
} else{
} else {
int res;
thrd_join(m->refresh_thread_id, &res);
}
@ -75,6 +76,7 @@ destroy(struct module *mod)
free(m->album);
free(m->artist);
free(m->title);
free(m->file);
assert(m->conn == NULL);
m->label->destroy(m->label);
@ -84,7 +86,7 @@ destroy(struct module *mod)
}
static const char *
description(struct module *mod)
description(const struct module *mod)
{
return "mpd";
}
@ -130,12 +132,11 @@ content(struct module *mod)
if (m->state == MPD_STATE_PLAY) {
elapsed += timespec_diff_milli_seconds(&now, &m->elapsed.when);
if (elapsed > m->duration) {
LOG_DBG(
"dynamic update of elapsed overflowed: "
"elapsed=%"PRIu64", duration=%"PRIu64, elapsed, m->duration);
LOG_DBG("dynamic update of elapsed overflowed: "
"elapsed=%" PRIu64 ", duration=%" PRIu64,
elapsed, m->duration);
elapsed = m->duration;
}
}
unsigned elapsed_secs = elapsed / 1000;
@ -152,16 +153,23 @@ content(struct module *mod)
state_str = "offline";
else {
switch (m->state) {
case MPD_STATE_UNKNOWN: state_str = "unknown"; break;
case MPD_STATE_STOP: state_str = "stopped"; break;
case MPD_STATE_PAUSE: state_str = "paused"; break;
case MPD_STATE_PLAY: state_str = "playing"; break;
case MPD_STATE_UNKNOWN:
state_str = "unknown";
break;
case MPD_STATE_STOP:
state_str = "stopped";
break;
case MPD_STATE_PAUSE:
state_str = "paused";
break;
case MPD_STATE_PLAY:
state_str = "playing";
break;
}
}
/* Tell particle to real-time track? */
enum tag_realtime_unit realtime = m->state == MPD_STATE_PLAY
? TAG_REALTIME_MSECS : TAG_REALTIME_NONE;
enum tag_realtime_unit realtime = m->state == MPD_STATE_PLAY ? TAG_REALTIME_MSECS : TAG_REALTIME_NONE;
struct tag_set tags = {
.tags = (struct tag *[]){
@ -169,17 +177,19 @@ content(struct module *mod)
tag_new_bool(mod, "repeat", m->repeat),
tag_new_bool(mod, "random", m->random),
tag_new_bool(mod, "consume", m->consume),
tag_new_bool(mod, "single", m->single),
tag_new_int_range(mod, "volume", m->volume, 0, 100),
tag_new_string(mod, "album", m->album),
tag_new_string(mod, "artist", m->artist),
tag_new_string(mod, "title", m->title),
tag_new_string(mod, "file", m->file),
tag_new_string(mod, "pos", pos),
tag_new_string(mod, "end", end),
tag_new_int(mod, "duration", m->duration),
tag_new_int_realtime(
mod, "elapsed", elapsed, 0, m->duration, realtime),
},
.count = 12,
.count = 14,
};
mtx_unlock(&mod->lock);
@ -223,7 +233,7 @@ wait_for_socket_create(const struct module *mod)
struct stat st;
if (stat(m->host, &st) == 0 && S_ISSOCK(st.st_mode)) {
int s = socket(AF_UNIX, SOCK_STREAM, 0);
int s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
struct sockaddr_un addr = {.sun_family = AF_UNIX};
strncpy(addr.sun_path, m->host, sizeof(addr.sun_path) - 1);
@ -234,8 +244,7 @@ wait_for_socket_create(const struct module *mod)
LOG_DBG("%s: already exists, and is connectable", m->host);
have_mpd_socket = true;
} else {
LOG_DBG("%s: already exists, but isn't connectable: %s",
m->host, strerror(errno));
LOG_DBG("%s: already exists, but isn't connectable: %s", m->host, strerror(errno));
}
close(s);
@ -246,12 +255,15 @@ wait_for_socket_create(const struct module *mod)
bool ret = false;
while (!have_mpd_socket) {
struct pollfd fds[] = {
{.fd = mod->abort_fd, .events = POLLIN},
{.fd = fd, .events = POLLIN}
};
struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}, {.fd = fd, .events = POLLIN}};
poll(fds, 2, -1);
if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) < 0) {
if (errno == EINTR)
continue;
LOG_ERRNO("failed to poll");
break;
}
if (fds[0].revents & POLLIN) {
ret = true;
@ -263,7 +275,7 @@ wait_for_socket_create(const struct module *mod)
char buf[1024];
ssize_t len = read(fd, buf, sizeof(buf));
for (const char *ptr = buf; ptr < buf + len; ) {
for (const char *ptr = buf; ptr < buf + len;) {
const struct inotify_event *e = (const struct inotify_event *)ptr;
LOG_DBG("inotify: CREATED: %s/%.*s", directory, e->len, e->name);
@ -273,7 +285,7 @@ wait_for_socket_create(const struct module *mod)
break;
}
ptr += sizeof(*e) + e->len;
ptr += sizeof(*e) + e->len;
}
}
@ -296,8 +308,7 @@ connect_to_mpd(const struct module *mod)
enum mpd_error merr = mpd_connection_get_error(conn);
if (merr != MPD_ERROR_SUCCESS) {
LOG_WARN("failed to connect to MPD: %s",
mpd_connection_get_error_message(conn));
LOG_WARN("failed to connect to MPD: %s", mpd_connection_get_error_message(conn));
mpd_connection_free(conn);
return NULL;
}
@ -315,8 +326,7 @@ update_status(struct module *mod)
struct mpd_status *status = mpd_run_status(m->conn);
if (status == NULL) {
LOG_ERR("failed to get status: %s",
mpd_connection_get_error_message(m->conn));
LOG_ERR("failed to get status: %s", mpd_connection_get_error_message(m->conn));
return false;
}
@ -328,6 +338,7 @@ update_status(struct module *mod)
m->repeat = mpd_status_get_repeat(status);
m->random = mpd_status_get_random(status);
m->consume = mpd_status_get_consume(status);
m->single = mpd_status_get_single_state(status) == MPD_SINGLE_ONESHOT;
m->volume = mpd_status_get_volume(status);
m->duration = mpd_status_get_total_time(status) * 1000;
m->elapsed.value = mpd_status_get_elapsed_ms(status);
@ -338,30 +349,37 @@ update_status(struct module *mod)
struct mpd_song *song = mpd_run_current_song(m->conn);
if (song == NULL && mpd_connection_get_error(m->conn) != MPD_ERROR_SUCCESS) {
LOG_ERR("failed to get current song: %s",
mpd_connection_get_error_message(m->conn));
LOG_ERR("failed to get current song: %s", mpd_connection_get_error_message(m->conn));
return false;
}
if (song == NULL) {
mtx_lock(&mod->lock);
free(m->album); m->album = NULL;
free(m->artist); m->artist = NULL;
free(m->title); m->title = NULL;
free(m->album);
m->album = NULL;
free(m->artist);
m->artist = NULL;
free(m->title);
m->title = NULL;
free(m->file);
m->file = NULL;
mtx_unlock(&mod->lock);
} else {
const char *album = mpd_song_get_tag(song, MPD_TAG_ALBUM, 0);
const char *artist = mpd_song_get_tag(song, MPD_TAG_ARTIST, 0);
const char *title = mpd_song_get_tag(song, MPD_TAG_TITLE, 0);
const char *file = mpd_song_get_uri(song);
mtx_lock(&mod->lock);
free(m->album);
free(m->artist);
free(m->title);
free(m->file);
m->album = album != NULL ? strdup(album) : NULL;
m->artist = artist != NULL ? strdup(artist) : NULL;
m->title = title != NULL ? strdup(title) : NULL;
m->file = file != NULL ? strdup(file) : NULL;
mtx_unlock(&mod->lock);
mpd_song_free(song);
@ -377,8 +395,9 @@ run(struct module *mod)
struct private *m = mod->private;
bool aborted = false;
int ret = 0;
while (!aborted) {
while (!aborted && ret == 0) {
if (m->conn != NULL) {
mpd_connection_free(m->conn);
@ -387,16 +406,21 @@ run(struct module *mod)
/* Reset state */
mtx_lock(&mod->lock);
free(m->album); m->album = NULL;
free(m->artist); m->artist = NULL;
free(m->title); m->title = NULL;
free(m->album);
m->album = NULL;
free(m->artist);
m->artist = NULL;
free(m->title);
m->title = NULL;
free(m->file);
m->file = NULL;
m->state = MPD_STATE_UNKNOWN;
m->elapsed.value = m->duration = 0;
m->elapsed.when.tv_sec = m->elapsed.when.tv_nsec = 0;
mtx_unlock(&mod->lock);
/* Keep trying to connect, until we succeed */
while (!aborted) {
while (!aborted && ret == 0) {
if (m->port == 0) {
/* Use inotify to watch for socket creation */
aborted = wait_for_socket_create(mod);
@ -414,16 +438,33 @@ run(struct module *mod)
* host), wait for a while until we try to re-connect
* again.
*/
struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}};
int res = poll(fds, 1, 10 * 1000);
while (!aborted) {
struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}};
int res = poll(fds, sizeof(fds) / sizeof(fds[0]), 2 * 1000);
if (res < 0) {
if (errno == EINTR)
continue;
LOG_ERRNO("failed to poll");
ret = 1;
break;
}
if (res == 0) {
ret = 0;
break;
}
else if (res == 1) {
assert(fds[0].revents & POLLIN);
aborted = true;
}
if (res == 1) {
assert(fds[0].revents & POLLIN);
aborted = true;
}
}
if (aborted)
if (aborted || ret != 0)
break;
/* Initial state (after establishing a connection) */
@ -441,12 +482,18 @@ run(struct module *mod)
};
if (!mpd_send_idle(m->conn)) {
LOG_ERR("failed to send IDLE command: %s",
mpd_connection_get_error_message(m->conn));
LOG_ERR("failed to send IDLE command: %s", mpd_connection_get_error_message(m->conn));
break;
}
poll(fds, 2, -1);
if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) < 0) {
if (errno == EINTR)
continue;
LOG_ERRNO("failed to poll");
ret = 1;
break;
}
if (fds[0].revents & POLLIN) {
aborted = true;
@ -459,8 +506,7 @@ run(struct module *mod)
}
if (fds[1].revents & POLLIN) {
enum mpd_idle idle __attribute__ ((unused)) =
mpd_recv_idle(m->conn, true);
enum mpd_idle idle __attribute__((unused)) = mpd_recv_idle(m->conn, true);
LOG_DBG("IDLE mask: %d", idle);
@ -477,7 +523,7 @@ run(struct module *mod)
m->conn = NULL;
}
return 0;
return aborted ? 0 : ret;
}
struct refresh_context {
@ -532,9 +578,7 @@ refresh_in(struct module *mod, long milli_seconds)
/* Signal abort to thread */
assert(m->refresh_abort_fd != -1);
if (write(m->refresh_abort_fd, &(uint64_t){1}, sizeof(uint64_t))
!= sizeof(uint64_t))
{
if (write(m->refresh_abort_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) {
LOG_ERRNO("failed to signal abort to refresher thread");
return false;
}
@ -574,7 +618,7 @@ refresh_in(struct module *mod, long milli_seconds)
}
/* Detach - we don't want to have to thrd_join() it */
//thrd_detach(tid);
// thrd_detach(tid);
return r == 0;
}
@ -605,10 +649,8 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited)
const struct yml_node *port = yml_get_value(node, "port");
const struct yml_node *c = yml_get_value(node, "content");
return mpd_new(
yml_value_as_string(host),
port != NULL ? yml_value_as_int(port) : 0,
conf_to_particle(c, inherited));
return mpd_new(yml_value_as_string(host), port != NULL ? yml_value_as_int(port) : 0,
conf_to_particle(c, inherited));
}
static bool
@ -616,7 +658,7 @@ verify_conf(keychain_t *chain, const struct yml_node *node)
{
static const struct attr_info attrs[] = {
{"host", true, &conf_verify_string},
{"port", false, &conf_verify_int},
{"port", false, &conf_verify_unsigned},
MODULE_COMMON_ATTRS,
};

1100
modules/mpris.c Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

377
modules/niri-common.c Normal file
View file

@ -0,0 +1,377 @@
#include <errno.h>
#include <json-c/json.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/eventfd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <threads.h>
#include <unistd.h>
#include "../log.h"
#include "niri-common.h"
#define LOG_MODULE "niri:common"
#define LOG_ENABLE_DBG 0
static struct niri_socket instance = {
.fd = -1,
.abort_fd = -1,
};
static void
workspace_free(struct niri_workspace *workspace)
{
free(workspace->name);
free(workspace);
}
static void
parser(char *response)
{
enum json_tokener_error error = json_tokener_success;
struct json_object *json = json_tokener_parse_verbose(response, &error);
if (error != json_tokener_success) {
LOG_WARN("failed to parse niri socket's response");
return;
}
enum niri_event events = 0;
struct json_object_iterator it = json_object_iter_begin(json);
struct json_object_iterator end = json_object_iter_end(json);
while (!json_object_iter_equal(&it, &end)) {
char const *key = json_object_iter_peek_name(&it);
// "WorkspacesChanged": {
// "workspaces": [
// {
// "id": 3,
// "idx": 1,
// "name": null,
// "output": "DP-4",
// "is_active": true,
// "is_focused": true,
// "active_window_id": 24
// },
// ...
// ]
// }
if (strcmp(key, "WorkspacesChanged") == 0) {
mtx_lock(&instance.mtx);
tll_foreach(instance.workspaces, it) { tll_remove_and_free(instance.workspaces, it, workspace_free); }
mtx_unlock(&instance.mtx);
json_object *obj = json_object_iter_peek_value(&it);
json_object *workspaces = json_object_object_get(obj, "workspaces");
size_t length = json_object_array_length(workspaces);
for (size_t i = 0; i < length; ++i) {
json_object *ws_obj = json_object_array_get_idx(workspaces, i);
// only add workspaces on the current yambar's monitor
struct json_object *output = json_object_object_get(ws_obj, "output");
if (strcmp(instance.monitor, json_object_get_string(output)) != 0)
continue;
struct niri_workspace *ws = calloc(1, sizeof(*ws));
ws->idx = json_object_get_int(json_object_object_get(ws_obj, "idx"));
ws->id = json_object_get_int(json_object_object_get(ws_obj, "id"));
ws->active = json_object_get_boolean(json_object_object_get(ws_obj, "is_active"));
ws->focused = json_object_get_boolean(json_object_object_get(ws_obj, "is_focused"));
ws->empty = json_object_get_int(json_object_object_get(ws_obj, "active_window_id")) == 0;
char const *name = json_object_get_string(json_object_object_get(ws_obj, "name"));
if (name)
ws->name = strdup(name);
mtx_lock(&instance.mtx);
bool inserted = false;
tll_foreach(instance.workspaces, it)
{
if (it->item->idx > ws->idx) {
tll_insert_before(instance.workspaces, it, ws);
inserted = true;
break;
}
}
if (!inserted)
tll_push_back(instance.workspaces, ws);
mtx_unlock(&instance.mtx);
events |= workspaces_changed;
}
}
// "WorkspaceActivated": {
// "id": 7,
// "focused":true
// }
else if (strcmp(key, "WorkspaceActivated") == 0) {
json_object *obj = json_object_iter_peek_value(&it);
int id = json_object_get_int(json_object_object_get(obj, "id"));
mtx_lock(&instance.mtx);
tll_foreach(instance.workspaces, it)
{
bool b = it->item->id == id;
it->item->focused = b;
it->item->active = b;
}
mtx_unlock(&instance.mtx);
events |= workspace_activated;
}
// "WorkspaceActiveWindowChanged": {
// "workspace_id": 3,
// "active_window_id": 8
// }
else if (strcmp(key, "WorkspaceActiveWindowChanged") == 0) {
json_object *obj = json_object_iter_peek_value(&it);
int id = json_object_get_int(json_object_object_get(obj, "id"));
bool empty = json_object_get_int(json_object_object_get(obj, "active_window_id")) == 0;
mtx_lock(&instance.mtx);
tll_foreach(instance.workspaces, it)
{
if (it->item->id == id) {
it->item->empty = empty;
break;
}
}
mtx_unlock(&instance.mtx);
events |= workspace_active_window_changed;
}
//
// "KeyboardLayoutsChanged": {
// "keyboard_layouts": {
// "names": [
// "English (US)",
// "Russian"
// ],
// "current_idx": 0
// }
// }
else if (strcmp(key, "KeyboardLayoutsChanged") == 0) {
tll_foreach(instance.keyboard_layouts, it) { tll_remove_and_free(instance.keyboard_layouts, it, free); }
json_object *obj = json_object_iter_peek_value(&it);
json_object *kb_layouts = json_object_object_get(obj, "keyboard_layouts");
instance.keyboard_layout_index = json_object_get_int(json_object_object_get(kb_layouts, "current_idx"));
json_object *names = json_object_object_get(kb_layouts, "names");
size_t names_length = json_object_array_length(names);
for (size_t i = 0; i < names_length; ++i) {
char const *name = json_object_get_string(json_object_array_get_idx(names, i));
tll_push_back(instance.keyboard_layouts, strdup(name));
}
events |= keyboard_layouts_changed;
}
// "KeyboardLayoutSwitched": {
// "idx": 1
// }
else if (strcmp(key, "KeyboardLayoutSwitched") == 0) {
json_object *obj = json_object_iter_peek_value(&it);
instance.keyboard_layout_index = json_object_get_int(json_object_object_get(obj, "idx"));
events |= keyboard_layouts_switched;
}
json_object_iter_next(&it);
}
json_object_put(json);
mtx_lock(&instance.mtx);
tll_foreach(instance.subscribers, it)
{
if (it->item->events & events)
if (write(it->item->fd, &(uint64_t){1}, sizeof(uint64_t)) == -1)
LOG_ERRNO("failed to write");
}
mtx_unlock(&instance.mtx);
}
static int
run(void *userdata)
{
static char msg[] = "\"EventStream\"\n";
static char expected[] = "{\"Ok\":\"Handled\"}";
if (write(instance.fd, msg, sizeof(msg) / sizeof(msg[0])) == -1) {
LOG_ERRNO("failed to sent message to niri socket");
return thrd_error;
}
static char buffer[8192];
if (read(instance.fd, buffer, sizeof(buffer) / sizeof(buffer[0]) - 1) == -1) {
LOG_ERRNO("failed to read response of niri socket");
return thrd_error;
}
char *saveptr;
char *response = strtok_r(buffer, "\n", &saveptr);
if (response == NULL || strcmp(expected, response) != 0) {
// unexpected first response, something went wrong
LOG_ERR("unexpected response of niri socket");
return thrd_error;
}
while ((response = strtok_r(NULL, "\n", &saveptr)) != NULL)
parser(response);
while (true) {
struct pollfd fds[] = {
(struct pollfd){.fd = instance.abort_fd, .events = POLLIN},
(struct pollfd){.fd = instance.fd, .events = POLLIN},
};
if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) == -1) {
if (errno == EINTR)
continue;
LOG_ERRNO("failed to poll");
break;
}
if (fds[0].revents & POLLIN)
break;
static char buffer[8192];
ssize_t length = read(fds[1].fd, buffer, sizeof(buffer) / sizeof(buffer[0]));
if (length == 0)
break;
if (length == -1) {
if (errno == EAGAIN || errno == EINTR)
continue;
LOG_ERRNO("unable to read niri socket");
break;
}
buffer[length] = '\0';
saveptr = NULL;
response = strtok_r(buffer, "\n", &saveptr);
do {
parser(response);
} while ((response = strtok_r(NULL, "\n", &saveptr)) != NULL);
}
return thrd_success;
}
struct niri_socket *
niri_socket_open(char const *monitor)
{
if (instance.fd >= 0)
return &instance;
char const *path = getenv("NIRI_SOCKET");
if (path == NULL) {
LOG_ERR("NIRI_SOCKET is empty. Is niri running?");
return NULL;
}
if ((instance.fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)) == -1) {
LOG_ERRNO("failed to create socket");
goto error;
}
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
if (connect(instance.fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
LOG_ERRNO("failed to connect to niri socket");
goto error;
}
if ((instance.abort_fd = eventfd(0, EFD_CLOEXEC)) == -1) {
LOG_ERRNO("failed to create abort_fd");
goto error;
}
if (mtx_init(&instance.mtx, mtx_plain) != thrd_success) {
LOG_ERR("failed to initialize mutex");
goto error;
}
if (thrd_create(&instance.thrd, run, NULL) != thrd_success) {
LOG_ERR("failed to create thread");
mtx_destroy(&instance.mtx);
goto error;
}
instance.monitor = monitor;
return &instance;
error:
if (instance.fd >= 0)
close(instance.fd);
if (instance.abort_fd >= 0)
close(instance.abort_fd);
instance.fd = -1;
instance.abort_fd = -1;
instance.monitor = NULL;
return NULL;
}
static void
socket_close(void)
{
if (write(instance.abort_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t))
LOG_ERRNO("failed to write to abort_fd");
thrd_join(instance.thrd, NULL);
close(instance.abort_fd);
close(instance.fd);
instance.abort_fd = -1;
instance.fd = -1;
mtx_destroy(&instance.mtx);
tll_free_and_free(instance.subscribers, free);
tll_free_and_free(instance.workspaces, workspace_free);
tll_free_and_free(instance.keyboard_layouts, free);
}
void
niri_socket_close(void)
{
static once_flag flag = ONCE_FLAG_INIT;
call_once(&flag, socket_close);
}
int
niri_socket_subscribe(enum niri_event events)
{
int fd = eventfd(0, EFD_CLOEXEC);
if (fd == -1) {
LOG_ERRNO("failed to create eventfd");
return -1;
}
struct niri_subscriber *subscriber = calloc(1, sizeof(*subscriber));
subscriber->events = events;
subscriber->fd = fd;
mtx_lock(&instance.mtx);
tll_push_back(instance.subscribers, subscriber);
mtx_unlock(&instance.mtx);
return subscriber->fd;
}

45
modules/niri-common.h Normal file
View file

@ -0,0 +1,45 @@
#pragma once
#include <stdbool.h>
#include <threads.h>
#include <tllist.h>
enum niri_event {
workspaces_changed = (1 << 0),
workspace_activated = (1 << 1),
workspace_active_window_changed = (1 << 2),
keyboard_layouts_changed = (1 << 3),
keyboard_layouts_switched = (1 << 4),
};
struct niri_subscriber {
int events;
int fd;
};
struct niri_workspace {
int id;
int idx;
char *name;
bool active;
bool focused;
bool empty;
};
struct niri_socket {
char const *monitor;
int abort_fd;
int fd;
tll(struct niri_subscriber *) subscribers;
tll(struct niri_workspace *) workspaces;
tll(char *) keyboard_layouts;
size_t keyboard_layout_index;
thrd_t thrd;
mtx_t mtx;
};
struct niri_socket *niri_socket_open(char const *monitor);
void niri_socket_close(void);
int niri_socket_subscribe(enum niri_event events);

160
modules/niri-language.c Normal file
View file

@ -0,0 +1,160 @@
#include <errno.h>
#include <json-c/json.h>
#include <poll.h>
#include <string.h>
#include <unistd.h>
#define LOG_MODULE "niri-language"
#define LOG_ENABLE_DBG 0
#include "niri-common.h"
#include "../log.h"
#include "../particles/dynlist.h"
#include "../plugin.h"
struct private
{
struct particle *label;
struct niri_socket *niri;
};
static void
destroy(struct module *module)
{
struct private *private = module->private;
private->label->destroy(private->label);
free(private);
module_default_destroy(module);
}
static const char *
description(const struct module *module)
{
return "niri-lang";
}
static struct exposable *
content(struct module *module)
{
const struct private *private = module->private;
if (private->niri == NULL)
return dynlist_exposable_new(&((struct exposable *){0}), 0, 0, 0);
mtx_lock(&module->lock);
mtx_lock(&private->niri->mtx);
char *name = "???";
size_t i = 0;
tll_foreach(private->niri->keyboard_layouts, it)
{
if (i++ == private->niri->keyboard_layout_index)
name = it->item;
}
struct tag_set tags = {
.tags = (struct tag *[]){tag_new_string(module, "language", name)},
.count = 1,
};
struct exposable *exposable = private->label->instantiate(private->label, &tags);
tag_set_destroy(&tags);
mtx_unlock(&private->niri->mtx);
mtx_unlock(&module->lock);
return exposable;
}
static int
run(struct module *module)
{
struct private *private = module->private;
/* Ugly, but I didn't find better way for waiting
* the monitor's name to be set */
char const *monitor;
do {
monitor = module->bar->output_name(module->bar);
usleep(50);
} while (monitor == NULL);
private->niri = niri_socket_open(monitor);
if (private->niri == NULL)
return 1;
int fd = niri_socket_subscribe(keyboard_layouts_changed | keyboard_layouts_switched);
if (fd == -1) {
niri_socket_close();
return 1;
}
module->bar->refresh(module->bar);
while (true) {
struct pollfd fds[] = {
(struct pollfd){.fd = module->abort_fd, .events = POLLIN},
(struct pollfd){.fd = fd, .events = POLLIN},
};
if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) == -1) {
if (errno == EINTR)
continue;
LOG_ERRNO("failed to poll");
break;
}
if (fds[0].revents & POLLIN)
break;
if (read(fds[1].fd, &(uint64_t){0}, sizeof(uint64_t)) == -1)
LOG_ERRNO("failed to read from eventfd");
module->bar->refresh(module->bar);
}
niri_socket_close();
return 0;
}
static struct module *
niri_language_new(struct particle *label)
{
struct private *private = calloc(1, sizeof(struct private));
private->label = label;
struct module *module = module_common_new();
module->private = private;
module->run = &run;
module->destroy = &destroy;
module->content = &content;
module->description = &description;
return module;
}
static struct module *
from_conf(struct yml_node const *node, struct conf_inherit inherited)
{
struct yml_node const *content = yml_get_value(node, "content");
return niri_language_new(conf_to_particle(content, inherited));
}
static bool
verify_conf(keychain_t *chain, const struct yml_node *node)
{
static struct attr_info const attrs[] = {
MODULE_COMMON_ATTRS,
};
return conf_verify_dict(chain, node, attrs);
}
const struct module_iface module_niri_language_iface = {
.verify_conf = &verify_conf,
.from_conf = &from_conf,
};
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
extern const struct module_iface iface __attribute__((weak, alias("module_niri_language_iface")));
#endif

163
modules/niri-workspaces.c Normal file
View file

@ -0,0 +1,163 @@
#include <errno.h>
#include <poll.h>
#include <string.h>
#include <unistd.h>
#define LOG_MODULE "niri-workspaces"
#define LOG_ENABLE_DBG 0
#include "niri-common.h"
#include "../log.h"
#include "../particles/dynlist.h"
#include "../plugin.h"
struct private
{
struct particle *label;
struct niri_socket *niri;
};
static void
destroy(struct module *module)
{
struct private *private = module->private;
private->label->destroy(private->label);
free(private);
module_default_destroy(module);
}
static const char *
description(const struct module *module)
{
return "niri-ws";
}
static struct exposable *
content(struct module *module)
{
struct private const *private = module->private;
if (private->niri == NULL)
return dynlist_exposable_new(&((struct exposable *){0}), 0, 0, 0);
mtx_lock(&module->lock);
mtx_lock(&private->niri->mtx);
size_t i = 0;
struct exposable *exposable[tll_length(private->niri->workspaces)];
tll_foreach(private->niri->workspaces, it)
{
struct tag_set tags = {
.tags = (struct tag*[]){
tag_new_int(module, "id", it->item->idx),
tag_new_string(module, "name", it->item->name),
tag_new_bool(module, "active", it->item->active),
tag_new_bool(module, "focused", it->item->focused),
tag_new_bool(module, "empty", it->item->empty),
},
.count = 5,
};
exposable[i++] = private->label->instantiate(private->label, &tags);
tag_set_destroy(&tags);
}
mtx_unlock(&private->niri->mtx);
mtx_unlock(&module->lock);
return dynlist_exposable_new(exposable, i, 0, 0);
}
static int
run(struct module *module)
{
struct private *private = module->private;
/* Ugly, but I didn't find better way for waiting
* the monitor's name to be set */
char const *monitor;
do {
monitor = module->bar->output_name(module->bar);
usleep(50);
} while (monitor == NULL);
private->niri = niri_socket_open(monitor);
if (private->niri == NULL)
return 1;
int fd = niri_socket_subscribe(workspaces_changed | workspace_activated | workspace_active_window_changed);
if (fd == -1) {
niri_socket_close();
return 1;
}
module->bar->refresh(module->bar);
while (true) {
struct pollfd fds[] = {
(struct pollfd){.fd = module->abort_fd, .events = POLLIN},
(struct pollfd){.fd = fd, .events = POLLIN},
};
if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) == -1) {
if (errno == EINTR)
continue;
LOG_ERRNO("failed to poll");
break;
}
if (fds[0].revents & POLLIN)
break;
if (read(fds[1].fd, &(uint64_t){0}, sizeof(uint64_t)) == -1)
LOG_ERRNO("failed to read from eventfd");
module->bar->refresh(module->bar);
}
niri_socket_close();
return 0;
}
static struct module *
niri_workspaces_new(struct particle *label)
{
struct private *private = calloc(1, sizeof(struct private));
private->label = label;
struct module *module = module_common_new();
module->private = private;
module->run = &run;
module->destroy = &destroy;
module->content = &content;
module->description = &description;
return module;
}
static struct module *
from_conf(struct yml_node const *node, struct conf_inherit inherited)
{
struct yml_node const *content = yml_get_value(node, "content");
return niri_workspaces_new(conf_to_particle(content, inherited));
}
static bool
verify_conf(keychain_t *chain, const struct yml_node *node)
{
static struct attr_info const attrs[] = {
MODULE_COMMON_ATTRS,
};
return conf_verify_dict(chain, node, attrs);
}
const struct module_iface module_niri_workspaces_iface = {
.verify_conf = &verify_conf,
.from_conf = &from_conf,
};
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
extern const struct module_iface iface __attribute__((weak, alias("module_niri_workspaces_iface")));
#endif

1025
modules/pipewire.c Normal file

File diff suppressed because it is too large Load diff

523
modules/pulse.c Normal file
View file

@ -0,0 +1,523 @@
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/timerfd.h>
#include <unistd.h>
#include <pulse/pulseaudio.h>
#define LOG_MODULE "pulse"
#define LOG_ENABLE_DBG 0
#include "../bar/bar.h"
#include "../config-verify.h"
#include "../config.h"
#include "../log.h"
#include "../plugin.h"
struct private
{
char *sink_name;
char *source_name;
struct particle *label;
bool online;
bool sink_online;
pa_cvolume sink_volume;
bool sink_muted;
char *sink_port;
uint32_t sink_index;
bool source_online;
pa_cvolume source_volume;
bool source_muted;
char *source_port;
uint32_t source_index;
int refresh_timer_fd;
bool refresh_scheduled;
pa_mainloop *mainloop;
pa_context *context;
};
static void
destroy(struct module *mod)
{
struct private *priv = mod->private;
priv->label->destroy(priv->label);
free(priv->sink_name);
free(priv->source_name);
free(priv->sink_port);
free(priv->source_port);
free(priv);
module_default_destroy(mod);
}
static const char *
description(const struct module *mod)
{
return "pulse";
}
static struct exposable *
content(struct module *mod)
{
struct private *priv = mod->private;
mtx_lock(&mod->lock);
pa_volume_t sink_volume_max = pa_cvolume_max(&priv->sink_volume);
pa_volume_t source_volume_max = pa_cvolume_max(&priv->source_volume);
int sink_percent = round(100.0 * sink_volume_max / PA_VOLUME_NORM);
int source_percent = round(100.0 * source_volume_max / PA_VOLUME_NORM);
struct tag_set tags = {
.tags = (struct tag *[]){
tag_new_bool(mod, "online", priv->online),
tag_new_bool(mod, "sink_online", priv->sink_online),
tag_new_int_range(mod, "sink_percent", sink_percent, 0, 100),
tag_new_bool(mod, "sink_muted", priv->sink_muted),
tag_new_string(mod, "sink_port", priv->sink_port),
tag_new_bool(mod, "source_online", priv->source_online),
tag_new_int_range(mod, "source_percent", source_percent, 0, 100),
tag_new_bool(mod, "source_muted", priv->source_muted),
tag_new_string(mod, "source_port", priv->source_port),
},
.count = 9,
};
mtx_unlock(&mod->lock);
struct exposable *exposable = priv->label->instantiate(priv->label, &tags);
tag_set_destroy(&tags);
return exposable;
}
static const char *
context_error(pa_context *c)
{
return pa_strerror(pa_context_errno(c));
}
static void
abort_event_cb(pa_mainloop_api *api, pa_io_event *event, int fd, pa_io_event_flags_t flags, void *userdata)
{
struct module *mod = userdata;
struct private *priv = mod->private;
pa_context_disconnect(priv->context);
}
static void
refresh_timer_cb(pa_mainloop_api *api, pa_io_event *event, int fd, pa_io_event_flags_t flags, void *userdata)
{
struct module *mod = userdata;
struct private *priv = mod->private;
// Drain the refresh timer.
uint64_t n;
if (read(priv->refresh_timer_fd, &n, sizeof n) < 0)
LOG_ERRNO("failed to read from timerfd");
// Clear the refresh flag.
priv->refresh_scheduled = false;
// Refresh the bar.
mod->bar->refresh(mod->bar);
}
// Refresh the bar after a small delay. Without the delay, the bar
// would be refreshed multiple times per event (e.g., a volume change),
// and sometimes the active port would be reported incorrectly for a
// brief moment. (This behavior was observed with PipeWire 0.3.61.)
static void
schedule_refresh(struct module *mod)
{
struct private *priv = mod->private;
// Do nothing if a refresh has already been scheduled.
if (priv->refresh_scheduled)
return;
// Start the refresh timer.
struct itimerspec t = {
.it_interval = {.tv_sec = 0, .tv_nsec = 0},
.it_value = {.tv_sec = 0, .tv_nsec = 50000000},
};
timerfd_settime(priv->refresh_timer_fd, 0, &t, NULL);
// Set the refresh flag.
priv->refresh_scheduled = true;
}
static void
set_server_online(struct module *mod)
{
struct private *priv = mod->private;
mtx_lock(&mod->lock);
priv->online = true;
mtx_unlock(&mod->lock);
schedule_refresh(mod);
}
static void
set_server_offline(struct module *mod)
{
struct private *priv = mod->private;
mtx_lock(&mod->lock);
priv->online = false;
priv->sink_online = false;
priv->source_online = false;
mtx_unlock(&mod->lock);
schedule_refresh(mod);
}
static void
set_sink_info(struct module *mod, const pa_sink_info *sink_info)
{
struct private *priv = mod->private;
mtx_lock(&mod->lock);
free(priv->sink_port);
priv->sink_online = true;
priv->sink_index = sink_info->index;
priv->sink_volume = sink_info->volume;
priv->sink_muted = sink_info->mute;
priv->sink_port = sink_info->active_port != NULL ? strdup(sink_info->active_port->description) : NULL;
mtx_unlock(&mod->lock);
schedule_refresh(mod);
}
static void
set_sink_offline(struct module *mod)
{
struct private *priv = mod->private;
mtx_lock(&mod->lock);
priv->sink_online = false;
mtx_unlock(&mod->lock);
schedule_refresh(mod);
}
static void
set_source_info(struct module *mod, const pa_source_info *source_info)
{
struct private *priv = mod->private;
mtx_lock(&mod->lock);
free(priv->source_port);
priv->source_online = true;
priv->source_index = source_info->index;
priv->source_volume = source_info->volume;
priv->source_muted = source_info->mute;
priv->source_port = source_info->active_port != NULL ? strdup(source_info->active_port->description) : NULL;
mtx_unlock(&mod->lock);
schedule_refresh(mod);
}
static void
set_source_offline(struct module *mod)
{
struct private *priv = mod->private;
mtx_lock(&mod->lock);
priv->source_online = false;
mtx_unlock(&mod->lock);
schedule_refresh(mod);
}
static void
sink_info_cb(pa_context *c, const pa_sink_info *i, int eol, void *userdata)
{
struct module *mod = userdata;
if (eol < 0) {
LOG_ERR("failed to get sink info: %s", context_error(c));
set_sink_offline(mod);
} else if (eol == 0) {
set_sink_info(mod, i);
}
}
static void
source_info_cb(pa_context *c, const pa_source_info *i, int eol, void *userdata)
{
struct module *mod = userdata;
if (eol < 0) {
LOG_ERR("failed to get source info: %s", context_error(c));
set_source_offline(mod);
} else if (eol == 0) {
set_source_info(mod, i);
}
}
static void
server_info_cb(pa_context *c, const pa_server_info *i, void *userdata)
{
LOG_INFO("%s, version %s", i->server_name, i->server_version);
}
static void
get_sink_info_by_name(pa_context *c, const char *name, void *userdata)
{
pa_operation *o = pa_context_get_sink_info_by_name(c, name, sink_info_cb, userdata);
pa_operation_unref(o);
}
static void
get_source_info_by_name(pa_context *c, const char *name, void *userdata)
{
pa_operation *o = pa_context_get_source_info_by_name(c, name, source_info_cb, userdata);
pa_operation_unref(o);
}
static void
get_sink_info_by_index(pa_context *c, uint32_t index, void *userdata)
{
pa_operation *o = pa_context_get_sink_info_by_index(c, index, sink_info_cb, userdata);
pa_operation_unref(o);
}
static void
get_source_info_by_index(pa_context *c, uint32_t index, void *userdata)
{
pa_operation *o = pa_context_get_source_info_by_index(c, index, source_info_cb, userdata);
pa_operation_unref(o);
}
static void
get_server_info(pa_context *c, void *userdata)
{
pa_operation *o = pa_context_get_server_info(c, server_info_cb, userdata);
pa_operation_unref(o);
}
static void
subscribe(pa_context *c, void *userdata)
{
pa_subscription_mask_t mask = PA_SUBSCRIPTION_MASK_SERVER | PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE;
pa_operation *o = pa_context_subscribe(c, mask, NULL, userdata);
pa_operation_unref(o);
}
static pa_context *connect_to_server(struct module *mod);
static void
context_state_change_cb(pa_context *c, void *userdata)
{
struct module *mod = userdata;
struct private *priv = mod->private;
pa_context_state_t state = pa_context_get_state(c);
switch (state) {
case PA_CONTEXT_UNCONNECTED:
case PA_CONTEXT_CONNECTING:
case PA_CONTEXT_AUTHORIZING:
case PA_CONTEXT_SETTING_NAME:
break;
case PA_CONTEXT_READY:
set_server_online(mod);
subscribe(c, mod);
get_server_info(c, mod);
get_sink_info_by_name(c, priv->sink_name, mod);
get_source_info_by_name(c, priv->source_name, mod);
break;
case PA_CONTEXT_FAILED:
LOG_WARN("connection lost");
set_server_offline(mod);
pa_context_unref(priv->context);
priv->context = connect_to_server(mod);
break;
case PA_CONTEXT_TERMINATED:
LOG_DBG("connection terminated");
set_server_offline(mod);
pa_mainloop_quit(priv->mainloop, 0);
break;
}
}
static void
subscription_event_cb(pa_context *c, pa_subscription_event_type_t event_type, uint32_t index, void *userdata)
{
struct module *mod = userdata;
struct private *priv = mod->private;
int facility = event_type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
int type = event_type & PA_SUBSCRIPTION_EVENT_TYPE_MASK;
switch (facility) {
case PA_SUBSCRIPTION_EVENT_SERVER:
get_sink_info_by_name(c, priv->sink_name, mod);
get_source_info_by_name(c, priv->source_name, mod);
break;
case PA_SUBSCRIPTION_EVENT_SINK:
if (index == priv->sink_index) {
if (type == PA_SUBSCRIPTION_EVENT_CHANGE)
get_sink_info_by_index(c, index, mod);
else if (type == PA_SUBSCRIPTION_EVENT_REMOVE)
set_sink_offline(mod);
}
break;
case PA_SUBSCRIPTION_EVENT_SOURCE:
if (index == priv->source_index) {
if (type == PA_SUBSCRIPTION_EVENT_CHANGE)
get_source_info_by_index(c, index, mod);
else if (type == PA_SUBSCRIPTION_EVENT_REMOVE)
set_source_offline(mod);
}
break;
}
}
static pa_context *
connect_to_server(struct module *mod)
{
struct private *priv = mod->private;
// Create connection context.
pa_mainloop_api *api = pa_mainloop_get_api(priv->mainloop);
pa_context *c = pa_context_new(api, "yambar");
if (c == NULL) {
LOG_ERR("failed to create PulseAudio connection context");
return NULL;
}
// Register callback functions.
pa_context_set_state_callback(c, context_state_change_cb, mod);
pa_context_set_subscribe_callback(c, subscription_event_cb, mod);
// Connect to server.
pa_context_flags_t flags = PA_CONTEXT_NOFAIL | PA_CONTEXT_NOAUTOSPAWN;
if (pa_context_connect(c, NULL, flags, NULL) < 0) {
LOG_ERR("failed to connect to PulseAudio server: %s", context_error(c));
pa_context_unref(c);
return NULL;
}
return c;
}
static int
run(struct module *mod)
{
struct private *priv = mod->private;
int ret = -1;
// Create main loop.
priv->mainloop = pa_mainloop_new();
if (priv->mainloop == NULL) {
LOG_ERR("failed to create PulseAudio main loop");
return -1;
}
// Create refresh timer.
priv->refresh_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
if (priv->refresh_timer_fd < 0) {
LOG_ERRNO("failed to create timerfd");
pa_mainloop_free(priv->mainloop);
return -1;
}
// Connect to server.
priv->context = connect_to_server(mod);
if (priv->context == NULL) {
pa_mainloop_free(priv->mainloop);
close(priv->refresh_timer_fd);
return -1;
}
// Poll refresh timer and abort event.
pa_mainloop_api *api = pa_mainloop_get_api(priv->mainloop);
api->io_new(api, priv->refresh_timer_fd, PA_IO_EVENT_INPUT, refresh_timer_cb, mod);
api->io_new(api, mod->abort_fd, PA_IO_EVENT_INPUT | PA_IO_EVENT_HANGUP, abort_event_cb, mod);
// Run main loop.
if (pa_mainloop_run(priv->mainloop, &ret) < 0) {
LOG_ERR("PulseAudio main loop error");
ret = -1;
}
// Clean up.
pa_context_unref(priv->context);
pa_mainloop_free(priv->mainloop);
close(priv->refresh_timer_fd);
return ret;
}
static struct module *
pulse_new(const char *sink_name, const char *source_name, struct particle *label)
{
struct private *priv = calloc(1, sizeof *priv);
priv->label = label;
priv->sink_name = strdup(sink_name);
priv->source_name = strdup(source_name);
struct module *mod = module_common_new();
mod->private = priv;
mod->run = &run;
mod->destroy = &destroy;
mod->content = &content;
mod->description = &description;
return mod;
}
static struct module *
from_conf(const struct yml_node *node, struct conf_inherit inherited)
{
const struct yml_node *sink = yml_get_value(node, "sink");
const struct yml_node *source = yml_get_value(node, "source");
const struct yml_node *content = yml_get_value(node, "content");
return pulse_new(sink != NULL ? yml_value_as_string(sink) : "@DEFAULT_SINK@",
source != NULL ? yml_value_as_string(source) : "@DEFAULT_SOURCE@",
conf_to_particle(content, inherited));
}
static bool
verify_conf(keychain_t *chain, const struct yml_node *node)
{
static const struct attr_info attrs[] = {
{"sink", false, &conf_verify_string},
{"source", false, &conf_verify_string},
MODULE_COMMON_ATTRS,
};
return conf_verify_dict(chain, node, attrs);
}
const struct module_iface module_pulse_iface = {
.verify_conf = &verify_conf,
.from_conf = &from_conf,
};
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
extern const struct module_iface iface __attribute__((weak, alias("module_pulse_iface")));
#endif

Some files were not shown because too many files have changed in this diff Show more