├── .github
├── FUNDING.yml
└── workflows
│ └── rust-format.yml
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── build-scripts
└── swayosd-git.rpkg.spec
├── build.rs
├── data
├── config
│ ├── backend.toml
│ └── config.toml
├── dbus
│ └── org.erikreider.swayosd.conf
├── icons
│ └── scalable
│ │ └── status
│ │ ├── caps-lock-symbolic.svg
│ │ ├── display-brightness-symbolic.svg
│ │ ├── missing-symbolic.svg
│ │ ├── num-lock-symbolic.svg
│ │ ├── pause-large-symbolic.svg
│ │ ├── play-large-symbolic.svg
│ │ ├── playlist-consecutive-symbolic.svg
│ │ ├── playlist-shuffle-symbolic.svg
│ │ ├── scroll-lock-symbolic.svg
│ │ ├── seek-backward-large-symbolic.svg
│ │ ├── seek-forward-large-symbolic.svg
│ │ ├── sink-volume-high-symbolic.svg
│ │ ├── sink-volume-low-symbolic.svg
│ │ ├── sink-volume-medium-symbolic.svg
│ │ ├── sink-volume-muted-symbolic.svg
│ │ ├── sink-volume-overamplified-symbolic.svg
│ │ ├── source-volume-high-symbolic.svg
│ │ ├── source-volume-low-symbolic.svg
│ │ ├── source-volume-medium-symbolic.svg
│ │ ├── source-volume-muted-symbolic.svg
│ │ └── stop-large-symbolic.svg
├── meson.build
├── polkit
│ ├── actions
│ │ └── org.erikreider.swayosd.policy.in
│ └── rules
│ │ └── org.erikreider.swayosd.rules
├── services
│ ├── dbus
│ │ └── org.erikreider.swayosd.service.in
│ └── systemd
│ │ └── swayosd-libinput-backend.service.in
├── style
│ └── style.scss
├── swayosd.gresource.xml
└── udev
│ └── 99-swayosd.rules
├── meson.build
├── rust-toolchain
├── rustfmt.toml
└── src
├── argtypes.rs
├── brightness_backend
├── blight.rs
├── brightnessctl.rs
└── mod.rs
├── client
└── main.rs
├── config.rs
├── config
├── backend.rs
└── user.rs
├── global_utils.rs
├── input-backend
├── dbus_server.rs
└── main.rs
├── meson.build
├── mpris-backend
└── mod.rs
└── server
├── application.rs
├── main.rs
├── osd_window.rs
└── utils.rs
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [erikreider] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/.github/workflows/rust-format.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: Rust Format
4 |
5 | # Controls when the workflow will run
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the main branch
8 | push:
9 | branches: [ main ]
10 | pull_request:
11 | branches: [ main ]
12 |
13 | # Allows you to run this workflow manually from the Actions tab
14 | workflow_dispatch:
15 |
16 | jobs:
17 | formatting:
18 | name: cargo fmt
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: actions/checkout@v4
22 | # Ensure rustfmt is installed and setup problem matcher
23 | - uses: actions-rust-lang/setup-rust-toolchain@v1
24 | with:
25 | components: rustfmt
26 | - name: Rustfmt Check
27 | uses: actions-rust-lang/rustfmt@v1
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries
2 | /target
3 | /build
4 |
5 | *.gresource
6 |
7 | # These are backup files generated by rustfmt
8 | **/*.rs.bk
9 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "swayosd"
3 | version = "0.2.0"
4 | edition = "2021"
5 |
6 | [[bin]]
7 | name = "swayosd-server"
8 | path = "src/server/main.rs"
9 |
10 | [[bin]]
11 | name = "swayosd-client"
12 | path = "src/client/main.rs"
13 |
14 | [[bin]]
15 | name = "swayosd-libinput-backend"
16 | path = "src/input-backend/main.rs"
17 |
18 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
19 |
20 | [dependencies]
21 | # Config dependencies
22 | toml = "0.8"
23 | serde = "1"
24 | serde_derive = "1"
25 | # GUI Dependencies
26 | gtk = { package = "gtk4", version = "0.9.1" }
27 | gtk-layer-shell = { package = "gtk4-layer-shell", version = "0.4.0" }
28 | shrinkwraprs = "0.3.0"
29 | cascade = "1.0.1"
30 | pulse = { version = "2.26.0", package = "libpulse-binding" }
31 | pulsectl-rs = "0.3.2"
32 | substring = "1.4.5"
33 | lazy_static = "1.4.0"
34 | zbus = "5"
35 | # Backend Dependencies
36 | input = "0.8"
37 | libc = "0.2.147"
38 | evdev-rs = "0.6.1"
39 | async-std = "1.12.0"
40 | nix = { version = "0.29", features = ["poll"] }
41 | blight = "0.7.0"
42 | anyhow = "1.0.75"
43 | thiserror = "1.0.49"
44 | async-channel = "2.3.1"
45 | mpris = "2.0.1"
46 | runtime-format = "0.1.3"
47 | strfmt = "0.2.4"
48 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwayOSD
2 |
3 | A OSD window for common actions like volume and capslock.
4 |
5 | This is my first time coding in Rust so fixes and improvements are appreciated :)
6 |
7 | ## Features:
8 |
9 | - LibInput listener Backend for these keys:
10 | - Caps Lock
11 | - Num Lock
12 | - Scroll Lock
13 | - Input and output volume change indicator
14 | - Input and output mute change indicator
15 | - Customizable maximum Volume
16 | - Capslock change (Note: doesn't change the caps lock state)
17 | - Brightness change indicator
18 |
19 | ## Images
20 |
21 | 
22 |
23 | 
24 |
25 | ## Install:
26 |
27 | There's a new LibInput watcher binary shipped with SwayOSD (`swayosd-libinput-backend`)
28 | which can automatically detect key presses, so no need for binding key combos.
29 | The supported keys are listed above in [Features](#features)
30 |
31 | ### Through Meson
32 |
33 | ```zsh
34 | # Please note that the command below might require `--prefix /usr` on some systems
35 | meson setup build
36 | ninja -C build
37 | meson install -C build
38 | ```
39 |
40 | ### AUR
41 |
42 | Available on the AUR thanks to @jgmdev! (Don't open a issue here about AUR package)
43 |
44 | - [swayosd-git](https://aur.archlinux.org/packages/swayosd-git)
45 |
46 | ### Debian / Ubuntu
47 |
48 | Starting with Debian trixie and Ubuntu Plucky swayosd is available via apt.
49 |
50 | - [swayosd](https://tracker.debian.org/swayosd)
51 |
52 | ## Usage:
53 |
54 | ### SwayOSD LibInput Backend
55 |
56 | Using Systemd: `sudo systemctl enable --now swayosd-libinput-backend.service`
57 |
58 | Other users can run: `pkexec swayosd-libinput-backend`
59 |
60 | ### SwayOSD Frontend
61 |
62 | #### Sway examples
63 |
64 | ##### Start Server
65 |
66 | ```zsh
67 | # OSD server
68 | exec swayosd-server
69 | ```
70 |
71 | ##### Add Client bindings
72 |
73 | ```zsh
74 | # Sink volume raise optionally with --device
75 | bindsym XF86AudioRaiseVolume exec swayosd-client --output-volume raise
76 | # Sink volume lower optionally with --device
77 | bindsym XF86AudioLowerVolume exec swayosd-client --output-volume lower --device alsa_output.pci-0000_11_00.4.analog-stereo.monitor
78 | # Sink volume toggle mute
79 | bindsym XF86AudioMute exec swayosd-client --output-volume mute-toggle
80 | # Source volume toggle mute
81 | bindsym XF86AudioMicMute exec swayosd-client --input-volume mute-toggle
82 |
83 | # Volume raise with custom value
84 | bindsym XF86AudioRaiseVolume exec swayosd-client --output-volume 15
85 | # Volume lower with custom value
86 | bindsym XF86AudioLowerVolume exec swayosd-client --output-volume -15
87 |
88 | # Volume raise with max value
89 | bindsym XF86AudioRaiseVolume exec swayosd-client --output-volume raise --max-volume 120
90 | # Volume lower with max value
91 | bindsym XF86AudioLowerVolume exec swayosd-client --output-volume lower --max-volume 120
92 |
93 | # Sink volume raise with custom value optionally with --device
94 | bindsym XF86AudioRaiseVolume exec swayosd-client --output-volume +10 --device alsa_output.pci-0000_11_00.4.analog-stereo.monitor
95 | # Sink volume lower with custom value optionally with --device
96 | bindsym XF86AudioLowerVolume exec swayosd-client --output-volume -10 --device alsa_output.pci-0000_11_00.4.analog-stereo.monitor
97 |
98 | # Capslock (If you don't want to use the backend)
99 | bindsym --release Caps_Lock exec swayosd-client --caps-lock
100 | # Capslock but specific LED name (/sys/class/leds/)
101 | bindsym --release Caps_Lock exec swayosd-client --caps-lock-led input19::capslock
102 |
103 | # Brightness raise
104 | bindsym XF86MonBrightnessUp exec swayosd-client --brightness raise
105 | # Brightness lower
106 | bindsym XF86MonBrightnessDown exec swayosd-client --brightness lower
107 |
108 | # Brightness raise with custom value('+' sign needed)
109 | bindsym XF86MonBrightnessUp exec swayosd-client --brightness +10
110 | # Brightness lower with custom value('-' sign needed)
111 | bindsym XF86MonBrightnessDown exec swayosd-client --brightness -10
112 | ```
113 |
114 | ### Notes on using `--device`:
115 |
116 | - It is for audio devices only.
117 | - If it is omitted the default audio device is used.
118 | - It only changes the target device for the current action that changes the volume.
119 | - You can list your input audio devices using `pactl list short sources`, for outputs replace `sources` with `sinks`.
120 |
121 | ### Notes on using `--monitor`:
122 |
123 | - By default, without using --monitor the osd will be shown on all monitors
124 | - On setups with multiple monitors, if you only want to show the osd on the focused monitor, you can do so with the help of window manager specific commands:
125 | ```sh
126 | # Sway
127 | swayosd-client --monitor "$(swaymsg -t get_outputs | jq -r '.[] | select(.focused == true).name')" --output-volume raise
128 |
129 | # Hyprland
130 | swayosd-client --monitor "$(hyprctl monitors -j | jq -r '.[] | select(.focused == true).name')" --output-volume raise
131 | ```
132 |
133 | ## Theming
134 |
135 | Since SwayOSD uses GTK, its appearance can be changed. Initially scss is used, which GTK does not support, so we need to use plain css.
136 | The style conifg file is in `~/.config/swayosd/style.css` (it is not automatically generated). For reference you can check [this](https://github.com/ErikReider/SwayOSD/blob/main/data/style/style.scss) and [this](https://github.com/ErikReider/SwayOSD/issues/36).
137 |
138 | ## Brightness Control
139 |
140 | Some devices may not have permission to write `/sys/class/backlight/*/brightness`.
141 | So using the provided packaged `udev` rules + adding the user to `video` group
142 | by running `sudo usermod -a -G video $USER`, everything should work as expected.
143 |
--------------------------------------------------------------------------------
/build-scripts/swayosd-git.rpkg.spec:
--------------------------------------------------------------------------------
1 | # vim: syntax=spec
2 | %global alt_pkg_name swayosd
3 |
4 | Name: %{alt_pkg_name}-git
5 | Version: {{{ git_repo_release lead="$(git describe --tags --abbrev=0)" }}}
6 | Release: {{{ echo -n "$(git rev-list --all --count)" }}}%{?dist}
7 | Summary: A GTK based on screen display for keyboard shortcuts like caps-lock and volume
8 | Provides: %{alt_pkg_name} = %{version}-%{release}
9 | Provides: %{alt_pkg_name}-git = %{version}-%{release}
10 | License: GPLv3
11 | URL: https://github.com/ErikReider/swayosd
12 | VCS: {{{ git_repo_vcs }}}
13 | Source: {{{ git_repo_pack }}}
14 |
15 | # TODO: Use fedora RPM rust packages
16 | BuildRequires: meson >= 1.5.1
17 | BuildRequires: rust
18 | BuildRequires: cargo
19 | BuildRequires: pkgconfig(gtk4)
20 | BuildRequires: pkgconfig(gtk4-layer-shell-0)
21 | BuildRequires: pkgconfig(glib-2.0) >= 2.50
22 | BuildRequires: pkgconfig(gobject-introspection-1.0) >= 1.68
23 | BuildRequires: pkgconfig(gee-0.8) >= 0.20
24 | BuildRequires: pkgconfig(libpulse)
25 | BuildRequires: pkgconfig(libudev)
26 | BuildRequires: pkgconfig(libevdev)
27 | BuildRequires: pkgconfig(libinput)
28 | BuildRequires: pkgconfig(dbus-1)
29 | BuildRequires: systemd-devel
30 | BuildRequires: systemd
31 | BuildRequires: sassc
32 |
33 | Requires: dbus
34 | %{?systemd_requires}
35 |
36 | %description
37 | A OSD window for common actions like volume and capslock.
38 |
39 | %prep
40 | {{{ git_repo_setup_macro }}}
41 |
42 | %build
43 | %meson
44 | %meson_build
45 |
46 | %install
47 | %meson_install
48 |
49 | %files
50 | %doc README.md
51 | %{_bindir}/swayosd-client
52 | %{_bindir}/swayosd-server
53 | %{_bindir}/swayosd-libinput-backend
54 | %license LICENSE
55 | %config(noreplace) %{_sysconfdir}/xdg/swayosd/backend.toml
56 | %config(noreplace) %{_sysconfdir}/xdg/swayosd/config.toml
57 | %config(noreplace) %{_sysconfdir}/xdg/swayosd/style.css
58 | %{_unitdir}/swayosd-libinput-backend.service
59 | %{_libdir}/udev/rules.d/99-swayosd.rules
60 | %{_datadir}/dbus-1/system-services/org.erikreider.swayosd.service
61 | %{_datadir}/dbus-1/system.d/org.erikreider.swayosd.conf
62 | %{_datadir}/polkit-1/actions/org.erikreider.swayosd.policy
63 | %{_datadir}/polkit-1/rules.d/org.erikreider.swayosd.rules
64 |
65 | # Changelog will be empty until you make first annotated Git tag.
66 | %changelog
67 | {{{ git_repo_changelog }}}
68 |
--------------------------------------------------------------------------------
/build.rs:
--------------------------------------------------------------------------------
1 | use std::{env, process::Command};
2 |
3 | fn main() {
4 | let output = Command::new("glib-compile-resources")
5 | .args(&["./data/swayosd.gresource.xml", "--sourcedir=./data"])
6 | .arg(&format!(
7 | "--target={}/swayosd.gresource",
8 | env::var("OUT_DIR").unwrap()
9 | ))
10 | .status()
11 | .expect("failed to execute process");
12 | assert!(output.success());
13 | }
14 |
--------------------------------------------------------------------------------
/data/config/backend.toml:
--------------------------------------------------------------------------------
1 | [input]
2 |
--------------------------------------------------------------------------------
/data/config/config.toml:
--------------------------------------------------------------------------------
1 | [client]
2 | ## style file for the OSD
3 | # style = /etc/xdg/swayosd/style.css
4 |
5 | ## on which height to show the OSD
6 | # top_margin = 0.85
7 |
8 | ## The maximum volume that can be reached in %
9 | # max_volume = 150
10 |
11 | ## show percentage on the right of the OSD
12 | # show_percentage = true
13 |
14 | ## set format for the media player OSD
15 | # playerctl_format = "{artist} - {title}"
16 | ## Available values:
17 | ## artist, albumArtist, title, album, trackNumber, discNumber, autoRating
18 |
19 | [server]
20 |
--------------------------------------------------------------------------------
/data/dbus/org.erikreider.swayosd.conf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
14 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/caps-lock-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/display-brightness-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/missing-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/num-lock-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/pause-large-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/play-large-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/playlist-consecutive-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/playlist-shuffle-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/scroll-lock-symbolic.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/seek-backward-large-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/seek-forward-large-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/sink-volume-high-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/sink-volume-low-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/sink-volume-medium-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/sink-volume-muted-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/sink-volume-overamplified-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/source-volume-high-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/source-volume-low-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/source-volume-medium-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/source-volume-muted-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/stop-large-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/data/meson.build:
--------------------------------------------------------------------------------
1 | datadir = get_option('datadir')
2 | libdir = get_option('libdir')
3 |
4 | # udev rules
5 | install_data(
6 | join_paths('udev', '99-swayosd.rules'),
7 | install_dir: join_paths(libdir, 'udev', 'rules.d')
8 | )
9 | # Dbus conf
10 | install_data(
11 | join_paths('dbus', 'org.erikreider.swayosd.conf'),
12 | install_dir: join_paths(datadir, 'dbus-1', 'system.d')
13 | )
14 | # Polkit rule
15 | install_data(
16 | join_paths('polkit', 'rules', 'org.erikreider.swayosd.rules'),
17 | install_dir: join_paths(datadir, 'polkit-1', 'rules.d')
18 | )
19 | # Polkit policy
20 | conf_data = configuration_data()
21 | conf_data.set('bindir', join_paths(get_option('prefix'), get_option('bindir')))
22 | configure_file(
23 | input: join_paths('polkit', 'actions', 'org.erikreider.swayosd.policy.in'),
24 | output: 'org.erikreider.swayosd.policy',
25 | configuration: conf_data,
26 | install: true,
27 | install_dir: join_paths(datadir, 'polkit-1', 'actions')
28 | )
29 | # Dbus service
30 | configure_file(
31 | configuration: conf_data,
32 | input: join_paths('services', 'dbus', 'org.erikreider.swayosd.service.in'),
33 | output: '@BASENAME@',
34 | install_dir: datadir + '/dbus-1/system-services'
35 | )
36 |
37 | # Systemd service unit
38 | systemd = dependency('systemd', required: false)
39 | if systemd.found()
40 | systemd_service_install_dir = systemd.get_variable(pkgconfig :'systemdsystemunitdir')
41 | else
42 | systemd_service_install_dir = join_paths(libdir, 'systemd', 'system')
43 | endif
44 |
45 | configure_file(
46 | configuration: conf_data,
47 | input: join_paths('services', 'systemd', 'swayosd-libinput-backend.service.in'),
48 | output: '@BASENAME@',
49 | install_dir: systemd_service_install_dir
50 | )
51 |
52 | # SCSS Compilation
53 | style_css = custom_target(
54 | 'SCSS Compilation',
55 | build_by_default: true,
56 | input : 'style/style.scss',
57 | output : 'style.css',
58 | install: true,
59 | install_dir: config_path,
60 | command : [
61 | sassc,
62 | '@INPUT@',
63 | '@OUTPUT@'
64 | ],
65 | )
66 |
67 | message(style_css.full_path())
68 |
69 | install_data(['config/config.toml', 'config/backend.toml'],
70 | install_dir : config_path
71 | )
72 |
--------------------------------------------------------------------------------
/data/polkit/actions/org.erikreider.swayosd.policy.in:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Run the backend as root to read input devices through libinput.
6 | pkexec @bindir@/swayosd-libinput-backend
7 |
8 | auth_admin
9 | auth_admin
10 | auth_admin_keep
11 |
12 | @bindir@/swayosd-libinput-backend
13 |
14 | false
15 |
16 |
17 |
--------------------------------------------------------------------------------
/data/polkit/rules/org.erikreider.swayosd.rules:
--------------------------------------------------------------------------------
1 | // vim: ft=javascript
2 | // Allow "wheel" group users to run the swayosd backend
3 | polkit.addRule(function(action, subject) {
4 | if (action.id == "org.erikreider.swayosd-libinput-backend" && subject.isInGroup("wheel")) {
5 | return polkit.Result.YES;
6 | }
7 | });
8 |
--------------------------------------------------------------------------------
/data/services/dbus/org.erikreider.swayosd.service.in:
--------------------------------------------------------------------------------
1 | [D-BUS Service]
2 | Name=org.erikreider.swayosd
3 | Exec=/bin/false
4 | User=root
5 | SystemdService=swayosd-libinput-backend.service
6 |
--------------------------------------------------------------------------------
/data/services/systemd/swayosd-libinput-backend.service.in:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=SwayOSD LibInput backend for listening to certain keys like CapsLock, ScrollLock, VolumeUp, etc...
3 | Documentation=https://github.com/ErikReider/SwayOSD
4 | PartOf=graphical.target
5 | After=graphical.target
6 |
7 | [Service]
8 | Type=dbus
9 | BusName=org.erikreider.swayosd
10 | ExecStart=@bindir@/swayosd-libinput-backend
11 | Restart=on-failure
12 |
13 | [Install]
14 | WantedBy=graphical.target
15 |
--------------------------------------------------------------------------------
/data/style/style.scss:
--------------------------------------------------------------------------------
1 | window#osd {
2 | padding: 12px 20px;
3 | border-radius: 999px;
4 | border: none;
5 | background: #{"alpha(@theme_bg_color, 0.8)"};
6 |
7 | #container {
8 | margin: 16px;
9 | }
10 |
11 | image,
12 | label {
13 | color: #{"@theme_fg_color"};
14 | }
15 |
16 | progressbar:disabled,
17 | image:disabled {
18 | opacity: 0.5;
19 | }
20 |
21 | progressbar {
22 | min-height: 6px;
23 | border-radius: 999px;
24 | background: transparent;
25 | border: none;
26 | }
27 | trough {
28 | min-height: inherit;
29 | border-radius: inherit;
30 | border: none;
31 | background: #{"alpha(@theme_fg_color, 0.5)"};
32 | }
33 | progress {
34 | min-height: inherit;
35 | border-radius: inherit;
36 | border: none;
37 | background: #{"@theme_fg_color"};
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/data/swayosd.gresource.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | icons/scalable/status/missing-symbolic.svg
5 |
6 | icons/scalable/status/caps-lock-symbolic.svg
7 | icons/scalable/status/num-lock-symbolic.svg
8 | icons/scalable/status/scroll-lock-symbolic.svg
9 |
10 | icons/scalable/status/display-brightness-symbolic.svg
11 |
12 | icons/scalable/status/sink-volume-overamplified-symbolic.svg
13 | icons/scalable/status/sink-volume-high-symbolic.svg
14 | icons/scalable/status/sink-volume-medium-symbolic.svg
15 | icons/scalable/status/sink-volume-low-symbolic.svg
16 | icons/scalable/status/sink-volume-muted-symbolic.svg
17 | icons/scalable/status/source-volume-high-symbolic.svg
18 | icons/scalable/status/source-volume-medium-symbolic.svg
19 | icons/scalable/status/source-volume-low-symbolic.svg
20 | icons/scalable/status/source-volume-muted-symbolic.svg
21 |
22 | icons/scalable/status/pause-large-symbolic.svg
23 | icons/scalable/status/play-large-symbolic.svg
24 | icons/scalable/status/seek-backward-large-symbolic.svg
25 | icons/scalable/status/seek-forward-large-symbolic.svg
26 | icons/scalable/status/playlist-consecutive-symbolic.svg
27 | icons/scalable/status/playlist-shuffle-symbolic.svg
28 | icons/scalable/status/stop-large-symbolic.svg
29 |
30 |
31 |
--------------------------------------------------------------------------------
/data/udev/99-swayosd.rules:
--------------------------------------------------------------------------------
1 | ACTION=="add", SUBSYSTEM=="backlight", RUN+="/bin/chgrp video /sys/class/backlight/%k/brightness"
2 | ACTION=="add", SUBSYSTEM=="backlight", RUN+="/bin/chmod g+w /sys/class/backlight/%k/brightness"
3 |
--------------------------------------------------------------------------------
/meson.build:
--------------------------------------------------------------------------------
1 | project('swayosd', 'rust',
2 | version: '0.2.0',
3 | meson_version: '>= 0.62.0',
4 | default_options: [ 'warning_level=2', 'werror=false', ],
5 | )
6 |
7 | config_path = join_paths(get_option('sysconfdir'), 'xdg', 'swayosd')
8 |
9 | # glib-compile-resources Dependency
10 | assert(find_program('glib-compile-resources').found())
11 |
12 | # SCSS Dependency
13 | sassc = find_program('sassc')
14 | assert(sassc.found())
15 |
16 | subdir('data')
17 |
18 | subdir('src')
19 |
--------------------------------------------------------------------------------
/rust-toolchain:
--------------------------------------------------------------------------------
1 | nightly
2 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | max_width = 100
2 | hard_tabs = true
3 | tab_spaces = 4
4 | newline_style = "Auto"
5 | indent_style = "Block"
6 | use_small_heuristics = "Default"
7 | fn_call_width = 60
8 | attr_fn_like_width = 70
9 | struct_lit_width = 18
10 | struct_variant_width = 35
11 | array_width = 60
12 | chain_width = 60
13 | single_line_if_else_max_width = 50
14 | wrap_comments = false
15 | format_code_in_doc_comments = false
16 | doc_comment_code_block_width = 100
17 | comment_width = 80
18 | normalize_comments = false
19 | normalize_doc_attributes = false
20 | format_strings = false
21 | format_macro_matchers = false
22 | format_macro_bodies = true
23 | hex_literal_case = "Preserve"
24 | empty_item_single_line = true
25 | struct_lit_single_line = true
26 | fn_single_line = false
27 | where_single_line = false
28 | imports_indent = "Block"
29 | imports_layout = "Mixed"
30 | imports_granularity = "Preserve"
31 | group_imports = "Preserve"
32 | reorder_imports = true
33 | reorder_modules = true
34 | reorder_impl_items = false
35 | type_punctuation_density = "Wide"
36 | space_before_colon = false
37 | space_after_colon = true
38 | spaces_around_ranges = false
39 | binop_separator = "Front"
40 | remove_nested_parens = true
41 | combine_control_expr = true
42 | short_array_element_width_threshold = 10
43 | overflow_delimited_expr = false
44 | struct_field_align_threshold = 0
45 | enum_discrim_align_threshold = 0
46 | match_arm_blocks = true
47 | match_arm_leading_pipes = "Never"
48 | force_multiline_blocks = false
49 | fn_params_layout = "Tall"
50 | brace_style = "SameLineWhere"
51 | control_brace_style = "AlwaysSameLine"
52 | trailing_semicolon = true
53 | trailing_comma = "Vertical"
54 | match_block_trailing_comma = false
55 | blank_lines_upper_bound = 1
56 | blank_lines_lower_bound = 0
57 | edition = "2015"
58 | style_edition = "2021"
59 | inline_attribute_width = 0
60 | format_generated_files = true
61 | merge_derives = true
62 | use_try_shorthand = false
63 | use_field_init_shorthand = false
64 | force_explicit_abi = true
65 | condense_wildcard_suffixes = false
66 | color = "Auto"
67 | unstable_features = false
68 | disable_all_formatting = false
69 | skip_children = false
70 | show_parse_errors = true
71 | error_on_line_overflow = false
72 | error_on_unformatted = false
73 | ignore = []
74 | emit_mode = "Files"
75 | make_backup = false
76 |
--------------------------------------------------------------------------------
/src/argtypes.rs:
--------------------------------------------------------------------------------
1 | use std::fmt;
2 | use std::str::{self};
3 |
4 | #[derive(Clone, Debug, PartialEq, PartialOrd)]
5 | pub enum ArgTypes {
6 | // should always be first to set a global variable before executing related functions
7 | DeviceName = isize::MIN,
8 | TopMargin = isize::MIN + 1,
9 | MaxVolume = isize::MIN + 2,
10 | CustomIcon = isize::MIN + 3,
11 | Player = isize::MIN + 4,
12 | MonitorName = isize::MIN + 5,
13 | // Other
14 | None = 0,
15 | CapsLock = 1,
16 | SinkVolumeRaise = 2,
17 | SinkVolumeLower = 3,
18 | SinkVolumeMuteToggle = 4,
19 | SourceVolumeRaise = 5,
20 | SourceVolumeLower = 6,
21 | SourceVolumeMuteToggle = 7,
22 | BrightnessRaise = 8,
23 | BrightnessLower = 9,
24 | BrightnessSet = 12,
25 | NumLock = 10,
26 | ScrollLock = 11,
27 | CustomMessage = 13,
28 | Playerctl = 14,
29 | }
30 |
31 | impl fmt::Display for ArgTypes {
32 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 | let string = match self {
34 | ArgTypes::None => "NONE",
35 | ArgTypes::CapsLock => "CAPSLOCK",
36 | ArgTypes::MaxVolume => "MAX-VOLUME",
37 | ArgTypes::SinkVolumeRaise => "SINK-VOLUME-RAISE",
38 | ArgTypes::SinkVolumeLower => "SINK-VOLUME-LOWER",
39 | ArgTypes::SinkVolumeMuteToggle => "SINK-VOLUME-MUTE-TOGGLE",
40 | ArgTypes::SourceVolumeRaise => "SOURCE-VOLUME-RAISE",
41 | ArgTypes::SourceVolumeLower => "SOURCE-VOLUME-LOWER",
42 | ArgTypes::SourceVolumeMuteToggle => "SOURCE-VOLUME-MUTE-TOGGLE",
43 | ArgTypes::BrightnessRaise => "BRIGHTNESS-RAISE",
44 | ArgTypes::BrightnessLower => "BRIGHTNESS-LOWER",
45 | ArgTypes::BrightnessSet => "BRIGHTNESS-SET",
46 | ArgTypes::NumLock => "NUM-LOCK",
47 | ArgTypes::ScrollLock => "SCROLL-LOCK",
48 | ArgTypes::DeviceName => "DEVICE-NAME",
49 | ArgTypes::TopMargin => "TOP-MARGIN",
50 | ArgTypes::CustomMessage => "CUSTOM-MESSAGE",
51 | ArgTypes::CustomIcon => "CUSTOM-ICON",
52 | ArgTypes::Playerctl => "PLAYERCTL",
53 | ArgTypes::Player => "PLAYER",
54 | ArgTypes::MonitorName => "MONITOR-NAME",
55 | };
56 | return write!(f, "{}", string);
57 | }
58 | }
59 |
60 | impl str::FromStr for ArgTypes {
61 | type Err = String;
62 |
63 | fn from_str(input: &str) -> Result {
64 | let result = match input {
65 | "CAPSLOCK" => ArgTypes::CapsLock,
66 | "SINK-VOLUME-RAISE" => ArgTypes::SinkVolumeRaise,
67 | "SINK-VOLUME-LOWER" => ArgTypes::SinkVolumeLower,
68 | "SINK-VOLUME-MUTE-TOGGLE" => ArgTypes::SinkVolumeMuteToggle,
69 | "SOURCE-VOLUME-RAISE" => ArgTypes::SourceVolumeRaise,
70 | "SOURCE-VOLUME-LOWER" => ArgTypes::SourceVolumeLower,
71 | "SOURCE-VOLUME-MUTE-TOGGLE" => ArgTypes::SourceVolumeMuteToggle,
72 | "BRIGHTNESS-RAISE" => ArgTypes::BrightnessRaise,
73 | "BRIGHTNESS-LOWER" => ArgTypes::BrightnessLower,
74 | "BRIGHTNESS-SET" => ArgTypes::BrightnessSet,
75 | "MAX-VOLUME" => ArgTypes::MaxVolume,
76 | "NUM-LOCK" => ArgTypes::NumLock,
77 | "SCROLL-LOCK" => ArgTypes::ScrollLock,
78 | "DEVICE-NAME" => ArgTypes::DeviceName,
79 | "TOP-MARGIN" => ArgTypes::TopMargin,
80 | "CUSTOM-MESSAGE" => ArgTypes::CustomMessage,
81 | "CUSTOM-ICON" => ArgTypes::CustomIcon,
82 | "PLAYERCTL" => ArgTypes::Playerctl,
83 | "PLAYER" => ArgTypes::Player,
84 | "MONITOR-NAME" => ArgTypes::MonitorName,
85 | other_type => return Err(other_type.to_owned()),
86 | };
87 | Ok(result)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/brightness_backend/blight.rs:
--------------------------------------------------------------------------------
1 | use blight::{Device, Direction};
2 |
3 | use super::{BrightnessBackend, BrightnessBackendConstructor};
4 |
5 | pub(super) struct Blight {
6 | device: Device,
7 | }
8 |
9 | impl BrightnessBackendConstructor for Blight {
10 | fn try_new(device_name: Option) -> anyhow::Result {
11 | Ok(Self {
12 | device: Device::new(device_name.map(Into::into))?,
13 | })
14 | }
15 | }
16 |
17 | impl BrightnessBackend for Blight {
18 | fn get_current(&mut self) -> u32 {
19 | self.device.reload();
20 | self.device.current()
21 | }
22 |
23 | fn get_max(&mut self) -> u32 {
24 | self.device.max()
25 | }
26 |
27 | fn lower(&mut self, by: u32) -> anyhow::Result<()> {
28 | let val = self.device.calculate_change(by, Direction::Dec);
29 | Ok(self.device.write_value(val)?)
30 | }
31 |
32 | fn raise(&mut self, by: u32) -> anyhow::Result<()> {
33 | let val = self.device.calculate_change(by, Direction::Inc);
34 | Ok(self.device.write_value(val)?)
35 | }
36 |
37 | fn set(&mut self, val: u32) -> anyhow::Result<()> {
38 | Ok(self.device.write_value(val)?)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/brightness_backend/brightnessctl.rs:
--------------------------------------------------------------------------------
1 | use super::{BrightnessBackend, BrightnessBackendConstructor};
2 |
3 | const EXPECT_STR: &str = "VirtualDevice didn't test the command during initialization";
4 |
5 | use anyhow::bail;
6 | use std::{error::Error, process::Command, str::FromStr};
7 | use thiserror::Error;
8 |
9 | enum CliArg<'arg> {
10 | Simple(&'arg str),
11 | KeyValue { key: &'arg str, value: &'arg str },
12 | }
13 |
14 | impl<'arg> From<&'arg str> for CliArg<'arg> {
15 | fn from(value: &'arg str) -> Self {
16 | CliArg::Simple(value)
17 | }
18 | }
19 |
20 | impl<'arg> From<(&'arg str, &'arg str)> for CliArg<'arg> {
21 | fn from((key, value): (&'arg str, &'arg str)) -> Self {
22 | CliArg::KeyValue { key, value }
23 | }
24 | }
25 |
26 | #[derive(Default)]
27 | struct VirtualDevice {
28 | name: Option,
29 | current: Option,
30 | max: Option,
31 | }
32 |
33 | pub(super) struct BrightnessCtl {
34 | device: VirtualDevice,
35 | }
36 |
37 | #[derive(Error, Debug)]
38 | #[error("Requested device '{device_name}' does not exist ")]
39 | pub struct DeviceDoesntExistError {
40 | device_name: String,
41 | }
42 |
43 | impl VirtualDevice {
44 | fn try_new(device_name: Option) -> anyhow::Result {
45 | let s = Self {
46 | name: device_name.clone(),
47 | ..Default::default()
48 | };
49 |
50 | // Check if the command is available to us before running it in other occasions
51 | let exit_code = s.command(CliArg::Simple("info")).output()?.status;
52 |
53 | if exit_code.success() {
54 | Ok(s)
55 | } else {
56 | bail!(DeviceDoesntExistError {
57 | device_name: device_name.unwrap()
58 | })
59 | }
60 | }
61 |
62 | fn command(&self, arg: CliArg) -> Command {
63 | let mut cmd = Command::new("brightnessctl");
64 |
65 | if let Some(name) = &self.name {
66 | cmd.arg("--device").arg(name);
67 | }
68 |
69 | match arg {
70 | CliArg::Simple(arg) => cmd.arg(arg),
71 | CliArg::KeyValue { key, value } => cmd.arg(key).arg(value),
72 | };
73 |
74 | cmd
75 | }
76 |
77 | fn run<'arg, T: FromStr, A: Into>>(&self, arg: A) -> anyhow::Result
78 | where
79 | ::Err: Error + Send + Sync + 'static,
80 | {
81 | let cmd_output = self.command(arg.into()).output()?.stdout;
82 |
83 | let cmd_output = String::from_utf8_lossy(&cmd_output);
84 |
85 | Ok(cmd_output.trim().parse()?)
86 | }
87 |
88 | fn get_current(&mut self) -> u32 {
89 | match self.current {
90 | Some(val) => val,
91 | None => {
92 | let val = self.run("get").expect(EXPECT_STR);
93 | self.current = Some(val);
94 | val
95 | }
96 | }
97 | }
98 |
99 | fn get_max(&mut self) -> u32 {
100 | match self.max {
101 | Some(val) => val,
102 | None => {
103 | let val = self.run("max").expect(EXPECT_STR);
104 | self.max = Some(val);
105 | val
106 | }
107 | }
108 | }
109 |
110 | fn set_percent(&mut self, mut val: u32) -> anyhow::Result<()> {
111 | val = val.clamp(0, 100);
112 | self.current = self.max.map(|max| val * max / 100);
113 | let _: String = self.run(("set", &*format!("{val}%")))?;
114 | Ok(())
115 | }
116 | }
117 |
118 | impl BrightnessBackendConstructor for BrightnessCtl {
119 | fn try_new(device_name: Option) -> anyhow::Result {
120 | Ok(Self {
121 | device: VirtualDevice::try_new(device_name)?,
122 | })
123 | }
124 | }
125 |
126 | impl BrightnessBackend for BrightnessCtl {
127 | fn get_current(&mut self) -> u32 {
128 | self.device.get_current()
129 | }
130 |
131 | fn get_max(&mut self) -> u32 {
132 | self.device.get_max()
133 | }
134 |
135 | fn lower(&mut self, by: u32) -> anyhow::Result<()> {
136 | let curr = self.get_current();
137 | let max = self.get_max();
138 |
139 | let curr = curr * 100 / max;
140 |
141 | self.device.set_percent(curr.saturating_sub(by))
142 | }
143 |
144 | fn raise(&mut self, by: u32) -> anyhow::Result<()> {
145 | let curr = self.get_current();
146 | let max = self.get_max();
147 |
148 | let curr = curr * 100 / max;
149 |
150 | self.device.set_percent(curr + by)
151 | }
152 |
153 | fn set(&mut self, val: u32) -> anyhow::Result<()> {
154 | self.device.set_percent(val)
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/brightness_backend/mod.rs:
--------------------------------------------------------------------------------
1 | use self::{blight::Blight, brightnessctl::BrightnessCtl};
2 |
3 | mod blight;
4 |
5 | mod brightnessctl;
6 |
7 | pub type BrightnessBackendResult = anyhow::Result>;
8 |
9 | pub trait BrightnessBackendConstructor: BrightnessBackend + Sized + 'static {
10 | fn try_new(device_name: Option) -> anyhow::Result;
11 |
12 | fn try_new_boxed(device_name: Option) -> BrightnessBackendResult {
13 | let backend = Self::try_new(device_name);
14 | match backend {
15 | Ok(backend) => Ok(Box::new(backend)),
16 | Err(e) => Err(e),
17 | }
18 | }
19 | }
20 |
21 | pub trait BrightnessBackend {
22 | fn get_current(&mut self) -> u32;
23 | fn get_max(&mut self) -> u32;
24 |
25 | fn lower(&mut self, by: u32) -> anyhow::Result<()>;
26 | fn raise(&mut self, by: u32) -> anyhow::Result<()>;
27 | fn set(&mut self, val: u32) -> anyhow::Result<()>;
28 | }
29 |
30 | #[allow(dead_code)]
31 | pub fn get_preferred_backend(device_name: Option) -> BrightnessBackendResult {
32 | println!("Trying BrightnessCtl Backend...");
33 | BrightnessCtl::try_new_boxed(device_name.clone()).or_else(|_| {
34 | println!("...Command failed! Falling back to Blight");
35 | Blight::try_new_boxed(device_name)
36 | })
37 | }
38 |
--------------------------------------------------------------------------------
/src/client/main.rs:
--------------------------------------------------------------------------------
1 | #[path = "../argtypes.rs"]
2 | mod argtypes;
3 | #[path = "../config.rs"]
4 | mod config;
5 | #[path = "../global_utils.rs"]
6 | mod global_utils;
7 |
8 | #[path = "../brightness_backend/mod.rs"]
9 | mod brightness_backend;
10 |
11 | use config::APPLICATION_NAME;
12 | use global_utils::{handle_application_args, HandleLocalStatus};
13 | use gtk::glib::{OptionArg, OptionFlags};
14 | use gtk::{gio::ApplicationFlags, Application};
15 | use gtk::{glib, prelude::*};
16 | use std::env::args_os;
17 | use std::path::PathBuf;
18 | use zbus::{blocking::Connection, proxy};
19 |
20 | #[proxy(
21 | interface = "org.erikreider.swayosd",
22 | default_service = "org.erikreider.swayosd-server",
23 | default_path = "/org/erikreider/swayosd"
24 | )]
25 | trait Server {
26 | async fn handle_action(&self, arg_type: String, data: String) -> zbus::Result;
27 | }
28 |
29 | fn get_proxy() -> zbus::Result> {
30 | let connection = Connection::session()?;
31 | Ok(ServerProxyBlocking::new(&connection)?)
32 | }
33 |
34 | fn main() -> Result<(), glib::Error> {
35 | // Get config path from command line
36 | let mut config_path: Option = None;
37 | let mut args = args_os().into_iter();
38 | while let Some(arg) = args.next() {
39 | match arg.to_str() {
40 | Some("--config") => {
41 | if let Some(path) = args.next() {
42 | config_path = Some(path.into());
43 | }
44 | }
45 | _ => (),
46 | }
47 | }
48 |
49 | // Parse Config
50 | let _client_config = config::user::read_user_config(config_path.as_deref())
51 | .expect("Failed to parse config file")
52 | .client;
53 |
54 | // Make sure that the server is running
55 | let proxy = match get_proxy() {
56 | Ok(proxy) => match proxy.0.introspect() {
57 | Ok(_) => proxy,
58 | Err(err) => {
59 | eprintln!("Could not connect to SwayOSD Server with error: {}", err);
60 | std::process::exit(1);
61 | }
62 | },
63 | Err(err) => {
64 | eprintln!("Dbus error: {}", err);
65 | std::process::exit(1);
66 | }
67 | };
68 |
69 | let app = Application::new(Some(APPLICATION_NAME), ApplicationFlags::FLAGS_NONE);
70 |
71 | // Config cmdline arg for documentation
72 | app.add_main_option(
73 | "config",
74 | glib::Char::from(0),
75 | OptionFlags::NONE,
76 | OptionArg::String,
77 | "Use a custom config file instead of looking for one.",
78 | Some(""),
79 | );
80 |
81 | // Capslock cmdline arg
82 | app.add_main_option(
83 | "caps-lock",
84 | glib::Char::from(0),
85 | OptionFlags::NONE,
86 | OptionArg::None,
87 | "Shows capslock osd. Note: Doesn't toggle CapsLock, just displays the status",
88 | None,
89 | );
90 | app.add_main_option(
91 | "num-lock",
92 | glib::Char::from(0),
93 | OptionFlags::NONE,
94 | OptionArg::None,
95 | "Shows numlock osd. Note: Doesn't toggle NumLock, just displays the status",
96 | None,
97 | );
98 | app.add_main_option(
99 | "scroll-lock",
100 | glib::Char::from(0),
101 | OptionFlags::NONE,
102 | OptionArg::None,
103 | "Shows scrolllock osd. Note: Doesn't toggle ScrollLock, just displays the status",
104 | None,
105 | );
106 | // Capslock with specific LED cmdline arg
107 | app.add_main_option(
108 | "caps-lock-led",
109 | glib::Char::from(0),
110 | OptionFlags::NONE,
111 | OptionArg::String,
112 | "Shows capslock osd. Uses LED class name. Note: Doesn't toggle CapsLock, just displays the status",
113 | Some("LED class name (/sys/class/leds/NAME)"),
114 | );
115 | app.add_main_option(
116 | "num-lock-led",
117 | glib::Char::from(0),
118 | OptionFlags::NONE,
119 | OptionArg::String,
120 | "Shows numlock osd. Uses LED class name. Note: Doesn't toggle NumLock, just displays the status",
121 | Some("LED class name (/sys/class/leds/NAME)"),
122 | );
123 | app.add_main_option(
124 | "scroll-lock-led",
125 | glib::Char::from(0),
126 | OptionFlags::NONE,
127 | OptionArg::String,
128 | "Shows scrolllock osd. Uses LED class name. Note: Doesn't toggle ScrollLock, just displays the status",
129 | Some("LED class name (/sys/class/leds/NAME)"),
130 | );
131 | // Sink volume cmdline arg
132 | app.add_main_option(
133 | "output-volume",
134 | glib::Char::from(0),
135 | OptionFlags::NONE,
136 | OptionArg::String,
137 | "Shows volume osd and raises, loweres or mutes default sink volume",
138 | Some("raise|lower|mute-toggle|(±)number"),
139 | );
140 | // Source volume cmdline arg
141 | app.add_main_option(
142 | "input-volume",
143 | glib::Char::from(0),
144 | OptionFlags::NONE,
145 | OptionArg::String,
146 | "Shows volume osd and raises, loweres or mutes default source volume",
147 | Some("raise|lower|mute-toggle|(±)number"),
148 | );
149 |
150 | // Sink brightness cmdline arg
151 | app.add_main_option(
152 | "brightness",
153 | glib::Char::from(0),
154 | OptionFlags::NONE,
155 | OptionArg::String,
156 | "Shows brightness osd and raises or loweres all available sources of brightness device",
157 | Some("raise|lower|(±)number"),
158 | );
159 |
160 | // Control players cmdline arg
161 | app.add_main_option(
162 | "playerctl",
163 | glib::Char::from(0),
164 | OptionFlags::NONE,
165 | OptionArg::String,
166 | "Shows Playerctl osd and runs the playerctl command",
167 | Some("play-pause|play|pause|stop|next|prev|shuffle"),
168 | );
169 | app.add_main_option(
170 | "max-volume",
171 | glib::Char::from(0),
172 | OptionFlags::NONE,
173 | OptionArg::String,
174 | "Sets the maximum Volume",
175 | Some("(+)number"),
176 | );
177 | app.add_main_option(
178 | "device",
179 | glib::Char::from(0),
180 | OptionFlags::NONE,
181 | OptionArg::String,
182 | "For which device to increase/decrease audio",
183 | Some("Pulseaudio device name (pactl list short sinks|sources)"),
184 | );
185 | app.add_main_option(
186 | "player",
187 | glib::Char::from(0),
188 | OptionFlags::NONE,
189 | OptionArg::String,
190 | "For which player to run the playerctl commands",
191 | Some("auto|all|(playerctl -l)"),
192 | );
193 |
194 | app.add_main_option(
195 | "monitor",
196 | glib::Char::from(0),
197 | OptionFlags::NONE,
198 | OptionArg::String,
199 | "Which monitor to display osd on",
200 | Some("Monitor identifier (e.g., HDMI-A-1, DP-1)"),
201 | );
202 |
203 | app.add_main_option(
204 | "custom-message",
205 | glib::Char::from(0),
206 | OptionFlags::NONE,
207 | OptionArg::String,
208 | "Message to display",
209 | Some("text"),
210 | );
211 |
212 | app.add_main_option(
213 | "custom-icon",
214 | glib::Char::from(0),
215 | OptionFlags::NONE,
216 | OptionArg::String,
217 | "Icon to display when using custom-message. Icon name is from Freedesktop specification (https://specifications.freedesktop.org/icon-naming-spec/latest/)",
218 | Some("Icon name"),
219 | );
220 |
221 | // Parse args
222 | app.connect_handle_local_options(move |_app, args| {
223 | let variant = args.to_variant();
224 | if variant.n_children() == 0 {
225 | eprintln!("No args provided...");
226 | return HandleLocalStatus::FAILURE as i32;
227 | }
228 | let actions = match handle_application_args(variant) {
229 | (HandleLocalStatus::SUCCESS, actions) => actions,
230 | (status @ HandleLocalStatus::FAILURE, _) => return status as i32,
231 | (status @ HandleLocalStatus::CONTINUE, _) => return status as i32,
232 | };
233 | // execute the sorted actions
234 | for (arg_type, data) in actions {
235 | let _ = proxy.handle_action(arg_type.to_string(), data.unwrap_or(String::new()));
236 | }
237 |
238 | HandleLocalStatus::SUCCESS as i32
239 | });
240 |
241 | std::process::exit(app.run().into());
242 | }
243 |
--------------------------------------------------------------------------------
/src/config.rs:
--------------------------------------------------------------------------------
1 | #![allow(dead_code)]
2 |
3 | #[path = "config/backend.rs"]
4 | pub mod backend;
5 | #[path = "config/user.rs"]
6 | pub mod user;
7 |
8 | pub const DBUS_PATH: &str = "/org/erikreider/swayosd";
9 | pub const DBUS_BACKEND_NAME: &str = "org.erikreider.swayosd";
10 | pub const DBUS_SERVER_NAME: &str = "org.erikreider.swayosd-server";
11 |
12 | pub const APPLICATION_NAME: &str = "org.erikreider.swayosd";
13 |
--------------------------------------------------------------------------------
/src/config/backend.rs:
--------------------------------------------------------------------------------
1 | use gtk::glib::system_config_dirs;
2 | use serde_derive::Deserialize;
3 | use std::error::Error;
4 | use std::path::PathBuf;
5 |
6 | #[derive(Deserialize, Default, Debug)]
7 | #[serde(deny_unknown_fields)]
8 | pub struct InputBackendConfig {
9 | pub ignore_caps_lock_key: Option,
10 | }
11 |
12 | #[derive(Deserialize, Default, Debug)]
13 | #[serde(deny_unknown_fields)]
14 | pub struct BackendConfig {
15 | #[serde(default)]
16 | pub input: InputBackendConfig,
17 | }
18 |
19 | fn find_backend_config() -> Option {
20 | for path in system_config_dirs() {
21 | let path = path.join("swayosd").join("backend.toml");
22 | if path.exists() {
23 | return Some(path);
24 | }
25 | }
26 |
27 | None
28 | }
29 |
30 | pub fn read_backend_config() -> Result> {
31 | let path = match find_backend_config() {
32 | Some(path) => path,
33 | None => return Ok(Default::default()),
34 | };
35 |
36 | let config_file = std::fs::read_to_string(path)?;
37 | let config: BackendConfig = toml::from_str(&config_file)?;
38 | Ok(config)
39 | }
40 |
--------------------------------------------------------------------------------
/src/config/user.rs:
--------------------------------------------------------------------------------
1 | use gtk::glib::system_config_dirs;
2 | use gtk::glib::user_config_dir;
3 | use serde_derive::Deserialize;
4 | use std::error::Error;
5 | use std::path::Path;
6 | use std::path::PathBuf;
7 |
8 | #[derive(Deserialize, Default, Debug)]
9 | #[serde(deny_unknown_fields)]
10 | pub struct ClientConfig {}
11 |
12 | #[derive(Deserialize, Default, Debug, Clone)]
13 | #[serde(deny_unknown_fields)]
14 | pub struct ServerConfig {
15 | pub style: Option,
16 | pub top_margin: Option,
17 | pub max_volume: Option,
18 | pub show_percentage: Option,
19 | pub playerctl_format: Option,
20 | }
21 |
22 | #[derive(Deserialize, Default, Debug)]
23 | #[serde(deny_unknown_fields)]
24 | pub struct UserConfig {
25 | #[serde(default)]
26 | pub server: ServerConfig,
27 | #[serde(default)]
28 | pub client: ClientConfig,
29 | }
30 |
31 | fn find_user_config() -> Option {
32 | let path = user_config_dir().join("swayosd").join("config.toml");
33 | if path.exists() {
34 | return Some(path);
35 | }
36 |
37 | for path in system_config_dirs() {
38 | let path = path.join("swayosd").join("config.toml");
39 | if path.exists() {
40 | return Some(path);
41 | }
42 | }
43 |
44 | None
45 | }
46 |
47 | pub fn read_user_config(path: Option<&Path>) -> Result> {
48 | let path = match path.map(Path::to_owned).or_else(find_user_config) {
49 | Some(path) => path,
50 | None => return Ok(Default::default()),
51 | };
52 |
53 | let config_file = std::fs::read_to_string(path)?;
54 | let config: UserConfig = toml::from_str(&config_file)?;
55 | Ok(config)
56 | }
57 |
--------------------------------------------------------------------------------
/src/global_utils.rs:
--------------------------------------------------------------------------------
1 | use gtk::glib::{variant::DictEntry, Variant};
2 |
3 | use crate::argtypes::ArgTypes;
4 |
5 | pub enum HandleLocalStatus {
6 | FAILURE = 1,
7 | SUCCESS = 0,
8 | CONTINUE = -1,
9 | }
10 |
11 | pub(crate) fn handle_application_args(
12 | variant: Variant,
13 | ) -> (HandleLocalStatus, Vec<(ArgTypes, Option)>) {
14 | let mut actions: Vec<(ArgTypes, Option)> = Vec::new();
15 |
16 | if variant.n_children() == 0 {
17 | return (HandleLocalStatus::CONTINUE, actions);
18 | }
19 |
20 | if !variant.is_container() {
21 | eprintln!("VariantDict isn't a container!...");
22 | return (HandleLocalStatus::FAILURE, actions);
23 | }
24 |
25 | for i in 0..variant.n_children() {
26 | let child: DictEntry = variant.child_get(i);
27 |
28 | let (option, value): (ArgTypes, Option) = match child.key().as_str() {
29 | "caps-lock" => (ArgTypes::CapsLock, None),
30 | "num-lock" => (ArgTypes::NumLock, None),
31 | "scroll-lock" => (ArgTypes::ScrollLock, None),
32 | "caps-lock-led" => match child.value().str() {
33 | Some(led) => (ArgTypes::CapsLock, Some(led.to_owned())),
34 | None => {
35 | eprintln!("Value for caps-lock-led isn't a string!...");
36 | return (HandleLocalStatus::FAILURE, actions);
37 | }
38 | },
39 | "num-lock-led" => match child.value().str() {
40 | Some(led) => (ArgTypes::NumLock, Some(led.to_owned())),
41 | None => {
42 | eprintln!("Value for num-lock-led isn't a string!...");
43 | return (HandleLocalStatus::FAILURE, actions);
44 | }
45 | },
46 | "scroll-lock-led" => match child.value().str() {
47 | Some(led) => (ArgTypes::ScrollLock, Some(led.to_owned())),
48 | None => {
49 | eprintln!("Value for scroll-lock-led isn't a string!...");
50 | return (HandleLocalStatus::FAILURE, actions);
51 | }
52 | },
53 | "output-volume" => {
54 | let value = child.value().str().unwrap_or("");
55 | let parsed = volume_parser(false, value);
56 | match parsed {
57 | Ok(p) => p,
58 | Err(_) => return (HandleLocalStatus::FAILURE, actions),
59 | }
60 | }
61 | "input-volume" => {
62 | let value = child.value().str().unwrap_or("");
63 | let parsed = volume_parser(true, value);
64 | match parsed {
65 | Ok(p) => p,
66 | Err(_) => return (HandleLocalStatus::FAILURE, actions),
67 | }
68 | }
69 | "brightness" => {
70 | let value = child.value().str().unwrap_or("");
71 |
72 | match (value, value.parse::()) {
73 | // Parse custom step values
74 | (_, Ok(num)) => match value.get(..1) {
75 | Some("+") => (ArgTypes::BrightnessRaise, Some(num.to_string())),
76 | Some("-") => (ArgTypes::BrightnessLower, Some(num.abs().to_string())),
77 | _ => (ArgTypes::BrightnessSet, Some(num.to_string())),
78 | },
79 |
80 | ("raise", _) => (ArgTypes::BrightnessRaise, None),
81 | ("lower", _) => (ArgTypes::BrightnessLower, None),
82 | (e, _) => {
83 | eprintln!("Unknown brightness mode: \"{}\"!...", e);
84 | return (HandleLocalStatus::FAILURE, actions);
85 | }
86 | }
87 | }
88 | "max-volume" => {
89 | let value = child.value().str().unwrap_or("").trim();
90 | match value.parse::() {
91 | Ok(_) => (ArgTypes::MaxVolume, Some(value.to_string())),
92 | Err(_) => {
93 | eprintln!("{} is not a number between 0 and {}!", value, u8::MAX);
94 | return (HandleLocalStatus::FAILURE, actions);
95 | }
96 | }
97 | }
98 | "playerctl" => {
99 | let value = child.value().str().unwrap_or("");
100 | match value {
101 | "play-pause" | "play" | "pause" | "next" | "prev" | "previous" | "shuffle"
102 | | "stop" => (),
103 | x => {
104 | eprintln!("Unknown Playerctl command: \"{}\"!...", x);
105 | return (HandleLocalStatus::FAILURE, actions);
106 | }
107 | }
108 |
109 | (ArgTypes::Playerctl, Some(value.to_string()))
110 | }
111 | "device" => {
112 | let value = match child.value().str() {
113 | Some(v) => v.to_string(),
114 | None => {
115 | eprintln!("--device found but no name given");
116 | return (HandleLocalStatus::FAILURE, actions);
117 | }
118 | };
119 | (ArgTypes::DeviceName, Some(value))
120 | }
121 | "monitor" => {
122 | let value = match child.value().str() {
123 | Some(v) => v.to_string(),
124 | None => {
125 | eprintln!("--monitor found but no name given");
126 | return (HandleLocalStatus::FAILURE, actions);
127 | }
128 | };
129 | (ArgTypes::MonitorName, Some(value))
130 | }
131 | "custom-message" => {
132 | let value = match child.value().str() {
133 | Some(v) => v.to_string(),
134 | None => {
135 | eprintln!("--custom-message found but no message given");
136 | return (HandleLocalStatus::FAILURE, actions);
137 | }
138 | };
139 | (ArgTypes::CustomMessage, Some(value))
140 | }
141 | "custom-icon" => {
142 | let value = match child.value().str() {
143 | Some(v) => v.to_string(),
144 | None => {
145 | eprintln!("--custom-icon found but no icon given");
146 | return (HandleLocalStatus::FAILURE, actions);
147 | }
148 | };
149 | (ArgTypes::CustomIcon, Some(value))
150 | }
151 | "player" => {
152 | let value = match child.value().str() {
153 | Some(v) => v.to_string(),
154 | None => {
155 | eprintln!("--player found but no name given");
156 | return (HandleLocalStatus::FAILURE, actions);
157 | }
158 | };
159 | (ArgTypes::Player, Some(value))
160 | }
161 | "top-margin" => {
162 | let value = child.value().str().unwrap_or("").trim();
163 | match value.parse::() {
164 | Ok(top_margin) if (0.0f32..=1.0f32).contains(&top_margin) => {
165 | (ArgTypes::TopMargin, Some(value.to_string()))
166 | }
167 | _ => {
168 | eprintln!("{} is not a number between 0.0 and 1.0!", value);
169 | return (HandleLocalStatus::FAILURE, actions);
170 | }
171 | }
172 | }
173 | "style" => continue,
174 | "config" => continue,
175 | e => {
176 | eprintln!("Unknown Variant Key: \"{}\"!...", e);
177 | return (HandleLocalStatus::FAILURE, actions);
178 | }
179 | };
180 | if option != ArgTypes::None {
181 | actions.push((option, value));
182 | }
183 | }
184 |
185 | // sort actions so that they always get executed in the correct order
186 | if actions.len() > 0 {
187 | for i in 0..actions.len() - 1 {
188 | for j in i + 1..actions.len() {
189 | if actions[i].0 > actions[j].0 {
190 | let temp = actions[i].clone();
191 | actions[i] = actions[j].clone();
192 | actions[j] = temp;
193 | }
194 | }
195 | }
196 | }
197 | (HandleLocalStatus::SUCCESS, actions)
198 | }
199 |
200 | fn volume_parser(is_sink: bool, value: &str) -> Result<(ArgTypes, Option), i32> {
201 | let mut v = match (value, value.parse::()) {
202 | // Parse custom step values
203 | (_, Ok(num)) => (
204 | if num.is_positive() {
205 | ArgTypes::SinkVolumeRaise
206 | } else {
207 | ArgTypes::SinkVolumeLower
208 | },
209 | Some(num.abs().to_string()),
210 | ),
211 | ("raise", _) => (ArgTypes::SinkVolumeRaise, None),
212 | ("lower", _) => (ArgTypes::SinkVolumeLower, None),
213 | ("mute-toggle", _) => (ArgTypes::SinkVolumeMuteToggle, None),
214 | (e, _) => {
215 | eprintln!("Unknown output volume mode: \"{}\"!...", e);
216 | return Err(1);
217 | }
218 | };
219 | if is_sink {
220 | if v.0 == ArgTypes::SinkVolumeRaise {
221 | v.0 = ArgTypes::SourceVolumeRaise;
222 | } else if v.0 == ArgTypes::SinkVolumeLower {
223 | v.0 = ArgTypes::SourceVolumeLower;
224 | } else {
225 | v.0 = ArgTypes::SourceVolumeMuteToggle;
226 | }
227 | }
228 | Ok(v)
229 | }
230 |
--------------------------------------------------------------------------------
/src/input-backend/dbus_server.rs:
--------------------------------------------------------------------------------
1 | use zbus::object_server::SignalEmitter;
2 | use zbus::{connection, interface, Connection};
3 |
4 | use crate::config::{DBUS_BACKEND_NAME, DBUS_PATH};
5 |
6 | pub struct DbusServer;
7 |
8 | #[interface(name = "org.erikreider.swayosd")]
9 | impl DbusServer {
10 | #[zbus(signal)]
11 | pub async fn key_pressed(
12 | signal_ctxt: &SignalEmitter<'_>,
13 | key_code: u16,
14 | state: i32,
15 | ) -> zbus::Result<()>;
16 | }
17 |
18 | impl DbusServer {
19 | async fn get_connection(&self) -> zbus::Result {
20 | let conn = connection::Builder::system()?
21 | .name(DBUS_BACKEND_NAME)?
22 | .serve_at(DBUS_PATH, DbusServer)?
23 | .build()
24 | .await?;
25 |
26 | Ok(conn)
27 | }
28 |
29 | pub async fn init(&self) -> Connection {
30 | match self.get_connection().await {
31 | Ok(conn) => conn,
32 | Err(error) => {
33 | eprintln!("Error: {}", error);
34 | std::process::exit(1)
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/input-backend/main.rs:
--------------------------------------------------------------------------------
1 | use async_std::task::{self, sleep};
2 | use config::DBUS_PATH;
3 | use dbus_server::DbusServer;
4 | use evdev_rs::enums::{int_to_ev_key, EventCode, EV_KEY, EV_LED};
5 | use evdev_rs::DeviceWrapper;
6 | use input::event::keyboard::KeyboardEventTrait;
7 | use input::event::tablet_pad::KeyState;
8 | use input::event::{EventTrait, KeyboardEvent};
9 | use input::{Event, Libinput, LibinputInterface};
10 | use libc::{O_RDONLY, O_RDWR};
11 | use nix::poll::{poll, PollFd, PollFlags};
12 | use std::fs::{File, OpenOptions};
13 | use std::os::fd::AsRawFd;
14 | use std::os::fd::BorrowedFd;
15 | use std::os::unix::{fs::OpenOptionsExt, io::OwnedFd};
16 | use std::path::Path;
17 | use std::time::Duration;
18 | use zbus::object_server::InterfaceRef;
19 |
20 | #[path = "../config.rs"]
21 | mod config;
22 | mod dbus_server;
23 |
24 | struct EventInfo {
25 | device_path: String,
26 | ev_key: EV_KEY,
27 | }
28 |
29 | struct Interface;
30 |
31 | impl LibinputInterface for Interface {
32 | fn open_restricted(&mut self, path: &Path, flags: i32) -> Result {
33 | OpenOptions::new()
34 | .custom_flags(flags)
35 | .read((flags & O_RDONLY != 0) | (flags & O_RDWR != 0))
36 | .open(path)
37 | .map(|file| file.into())
38 | .map_err(|err| err.raw_os_error().unwrap())
39 | }
40 | fn close_restricted(&mut self, fd: OwnedFd) {
41 | drop(File::from(fd));
42 | }
43 | }
44 |
45 | fn main() -> Result<(), zbus::Error> {
46 | // Parse Config
47 | let input_config = config::backend::read_backend_config()
48 | .expect("Failed to parse config file")
49 | .input;
50 |
51 | // Create DBUS server
52 | let connection = task::block_on(DbusServer.init());
53 | let object_server = connection.object_server();
54 | let iface_ref = task::block_on(object_server.interface::<_, DbusServer>(DBUS_PATH))?;
55 |
56 | // Init libinput
57 | let mut input = Libinput::new_with_udev(Interface);
58 | input
59 | .udev_assign_seat("seat0")
60 | .expect("Could not assign seat0");
61 | let fd = input.as_raw_fd();
62 | assert!(fd != -1);
63 | let borrowed_fd = unsafe { BorrowedFd::borrow_raw(input.as_raw_fd()) };
64 | let pollfd = PollFd::new(borrowed_fd, PollFlags::POLLIN);
65 | while poll(&mut [pollfd], None::).is_ok() {
66 | event(&input_config, &mut input, &iface_ref);
67 | }
68 |
69 | Ok(())
70 | }
71 |
72 | fn event(
73 | input_config: &config::backend::InputBackendConfig,
74 | input: &mut Libinput,
75 | iface_ref: &InterfaceRef,
76 | ) {
77 | input.dispatch().unwrap();
78 | for event in input.into_iter() {
79 | if let Event::Keyboard(KeyboardEvent::Key(event)) = event {
80 | if event.key_state() == KeyState::Pressed {
81 | continue;
82 | }
83 | let device = match unsafe { event.device().udev_device() } {
84 | Some(device) => device,
85 | None => continue,
86 | };
87 |
88 | let ev_key = match int_to_ev_key(event.key()) {
89 | // Basic Lock keys
90 | Some(key @ EV_KEY::KEY_CAPSLOCK) |
91 | Some(key @ EV_KEY::KEY_NUMLOCK) |
92 | Some(key @ EV_KEY::KEY_SCROLLLOCK) |
93 | // Display Brightness
94 | Some(key @ EV_KEY::KEY_BRIGHTNESSUP) |
95 | Some(key @ EV_KEY::KEY_BRIGHTNESSDOWN) |
96 | Some(key @ EV_KEY::KEY_BRIGHTNESS_MIN) |
97 | Some(key @ EV_KEY::KEY_BRIGHTNESS_MAX) |
98 | Some(key @ EV_KEY::KEY_BRIGHTNESS_AUTO) |
99 | Some(key @ EV_KEY::KEY_BRIGHTNESS_CYCLE) |
100 | // Keyboard Illumination
101 | Some(key @ EV_KEY::KEY_KBDILLUMUP) |
102 | Some(key @ EV_KEY::KEY_KBDILLUMDOWN) |
103 | Some(key @ EV_KEY::KEY_KBDILLUMTOGGLE) => key,
104 | // Keyboard Layout
105 | Some(key @ EV_KEY::KEY_KBD_LAYOUT_NEXT) => key,
106 | // Audio Keys
107 | Some(key @ EV_KEY::KEY_VOLUMEUP) |
108 | Some(key @ EV_KEY::KEY_VOLUMEDOWN) |
109 | Some(key @ EV_KEY::KEY_MUTE) |
110 | Some(key @ EV_KEY::KEY_UNMUTE) |
111 | Some(key @ EV_KEY::KEY_MICMUTE) => key,
112 | // Touchpad
113 | Some(key @ EV_KEY::KEY_TOUCHPAD_ON) |
114 | Some(key @ EV_KEY::KEY_TOUCHPAD_OFF) |
115 | Some(key @ EV_KEY::KEY_TOUCHPAD_TOGGLE) |
116 | // Media Keys
117 | Some(key @ EV_KEY::KEY_PREVIOUSSONG) |
118 | Some(key @ EV_KEY::KEY_PLAYPAUSE) |
119 | Some(key @ EV_KEY::KEY_PLAY) |
120 | Some(key @ EV_KEY::KEY_PAUSE) |
121 | Some(key @ EV_KEY::KEY_NEXTSONG) => key,
122 | _ => continue,
123 | };
124 |
125 | // Special case because several people have the caps lock key
126 | // bound to escape, so it doesn't affect the caps lock status
127 | if ev_key == EV_KEY::KEY_CAPSLOCK && input_config.ignore_caps_lock_key.unwrap_or(false)
128 | {
129 | continue;
130 | }
131 |
132 | if let Some(path) = device.devnode() {
133 | if let Some(path) = path.to_str() {
134 | let event_info = EventInfo {
135 | device_path: path.to_owned(),
136 | ev_key,
137 | };
138 | task::spawn(call(event_info, iface_ref.clone()));
139 | }
140 | }
141 | }
142 | }
143 | }
144 |
145 | async fn call(event_info: EventInfo, iface_ref: InterfaceRef) {
146 | // Wait for the LED value to change
147 | sleep(Duration::from_millis(50)).await;
148 |
149 | let Ok(device) = evdev_rs::Device::new_from_path(event_info.device_path) else {
150 | return;
151 | };
152 |
153 | let lock_state = match event_info.ev_key {
154 | EV_KEY::KEY_CAPSLOCK => device.event_value(&EventCode::EV_LED(EV_LED::LED_CAPSL)),
155 | EV_KEY::KEY_NUMLOCK => device.event_value(&EventCode::EV_LED(EV_LED::LED_NUML)),
156 | EV_KEY::KEY_SCROLLLOCK => device.event_value(&EventCode::EV_LED(EV_LED::LED_SCROLLL)),
157 | _ => None,
158 | };
159 |
160 | // Send signal
161 | let signal_result = DbusServer::key_pressed(
162 | iface_ref.signal_emitter(),
163 | event_info.ev_key as u16,
164 | lock_state.unwrap_or(-1),
165 | )
166 | .await;
167 |
168 | if let Err(error) = signal_result {
169 | eprintln!("Signal Error: {}", error)
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/meson.build:
--------------------------------------------------------------------------------
1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
2 |
3 | cargo_bin = find_program('cargo')
4 | assert(cargo_bin.found())
5 | cargo_opt = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ]
6 | cargo_opt += [ '--target-dir', meson.project_build_root() / 'src' ]
7 | cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ]
8 |
9 | if get_option('buildtype') == 'release'
10 | cargo_opt += [ '--release' ]
11 | rust_target = 'release'
12 | else
13 | rust_target = 'debug'
14 | endif
15 |
16 | binaries = [
17 | 'swayosd-server',
18 | 'swayosd-client',
19 | 'swayosd-libinput-backend'
20 | ]
21 | binaries_path = []
22 | foreach prog : binaries
23 | binaries_path += '@OUTDIR@/@0@/@1@'.format(rust_target, prog)
24 | endforeach
25 |
26 | custom_target(
27 | 'Cargo Build',
28 | build_by_default: true,
29 | build_always_stale: true,
30 | output: binaries,
31 | console: true,
32 | install: true,
33 | install_dir: join_paths(get_option('prefix'), get_option('bindir')),
34 | command: [
35 | 'env', cargo_env,
36 | cargo_bin, 'build', cargo_opt, '&&',
37 | 'cp', '-f', binaries_path, '@OUTDIR@'
38 | ]
39 | )
40 |
--------------------------------------------------------------------------------
/src/mpris-backend/mod.rs:
--------------------------------------------------------------------------------
1 | use mpris::{Metadata, PlaybackStatus, Player, PlayerFinder};
2 |
3 | use super::config::user::ServerConfig;
4 | use crate::utils::get_player;
5 | use std::{error::Error, sync::Arc, thread::sleep, time::Duration};
6 | use PlaybackStatus::*;
7 | use PlayerctlAction::*;
8 |
9 | pub enum PlayerctlAction {
10 | PlayPause,
11 | Play,
12 | Pause,
13 | Stop,
14 | Next,
15 | Prev,
16 | Shuffle,
17 | }
18 |
19 | #[derive(Clone, Debug)]
20 | pub enum PlayerctlDeviceRaw {
21 | None,
22 | All,
23 | Some(String),
24 | }
25 |
26 | pub enum PlayerctlDevice {
27 | All(Vec),
28 | Some(Player),
29 | }
30 |
31 | pub struct Playerctl {
32 | player: PlayerctlDevice,
33 | action: PlayerctlAction,
34 | pub icon: Option,
35 | pub label: Option,
36 | fmt_str: Option,
37 | }
38 |
39 | impl Playerctl {
40 | pub fn new(
41 | action: PlayerctlAction,
42 | config: Arc,
43 | ) -> Result> {
44 | let playerfinder = PlayerFinder::new()?;
45 | let player = get_player();
46 | let player = match player {
47 | PlayerctlDeviceRaw::None => PlayerctlDevice::Some(playerfinder.find_active()?),
48 | PlayerctlDeviceRaw::Some(name) => {
49 | PlayerctlDevice::Some(playerfinder.find_by_name(name.as_str())?)
50 | }
51 | PlayerctlDeviceRaw::All => PlayerctlDevice::All(playerfinder.find_all()?),
52 | };
53 | let fmt_str = config.playerctl_format.clone();
54 | Ok(Self {
55 | player,
56 | action,
57 | icon: None,
58 | label: None,
59 | fmt_str,
60 | })
61 | }
62 | pub fn run(&mut self) -> Result<(), Box> {
63 | let mut metadata = Err("some errro");
64 | let mut icon = Err("some errro");
65 | match &self.player {
66 | PlayerctlDevice::Some(player) => {
67 | icon = Ok(self.run_single(player)?);
68 | metadata = self.get_metadata(player).or_else(|_| Err(""));
69 | }
70 | PlayerctlDevice::All(players) => {
71 | for player in players {
72 | let icon_new = self.run_single(player);
73 | if let Ok(icon_new) = icon_new {
74 | if icon.is_err() {
75 | icon = Ok(icon_new);
76 | }
77 | };
78 | if let Err(_) = metadata {
79 | metadata = self.get_metadata(player).or_else(|_| Err(""));
80 | }
81 | }
82 | }
83 | };
84 |
85 | self.icon = Some(icon.unwrap_or("").to_string());
86 | let label = if let Ok(metadata) = metadata {
87 | Some(self.fmt_string(metadata))
88 | } else {
89 | None
90 | };
91 | self.label = label;
92 | Ok(())
93 | }
94 | fn run_single(&self, player: &Player) -> Result<&str, Box> {
95 | let out = match self.action {
96 | PlayPause => match player.get_playback_status()? {
97 | Playing => {
98 | player.pause()?;
99 | "pause-large-symbolic"
100 | }
101 | Paused | Stopped => {
102 | player.play()?;
103 | "play-large-symbolic"
104 | }
105 | },
106 | Shuffle => {
107 | let shuffle = player.get_shuffle()?;
108 | player.set_shuffle(!shuffle)?;
109 | if shuffle {
110 | "playlist-consecutive-symbolic"
111 | } else {
112 | "playlist-shuffle-symbolic"
113 | }
114 | }
115 | Play => {
116 | player.play()?;
117 | "play-large-symbolic"
118 | }
119 | Pause => {
120 | player.pause()?;
121 | "pause-large-symbolic"
122 | }
123 | Stop => {
124 | player.stop()?;
125 | "stop-large-symbolic"
126 | }
127 | Next => {
128 | player.next()?;
129 | "media-seek-forward-symbolic"
130 | }
131 | Prev => {
132 | player.previous()?;
133 | "media-seek-backward-symbolic"
134 | }
135 | };
136 | Ok(out)
137 | }
138 | fn get_metadata(&self, player: &Player) -> Result {
139 | match self.action {
140 | Next | Prev => {
141 | if let Ok(track_list) = player.get_track_list() {
142 | if let Some(track) = track_list.get(0) {
143 | return player.get_track_metadata(track);
144 | }
145 | }
146 | let metadata = player.get_metadata()?;
147 | let name1 = metadata.url().unwrap();
148 | let mut counter = 0;
149 | while counter < 20 {
150 | sleep(Duration::from_millis(5));
151 | counter += 1;
152 | let metadata = player.get_metadata()?;
153 | let name2 = metadata.url().unwrap();
154 | if name1 != name2 {
155 | return Ok(metadata);
156 | }
157 | }
158 | Ok(metadata)
159 | }
160 | _ => player.get_metadata(),
161 | }
162 | }
163 | fn fmt_string(&self, metadata: mpris::Metadata) -> String {
164 | use std::collections::HashMap;
165 | use strfmt::Format;
166 |
167 | let mut vars = HashMap::new();
168 | let artists = metadata.artists().unwrap_or(vec![""]);
169 | let artists_album = metadata.album_artists().unwrap_or(vec![""]);
170 | let artist = artists.get(0).map_or("", |v| v);
171 | let artist_album = artists_album.get(0).map_or("", |v| v);
172 |
173 | let title = metadata.title().unwrap_or("");
174 | let album = metadata.album_name().unwrap_or("");
175 | let track_num = metadata
176 | .track_number()
177 | .and_then(|x| Some(x.to_string()))
178 | .unwrap_or(String::new());
179 | let disc_num = metadata
180 | .disc_number()
181 | .and_then(|x| Some(x.to_string()))
182 | .unwrap_or(String::new());
183 | let autorating = metadata
184 | .auto_rating()
185 | .and_then(|x| Some(x.to_string()))
186 | .unwrap_or(String::new());
187 |
188 | vars.insert("artist".to_string(), artist);
189 | vars.insert("albumArtist".to_string(), artist_album);
190 | vars.insert("title".to_string(), title);
191 | vars.insert("album".to_string(), album);
192 | vars.insert("trackNumber".to_string(), &track_num);
193 | vars.insert("discNumber".to_string(), &disc_num);
194 | vars.insert("autoRating".to_string(), &autorating);
195 |
196 | self.fmt_str
197 | .clone()
198 | .unwrap_or("{artist} - {title}".into())
199 | .format(&vars)
200 | .unwrap_or_else(|e| {
201 | eprintln!("error: {}. using default string", e);
202 | "{artist} - {title}".format(&vars).unwrap()
203 | })
204 | }
205 | }
206 |
207 | impl PlayerctlAction {
208 | pub fn from(action: &str) -> Result {
209 | use PlayerctlAction::*;
210 | match action {
211 | "play-pause" => Ok(PlayPause),
212 | "play" => Ok(Play),
213 | "pause" => Ok(Pause),
214 | "stop" => Ok(Stop),
215 | "next" => Ok(Next),
216 | "prev" | "previous" => Ok(Prev),
217 | "shuffle" => Ok(Shuffle),
218 | x => Err(x.to_string()),
219 | }
220 | }
221 | }
222 |
223 | impl PlayerctlDeviceRaw {
224 | pub fn from(player: String) -> Result {
225 | use PlayerctlDeviceRaw::*;
226 | match player.as_str() {
227 | "auto" | "" => Ok(None),
228 | "all" => Ok(All),
229 | _ => Ok(Some(player)),
230 | }
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/src/server/application.rs:
--------------------------------------------------------------------------------
1 | use crate::argtypes::ArgTypes;
2 | use crate::config::{self, APPLICATION_NAME, DBUS_BACKEND_NAME};
3 | use crate::global_utils::{handle_application_args, HandleLocalStatus};
4 | use crate::osd_window::SwayosdWindow;
5 | use crate::playerctl::*;
6 | use crate::utils::{self, *};
7 | use async_channel::Receiver;
8 | use gtk::{
9 | gdk,
10 | gio::{
11 | self, ApplicationFlags, BusNameWatcherFlags, BusType, DBusSignalFlags, SignalSubscriptionId,
12 | },
13 | glib::{
14 | clone, variant::ToVariant, Char, ControlFlow::Break, MainContext, OptionArg, OptionFlags,
15 | },
16 | prelude::*,
17 | Application,
18 | };
19 | use pulsectl::controllers::{SinkController, SourceController};
20 | use std::cell::RefCell;
21 | use std::rc::Rc;
22 | use std::sync::{Arc, Mutex};
23 |
24 | use super::config::user::ServerConfig;
25 |
26 | #[derive(Clone, Shrinkwrap)]
27 | pub struct SwayOSDApplication {
28 | #[shrinkwrap(main_field)]
29 | app: gtk::Application,
30 | windows: Rc>>,
31 | _hold: Rc,
32 | }
33 |
34 | impl SwayOSDApplication {
35 | pub fn new(
36 | server_config: Arc,
37 | action_receiver: Receiver<(ArgTypes, String)>,
38 | ) -> Self {
39 | let app = Application::new(Some(APPLICATION_NAME), ApplicationFlags::FLAGS_NONE);
40 | let hold = Rc::new(app.hold());
41 |
42 | app.add_main_option(
43 | "config",
44 | Char::from(0),
45 | OptionFlags::NONE,
46 | OptionArg::String,
47 | "Use a custom config file instead of looking for one.",
48 | Some(""),
49 | );
50 |
51 | app.add_main_option(
52 | "style",
53 | Char::from('s' as u8),
54 | OptionFlags::NONE,
55 | OptionArg::String,
56 | "Use a custom Stylesheet file instead of looking for one",
57 | Some(""),
58 | );
59 |
60 | app.add_main_option(
61 | "top-margin",
62 | Char::from(0),
63 | OptionFlags::NONE,
64 | OptionArg::String,
65 | &format!(
66 | "OSD margin from top edge (0.5 would be screen center). Default is {}",
67 | *utils::TOP_MARGIN_DEFAULT
68 | ),
69 | Some(""),
70 | );
71 |
72 | let osd_app = SwayOSDApplication {
73 | app: app.clone(),
74 | windows: Rc::new(RefCell::new(Vec::new())),
75 | _hold: hold,
76 | };
77 |
78 | // Apply Server Config
79 | if let Some(margin) = server_config.top_margin {
80 | if (0_f32..1_f32).contains(&margin) {
81 | set_top_margin(margin);
82 | }
83 | }
84 | if let Some(max_volume) = server_config.max_volume {
85 | set_default_max_volume(max_volume);
86 | }
87 | if let Some(show) = server_config.show_percentage {
88 | set_show_percentage(show);
89 | }
90 |
91 | let server_config_shared = server_config.clone();
92 |
93 | // Parse args
94 | app.connect_handle_local_options(clone!(
95 | #[strong]
96 | osd_app,
97 | move |_app, args| {
98 | let actions = match handle_application_args(args.to_variant()) {
99 | (HandleLocalStatus::SUCCESS | HandleLocalStatus::CONTINUE, actions) => actions,
100 | (status @ HandleLocalStatus::FAILURE, _) => return status as i32,
101 | };
102 | for (arg_type, data) in actions {
103 | match (arg_type, data) {
104 | (ArgTypes::TopMargin, margin) => {
105 | let margin: Option = margin
106 | .and_then(|margin| margin.parse().ok())
107 | .and_then(|margin| {
108 | (0_f32..1_f32).contains(&margin).then_some(margin)
109 | });
110 |
111 | if let Some(margin) = margin {
112 | set_top_margin(margin)
113 | }
114 | }
115 | (ArgTypes::MaxVolume, max) => {
116 | let max: Option = max.and_then(|max| max.parse().ok());
117 |
118 | if let Some(max) = max {
119 | set_default_max_volume(max);
120 | }
121 | }
122 | (arg_type, data) => Self::action_activated(
123 | &osd_app,
124 | server_config_shared.clone(),
125 | arg_type,
126 | data,
127 | ),
128 | }
129 | }
130 |
131 | HandleLocalStatus::CONTINUE as i32
132 | }
133 | ));
134 |
135 | let server_config_shared = server_config.clone();
136 |
137 | MainContext::default().spawn_local(clone!(
138 | #[strong]
139 | osd_app,
140 | async move {
141 | while let Ok((arg_type, data)) = action_receiver.recv().await {
142 | Self::action_activated(
143 | &osd_app,
144 | server_config_shared.clone(),
145 | arg_type,
146 | (!data.is_empty()).then_some(data),
147 | );
148 | }
149 | Break
150 | }
151 | ));
152 |
153 | let server_config_shared = server_config.clone();
154 |
155 | // Listen to the LibInput Backend and activate the Application action
156 | let (sender, receiver) = async_channel::bounded::<(u16, i32)>(1);
157 | MainContext::default().spawn_local(clone!(
158 | #[strong]
159 | osd_app,
160 | async move {
161 | while let Ok((key_code, state)) = receiver.recv().await {
162 | let (arg_type, data): (ArgTypes, Option) =
163 | match evdev_rs::enums::int_to_ev_key(key_code as u32) {
164 | Some(evdev_rs::enums::EV_KEY::KEY_CAPSLOCK) => {
165 | (ArgTypes::CapsLock, Some(state.to_string()))
166 | }
167 | Some(evdev_rs::enums::EV_KEY::KEY_NUMLOCK) => {
168 | (ArgTypes::NumLock, Some(state.to_string()))
169 | }
170 | Some(evdev_rs::enums::EV_KEY::KEY_SCROLLLOCK) => {
171 | (ArgTypes::ScrollLock, Some(state.to_string()))
172 | }
173 | _ => continue,
174 | };
175 | Self::action_activated(&osd_app, server_config_shared.clone(), arg_type, data);
176 | }
177 | Break
178 | }
179 | ));
180 | // Start watching for the LibInput Backend
181 | let signal_id: Arc>> = Arc::new(Mutex::new(None));
182 | gio::bus_watch_name(
183 | BusType::System,
184 | DBUS_BACKEND_NAME,
185 | BusNameWatcherFlags::NONE,
186 | clone!(
187 | #[strong]
188 | sender,
189 | #[strong]
190 | signal_id,
191 | move |connection, _, _| {
192 | println!("Connecting to the SwayOSD LibInput Backend");
193 | let mut mutex = match signal_id.lock() {
194 | Ok(mut mutex) => match mutex.as_mut() {
195 | Some(_) => return,
196 | None => mutex,
197 | },
198 | Err(error) => return println!("Mutex lock Error: {}", error),
199 | };
200 | mutex.replace(connection.signal_subscribe(
201 | Some(config::DBUS_BACKEND_NAME),
202 | Some(config::DBUS_BACKEND_NAME),
203 | Some("KeyPressed"),
204 | Some(config::DBUS_PATH),
205 | None,
206 | DBusSignalFlags::NONE,
207 | clone!(
208 | #[strong]
209 | sender,
210 | move |_, _, _, _, _, variant| {
211 | let key_code = variant.try_child_get::(0);
212 | let state = variant.try_child_get::(1);
213 | match (key_code, state) {
214 | (Ok(Some(key_code)), Ok(Some(state))) => {
215 | MainContext::default().spawn_local(clone!(
216 | #[strong]
217 | sender,
218 | async move {
219 | if let Err(error) =
220 | sender.send((key_code, state)).await
221 | {
222 | eprintln!("Channel Send error: {}", error);
223 | }
224 | }
225 | ));
226 | }
227 | variables => {
228 | return eprintln!("Variables don't match: {:?}", variables)
229 | }
230 | };
231 | }
232 | ),
233 | ));
234 | }
235 | ),
236 | clone!(
237 | #[strong]
238 | signal_id,
239 | move |connection, _| {
240 | eprintln!("SwayOSD LibInput Backend isn't available, waiting...");
241 | match signal_id.lock() {
242 | Ok(mut mutex) => {
243 | if let Some(sig_id) = mutex.take() {
244 | connection.signal_unsubscribe(sig_id);
245 | }
246 | }
247 | Err(error) => println!("Mutex lock Error: {}", error),
248 | }
249 | }
250 | ),
251 | );
252 |
253 | return osd_app;
254 | }
255 |
256 | pub fn start(&self) -> i32 {
257 | let s = self.clone();
258 | self.app.connect_activate(move |_| {
259 | s.initialize();
260 | });
261 |
262 | let _ = self.app.register(gio::Cancellable::NONE);
263 | self.app.run().into()
264 | }
265 |
266 | fn choose_windows(osd_app: &SwayOSDApplication) -> Vec {
267 | let mut selected_windows = Vec::new();
268 |
269 | match get_monitor_name() {
270 | Some(monitor_name) => {
271 | for window in osd_app.windows.borrow().to_owned() {
272 | if let Some(monitor_connector) = window.monitor.connector() {
273 | if monitor_name == monitor_connector {
274 | selected_windows.push(window);
275 | }
276 | }
277 | }
278 | }
279 | None => return osd_app.windows.borrow().to_owned(),
280 | }
281 |
282 | if selected_windows.is_empty() {
283 | eprintln!("Specified monitor name, but found no matching output");
284 | return osd_app.windows.borrow().to_owned();
285 | } else {
286 | return selected_windows;
287 | }
288 | }
289 |
290 | fn action_activated(
291 | osd_app: &SwayOSDApplication,
292 | server_config: Arc,
293 | arg_type: ArgTypes,
294 | value: Option,
295 | ) {
296 | match (arg_type, value) {
297 | (ArgTypes::SinkVolumeRaise, step) => {
298 | let mut device_type = VolumeDeviceType::Sink(SinkController::create().unwrap());
299 | if let Some(device) =
300 | change_device_volume(&mut device_type, VolumeChangeType::Raise, step)
301 | {
302 | for window in Self::choose_windows(osd_app) {
303 | window.changed_volume(&device, &device_type);
304 | }
305 | }
306 | reset_max_volume();
307 | reset_device_name();
308 | reset_monitor_name();
309 | }
310 | (ArgTypes::SinkVolumeLower, step) => {
311 | let mut device_type = VolumeDeviceType::Sink(SinkController::create().unwrap());
312 | if let Some(device) =
313 | change_device_volume(&mut device_type, VolumeChangeType::Lower, step)
314 | {
315 | for window in Self::choose_windows(osd_app) {
316 | window.changed_volume(&device, &device_type);
317 | }
318 | }
319 | reset_max_volume();
320 | reset_device_name();
321 | reset_monitor_name();
322 | }
323 | (ArgTypes::SinkVolumeMuteToggle, _) => {
324 | let mut device_type = VolumeDeviceType::Sink(SinkController::create().unwrap());
325 | if let Some(device) =
326 | change_device_volume(&mut device_type, VolumeChangeType::MuteToggle, None)
327 | {
328 | for window in Self::choose_windows(osd_app) {
329 | window.changed_volume(&device, &device_type);
330 | }
331 | }
332 | reset_max_volume();
333 | reset_device_name();
334 | reset_monitor_name();
335 | }
336 | (ArgTypes::SourceVolumeRaise, step) => {
337 | let mut device_type = VolumeDeviceType::Source(SourceController::create().unwrap());
338 | if let Some(device) =
339 | change_device_volume(&mut device_type, VolumeChangeType::Raise, step)
340 | {
341 | for window in Self::choose_windows(osd_app) {
342 | window.changed_volume(&device, &device_type);
343 | }
344 | }
345 | reset_max_volume();
346 | reset_device_name();
347 | reset_monitor_name();
348 | }
349 | (ArgTypes::SourceVolumeLower, step) => {
350 | let mut device_type = VolumeDeviceType::Source(SourceController::create().unwrap());
351 | if let Some(device) =
352 | change_device_volume(&mut device_type, VolumeChangeType::Lower, step)
353 | {
354 | for window in Self::choose_windows(osd_app) {
355 | window.changed_volume(&device, &device_type);
356 | }
357 | }
358 | reset_max_volume();
359 | reset_device_name();
360 | reset_monitor_name();
361 | }
362 | (ArgTypes::SourceVolumeMuteToggle, _) => {
363 | let mut device_type = VolumeDeviceType::Source(SourceController::create().unwrap());
364 | if let Some(device) =
365 | change_device_volume(&mut device_type, VolumeChangeType::MuteToggle, None)
366 | {
367 | for window in Self::choose_windows(osd_app) {
368 | window.changed_volume(&device, &device_type);
369 | }
370 | }
371 | reset_max_volume();
372 | reset_device_name();
373 | reset_monitor_name();
374 | }
375 | // TODO: Brightness
376 | (ArgTypes::BrightnessRaise, step) => {
377 | if let Ok(mut brightness_backend) =
378 | change_brightness(BrightnessChangeType::Raise, step)
379 | {
380 | for window in Self::choose_windows(osd_app) {
381 | window.changed_brightness(brightness_backend.as_mut());
382 | }
383 | }
384 | reset_monitor_name();
385 | }
386 | (ArgTypes::BrightnessLower, step) => {
387 | if let Ok(mut brightness_backend) =
388 | change_brightness(BrightnessChangeType::Lower, step)
389 | {
390 | for window in Self::choose_windows(osd_app) {
391 | window.changed_brightness(brightness_backend.as_mut());
392 | }
393 | }
394 | reset_monitor_name();
395 | }
396 | (ArgTypes::BrightnessSet, value) => {
397 | if let Ok(mut brightness_backend) =
398 | change_brightness(BrightnessChangeType::Set, value)
399 | {
400 | for window in Self::choose_windows(osd_app) {
401 | window.changed_brightness(brightness_backend.as_mut());
402 | }
403 | }
404 | reset_monitor_name();
405 | }
406 | (ArgTypes::CapsLock, value) => {
407 | let i32_value = value.clone().unwrap_or("-1".to_owned());
408 | let state = match i32_value.parse::() {
409 | Ok(value) if value >= 0 && value <= 1 => value == 1,
410 | _ => get_key_lock_state(KeysLocks::CapsLock, value),
411 | };
412 | for window in Self::choose_windows(osd_app) {
413 | window.changed_keylock(KeysLocks::CapsLock, state)
414 | }
415 | reset_monitor_name();
416 | }
417 | (ArgTypes::NumLock, value) => {
418 | let i32_value = value.clone().unwrap_or("-1".to_owned());
419 | let state = match i32_value.parse::() {
420 | Ok(value) if value >= 0 && value <= 1 => value == 1,
421 | _ => get_key_lock_state(KeysLocks::NumLock, value),
422 | };
423 | for window in Self::choose_windows(osd_app) {
424 | window.changed_keylock(KeysLocks::NumLock, state)
425 | }
426 | reset_monitor_name();
427 | }
428 | (ArgTypes::ScrollLock, value) => {
429 | let i32_value = value.clone().unwrap_or("-1".to_owned());
430 | let state = match i32_value.parse::() {
431 | Ok(value) if value >= 0 && value <= 1 => value == 1,
432 | _ => get_key_lock_state(KeysLocks::ScrollLock, value),
433 | };
434 | for window in Self::choose_windows(osd_app) {
435 | window.changed_keylock(KeysLocks::ScrollLock, state)
436 | }
437 | reset_monitor_name();
438 | }
439 | (ArgTypes::MaxVolume, max) => {
440 | let volume: u8 = match max {
441 | Some(max) => match max.parse() {
442 | Ok(max) => max,
443 | _ => get_default_max_volume(),
444 | },
445 | _ => get_default_max_volume(),
446 | };
447 | set_max_volume(volume)
448 | }
449 | (ArgTypes::Player, name) => set_player(name.unwrap_or("".to_string())),
450 | (ArgTypes::Playerctl, value) => {
451 | let value = &value.unwrap_or("".to_string());
452 |
453 | let action = PlayerctlAction::from(value).unwrap();
454 | if let Ok(mut player) = Playerctl::new(action, server_config) {
455 | match player.run() {
456 | Ok(_) => {
457 | let (icon, label) = (player.icon.unwrap(), player.label.unwrap());
458 | for window in Self::choose_windows(osd_app) {
459 | window.changed_player(&icon, &label)
460 | }
461 | reset_monitor_name();
462 | }
463 | Err(x) => {
464 | eprintln!("couldn't run player change: \"{:?}\"!", x)
465 | }
466 | }
467 | } else {
468 | eprintln!("Unable to get players! are any opened?")
469 | }
470 |
471 | reset_player();
472 | }
473 | (ArgTypes::DeviceName, name) => {
474 | set_device_name(name.unwrap_or(DEVICE_NAME_DEFAULT.to_string()))
475 | }
476 | (ArgTypes::MonitorName, name) => {
477 | if let Some(name) = name {
478 | set_monitor_name(name)
479 | }
480 | }
481 | (ArgTypes::CustomMessage, message) => {
482 | if let Some(message) = message {
483 | for window in Self::choose_windows(osd_app) {
484 | window.custom_message(message.as_str(), get_icon_name().as_deref());
485 | }
486 | }
487 | reset_icon_name();
488 | reset_monitor_name();
489 | }
490 | (ArgTypes::CustomIcon, icon) => {
491 | set_icon_name(icon.unwrap_or(ICON_NAME_DEFAULT.to_string()))
492 | }
493 | (arg_type, data) => {
494 | eprintln!(
495 | "Failed to parse command... Type: {:?}, Data: {:?}",
496 | arg_type, data
497 | )
498 | }
499 | };
500 | }
501 |
502 | fn initialize(&self) {
503 | let display: gdk::Display = match gdk::Display::default() {
504 | Some(x) => x,
505 | _ => return,
506 | };
507 |
508 | self.init_windows(&display);
509 |
510 | let _self = self;
511 |
512 | display.connect_opened(clone!(
513 | #[strong]
514 | _self,
515 | move |d| {
516 | _self.init_windows(d);
517 | }
518 | ));
519 |
520 | display.connect_closed(clone!(
521 | #[strong]
522 | _self,
523 | move |_d, is_error| {
524 | if is_error {
525 | eprintln!("Display closed due to errors...");
526 | }
527 | _self.close_all_windows();
528 | }
529 | ));
530 |
531 | display.monitors().connect_items_changed(clone!(
532 | #[strong]
533 | _self,
534 | move |monitors, position, removed, added| {
535 | if removed != 0 {
536 | _self.init_windows(&display);
537 | } else if added != 0 {
538 | for i in 0..added {
539 | if let Some(mon) = monitors
540 | .item(position + i)
541 | .and_then(|obj| obj.downcast::().ok())
542 | {
543 | _self.add_window(&display, &mon);
544 | }
545 | }
546 | }
547 | }
548 | ));
549 | }
550 |
551 | fn add_window(&self, display: &gdk::Display, monitor: &gdk::Monitor) {
552 | let win = SwayosdWindow::new(&self.app, display, monitor);
553 | self.windows.borrow_mut().push(win);
554 | }
555 |
556 | fn init_windows(&self, display: &gdk::Display) {
557 | self.close_all_windows();
558 |
559 | let monitors = display.monitors();
560 | for i in 0..monitors.n_items() {
561 | let monitor = match monitors
562 | .item(i)
563 | .and_then(|obj| obj.downcast::().ok())
564 | {
565 | Some(x) => x,
566 | _ => continue,
567 | };
568 | self.add_window(display, &monitor);
569 | }
570 | }
571 |
572 | fn close_all_windows(&self) {
573 | self.windows.borrow_mut().retain(|window| {
574 | window.close();
575 | false
576 | });
577 | }
578 | }
579 |
--------------------------------------------------------------------------------
/src/server/main.rs:
--------------------------------------------------------------------------------
1 | mod application;
2 | mod osd_window;
3 | mod utils;
4 |
5 | #[path = "../argtypes.rs"]
6 | mod argtypes;
7 | #[path = "../config.rs"]
8 | mod config;
9 | #[path = "../global_utils.rs"]
10 | mod global_utils;
11 |
12 | #[path = "../brightness_backend/mod.rs"]
13 | mod brightness_backend;
14 |
15 | #[path = "../mpris-backend/mod.rs"]
16 | mod playerctl;
17 |
18 | #[macro_use]
19 | extern crate shrinkwraprs;
20 |
21 | #[macro_use]
22 | extern crate cascade;
23 |
24 | use application::SwayOSDApplication;
25 | use argtypes::ArgTypes;
26 | use async_channel::Sender;
27 | use config::{DBUS_PATH, DBUS_SERVER_NAME};
28 | use gtk::{
29 | gdk::Display,
30 | gio::{self, Resource},
31 | glib::Bytes,
32 | CssProvider, IconTheme,
33 | };
34 | use std::{env::args_os, future::pending, path::PathBuf, str::FromStr, sync::Arc};
35 | use utils::{get_system_css_path, user_style_path};
36 | use zbus::{connection, interface};
37 |
38 | struct DbusServer {
39 | sender: Sender<(ArgTypes, String)>,
40 | }
41 |
42 | #[interface(name = "org.erikreider.swayosd")]
43 | impl DbusServer {
44 | pub async fn handle_action(&self, arg_type: String, data: String) -> bool {
45 | let arg_type = match ArgTypes::from_str(&arg_type) {
46 | Ok(arg_type) => arg_type,
47 | Err(other_type) => {
48 | eprintln!("Unknown action in Dbus handle_action: {:?}", other_type);
49 | return false;
50 | }
51 | };
52 | if let Err(error) = self.sender.send((arg_type, data)).await {
53 | eprintln!("Channel Send error: {}", error);
54 | return false;
55 | }
56 | true
57 | }
58 | }
59 |
60 | impl DbusServer {
61 | async fn new(sender: Sender<(ArgTypes, String)>) -> zbus::Result<()> {
62 | let _connection = connection::Builder::session()?
63 | .name(DBUS_SERVER_NAME)?
64 | .serve_at(DBUS_PATH, DbusServer { sender })?
65 | .build()
66 | .await?;
67 | pending::<()>().await;
68 | Ok(())
69 | }
70 | }
71 |
72 | const GRESOURCE_BASE_PATH: &str = "/org/erikreider/swayosd";
73 |
74 | fn main() {
75 | if gtk::init().is_err() {
76 | eprintln!("failed to initialize GTK Application");
77 | std::process::exit(1);
78 | }
79 |
80 | // Load the compiled resource bundle
81 | let resources_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/swayosd.gresource"));
82 | let resource_data = Bytes::from(&resources_bytes[..]);
83 | let res = Resource::from_data(&resource_data).unwrap();
84 | gio::resources_register(&res);
85 |
86 | // Load the icon theme
87 | let theme = IconTheme::default();
88 | theme.add_resource_path(&format!("{}/icons", GRESOURCE_BASE_PATH));
89 |
90 | // Load the CSS themes
91 | let display = Display::default().expect("Failed getting the default screen");
92 |
93 | // Load the provided default CSS theme
94 | let provider = CssProvider::new();
95 | provider.connect_parsing_error(|_provider, _section, error| {
96 | eprintln!("Could not load default CSS stylesheet: {}", error);
97 | });
98 | match get_system_css_path() {
99 | Some(path) => {
100 | provider.load_from_path(path.to_str().unwrap());
101 | gtk::style_context_add_provider_for_display(
102 | &display,
103 | &provider,
104 | gtk::STYLE_PROVIDER_PRIORITY_APPLICATION as u32,
105 | );
106 | }
107 | None => eprintln!("Could not find the system CSS file..."),
108 | }
109 |
110 | // Get config path and CSS theme path from command line
111 | let mut config_path: Option = None;
112 | let mut custom_user_css: Option = None;
113 | let mut args = args_os().into_iter();
114 | while let Some(arg) = args.next() {
115 | match arg.to_str() {
116 | Some("--config") => {
117 | if let Some(path) = args.next() {
118 | config_path = Some(path.into());
119 | }
120 | }
121 | Some("-s") | Some("--style") => {
122 | if let Some(path) = args.next() {
123 | custom_user_css = Some(path.into());
124 | }
125 | }
126 | _ => (),
127 | }
128 | }
129 |
130 | // Parse Config
131 | let server_config = Arc::new(
132 | config::user::read_user_config(config_path.as_deref())
133 | .expect("Failed to parse config file")
134 | .server,
135 | );
136 |
137 | // Load style path from config if none is given on CLI
138 | if custom_user_css.is_none() {
139 | custom_user_css = server_config.style.clone();
140 | }
141 |
142 | // Try loading the users CSS theme
143 | if let Some(user_config_path) = user_style_path(custom_user_css) {
144 | let user_provider = CssProvider::new();
145 | user_provider.connect_parsing_error(|_provider, _section, error| {
146 | eprintln!("Failed loading user defined style.css: {}", error);
147 | });
148 | user_provider.load_from_path(&user_config_path);
149 | gtk::style_context_add_provider_for_display(
150 | &display,
151 | &user_provider,
152 | gtk::STYLE_PROVIDER_PRIORITY_APPLICATION as u32,
153 | );
154 | println!("Loaded user defined CSS file");
155 | }
156 |
157 | let (sender, receiver) = async_channel::bounded::<(ArgTypes, String)>(1);
158 | // Start the DBus Server
159 | async_std::task::spawn(DbusServer::new(sender));
160 | // Start the GTK Application
161 | std::process::exit(SwayOSDApplication::new(server_config, receiver).start());
162 | }
163 |
--------------------------------------------------------------------------------
/src/server/osd_window.rs:
--------------------------------------------------------------------------------
1 | use std::cell::RefCell;
2 | use std::rc::Rc;
3 | use std::time::Duration;
4 |
5 | use gtk::{
6 | gdk,
7 | glib::{self, clone},
8 | prelude::*,
9 | };
10 | use pulsectl::controllers::types::DeviceInfo;
11 |
12 | use crate::{
13 | brightness_backend::BrightnessBackend,
14 | utils::{
15 | get_max_volume, get_show_percentage, get_top_margin, volume_to_f64, KeysLocks,
16 | VolumeDeviceType,
17 | },
18 | };
19 |
20 | use gtk_layer_shell::LayerShell;
21 |
22 | const ICON_SIZE: i32 = 32;
23 |
24 | /// A window that our application can open that contains the main project view.
25 | #[derive(Clone, Debug)]
26 | pub struct SwayosdWindow {
27 | pub window: gtk::ApplicationWindow,
28 | pub display: gdk::Display,
29 | pub monitor: gdk::Monitor,
30 | container: gtk::Box,
31 | timeout_id: Rc>>,
32 | }
33 |
34 | impl SwayosdWindow {
35 | /// Create a new window and assign it to the given application.
36 | pub fn new(app: >k::Application, display: &gdk::Display, monitor: &gdk::Monitor) -> Self {
37 | let window = gtk::ApplicationWindow::new(app);
38 | window.set_widget_name("osd");
39 | window.add_css_class("osd");
40 |
41 | window.init_layer_shell();
42 | window.set_monitor(monitor);
43 | window.set_namespace("swayosd");
44 |
45 | window.set_exclusive_zone(-1);
46 | window.set_layer(gtk_layer_shell::Layer::Overlay);
47 | window.set_anchor(gtk_layer_shell::Edge::Top, true);
48 |
49 | // Set up the widgets
50 | window.set_width_request(250);
51 |
52 | let container = cascade! {
53 | gtk::Box::new(gtk::Orientation::Horizontal, 12);
54 | ..set_widget_name("container");
55 | };
56 |
57 | window.set_child(Some(&container));
58 |
59 | // Disable mouse input
60 | window.connect_map(|window| {
61 | if let Some(surface) = window.surface() {
62 | let region = gtk::cairo::Region::create();
63 | surface.set_input_region(®ion);
64 | }
65 | });
66 |
67 | let update_margins = |window: >k::ApplicationWindow, monitor: &gdk::Monitor| {
68 | // Monitor scale factor is not always correct
69 | // Transform monitor height into coordinate system of window
70 | let mon_height =
71 | monitor.geometry().height() * monitor.scale_factor() / window.scale_factor();
72 | // Calculate new margin
73 | let bottom = mon_height - window.allocated_height();
74 | let margin = (bottom as f32 * get_top_margin()).round() as i32;
75 | window.set_margin(gtk_layer_shell::Edge::Top, margin);
76 | };
77 |
78 | // Set the window margin
79 | update_margins(&window, monitor);
80 | // Ensure window margin is updated when necessary
81 | window.connect_scale_factor_notify(clone!(
82 | #[weak]
83 | monitor,
84 | move |window| update_margins(window, &monitor)
85 | ));
86 | monitor.connect_scale_factor_notify(clone!(
87 | #[weak]
88 | window,
89 | move |monitor| update_margins(&window, monitor)
90 | ));
91 | monitor.connect_geometry_notify(clone!(
92 | #[weak]
93 | window,
94 | move |monitor| update_margins(&window, monitor)
95 | ));
96 |
97 | Self {
98 | window,
99 | container,
100 | display: display.clone(),
101 | monitor: monitor.clone(),
102 | timeout_id: Rc::new(RefCell::new(None)),
103 | }
104 | }
105 |
106 | pub fn close(&self) {
107 | self.window.close();
108 | }
109 |
110 | pub fn changed_volume(&self, device: &DeviceInfo, device_type: &VolumeDeviceType) {
111 | self.clear_osd();
112 |
113 | let volume = volume_to_f64(&device.volume.avg());
114 | let icon_prefix = match device_type {
115 | VolumeDeviceType::Sink(_) => "sink",
116 | VolumeDeviceType::Source(_) => "source",
117 | };
118 | let icon_state = &match (device.mute, volume) {
119 | (true, _) => "muted",
120 | (_, x) if x == 0.0 => "muted",
121 | (false, x) if x > 0.0 && x <= 33.0 => "low",
122 | (false, x) if x > 33.0 && x <= 66.0 => "medium",
123 | (false, x) if x > 66.0 && x <= 100.0 => "high",
124 | (false, x) if x > 100.0 => match device_type {
125 | VolumeDeviceType::Sink(_) => "high",
126 | VolumeDeviceType::Source(_) => "overamplified",
127 | },
128 | (_, _) => "high",
129 | };
130 | let icon_name = &format!("{}-volume-{}-symbolic", icon_prefix, icon_state);
131 |
132 | let max_volume: f64 = get_max_volume().into();
133 |
134 | let icon = self.build_icon_widget(icon_name);
135 | let progress = self.build_progress_widget(volume / max_volume);
136 | let label = self.build_text_widget(Some(&format!("{}%", volume)));
137 |
138 | progress.set_sensitive(!device.mute);
139 |
140 | self.container.append(&icon);
141 | self.container.append(&progress);
142 | if get_show_percentage() {
143 | self.container.append(&label);
144 | }
145 |
146 | self.run_timeout();
147 | }
148 |
149 | pub fn changed_brightness(&self, brightness_backend: &mut dyn BrightnessBackend) {
150 | self.clear_osd();
151 |
152 | let icon_name = "display-brightness-symbolic";
153 | let icon = self.build_icon_widget(icon_name);
154 |
155 | let brightness = brightness_backend.get_current() as f64;
156 | let max = brightness_backend.get_max() as f64;
157 | let progress = self.build_progress_widget(brightness / max);
158 | let label = self.build_text_widget(Some(&format!("{}%", (brightness / max * 100.) as i32)));
159 |
160 | self.container.append(&icon);
161 | self.container.append(&progress);
162 | if get_show_percentage() {
163 | self.container.append(&label);
164 | }
165 |
166 | self.run_timeout();
167 | }
168 |
169 | pub fn changed_player(&self, icon: &str, label: &str) {
170 | self.clear_osd();
171 |
172 | let icon = self.build_icon_widget(&icon);
173 | let label = self.build_text_widget(Some(&label));
174 |
175 | self.container.append(&icon);
176 | self.container.append(&label);
177 |
178 | self.run_timeout();
179 | }
180 |
181 | pub fn changed_keylock(&self, key: KeysLocks, state: bool) {
182 | self.clear_osd();
183 |
184 | let label = self.build_text_widget(None);
185 |
186 | let on_off_text = match state {
187 | true => "On",
188 | false => "Off",
189 | };
190 |
191 | let (label_text, symbol) = match key {
192 | KeysLocks::CapsLock => {
193 | let symbol = "caps-lock-symbolic";
194 | let text = "Caps Lock ".to_string() + on_off_text;
195 | (text, symbol)
196 | }
197 | KeysLocks::NumLock => {
198 | let symbol = "num-lock-symbolic";
199 | let text = "Num Lock ".to_string() + on_off_text;
200 | (text, symbol)
201 | }
202 | KeysLocks::ScrollLock => {
203 | let symbol = "scroll-lock-symbolic";
204 | let text = "Scroll Lock ".to_string() + on_off_text;
205 | (text, symbol)
206 | }
207 | };
208 |
209 | label.set_text(&label_text);
210 | let icon = self.build_icon_widget(symbol);
211 |
212 | icon.set_sensitive(state);
213 |
214 | self.container.append(&icon);
215 | self.container.append(&label);
216 |
217 | self.run_timeout();
218 | }
219 |
220 | pub fn custom_message(&self, message: &str, icon_name: Option<&str>) {
221 | self.clear_osd();
222 |
223 | let label = self.build_text_widget(Some(message));
224 |
225 | if let Some(icon_name) = icon_name {
226 | let icon = self.build_icon_widget(icon_name);
227 | self.container.append(&icon);
228 | self.container.append(&label);
229 | let box_spacing = self.container.spacing();
230 | icon.connect_realize(move |icon| {
231 | label.set_margin_end(
232 | icon.allocation().width()
233 | + icon.margin_start()
234 | + icon.margin_end()
235 | + box_spacing,
236 | );
237 | });
238 | } else {
239 | self.container.append(&label);
240 | }
241 |
242 | self.run_timeout();
243 | }
244 |
245 | /// Clear all container children
246 | fn clear_osd(&self) {
247 | let mut next = self.container.first_child();
248 | while let Some(widget) = next {
249 | next = widget.next_sibling();
250 | self.container.remove(&widget);
251 | }
252 | }
253 |
254 | fn run_timeout(&self) {
255 | // Hide window after timeout
256 | if let Some(timeout_id) = self.timeout_id.take() {
257 | timeout_id.remove()
258 | }
259 | let s = self.clone();
260 | self.timeout_id.replace(Some(glib::timeout_add_local_once(
261 | Duration::from_millis(1000),
262 | move || {
263 | s.window.hide();
264 | s.timeout_id.replace(None);
265 | },
266 | )));
267 |
268 | self.window.show();
269 | }
270 |
271 | fn build_icon_widget(&self, icon_name: &str) -> gtk::Image {
272 | let icon = gtk::gio::ThemedIcon::from_names(&[icon_name, "missing-symbolic"]);
273 |
274 | cascade! {
275 | gtk::Image::from_gicon(&icon.upcast::());
276 | ..set_pixel_size(ICON_SIZE);
277 | }
278 | }
279 |
280 | fn build_text_widget(&self, text: Option<&str>) -> gtk::Label {
281 | cascade! {
282 | gtk::Label::new(text);
283 | ..set_halign(gtk::Align::Center);
284 | ..set_hexpand(true);
285 | ..add_css_class("title-4");
286 | }
287 | }
288 |
289 | fn build_progress_widget(&self, fraction: f64) -> gtk::ProgressBar {
290 | cascade! {
291 | gtk::ProgressBar::new();
292 | ..set_fraction(fraction);
293 | ..set_valign(gtk::Align::Center);
294 | ..set_hexpand(true);
295 | }
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/src/server/utils.rs:
--------------------------------------------------------------------------------
1 | use gtk::glib::{system_config_dirs, user_config_dir};
2 | use lazy_static::lazy_static;
3 | use substring::Substring;
4 |
5 | use std::{
6 | fs::{self, File},
7 | io::{prelude::*, BufReader},
8 | path::{Path, PathBuf},
9 | sync::Mutex,
10 | };
11 |
12 | use pulse::volume::Volume;
13 | use pulsectl::controllers::{types::DeviceInfo, DeviceControl, SinkController, SourceController};
14 |
15 | use crate::brightness_backend;
16 | use crate::playerctl::PlayerctlDeviceRaw;
17 |
18 | static PRIV_MAX_VOLUME_DEFAULT: u8 = 100_u8;
19 |
20 | lazy_static! {
21 | static ref MAX_VOLUME_DEFAULT: Mutex = Mutex::new(PRIV_MAX_VOLUME_DEFAULT);
22 | static ref MAX_VOLUME: Mutex = Mutex::new(PRIV_MAX_VOLUME_DEFAULT);
23 | pub static ref DEVICE_NAME_DEFAULT: &'static str = "default";
24 | static ref DEVICE_NAME: Mutex