Initial commit
This commit is contained in:
commit
3eedfa1eb5
11 changed files with 2453 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1389
Cargo.lock
generated
Normal file
1389
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "multibg-sway"
|
||||
version = "0.1.0"
|
||||
authors = ["Gergő Sályi <salyigergo94@gmail.com>"]
|
||||
edition = "2021"
|
||||
description = "Set a different wallpaper for the background of each Sway workspace"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["wallpaper", "background", "desktop", "wayland", "sway"]
|
||||
categories = ["command-line-utilities", "multimedia::images"]
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.2.1", features = ["derive"] }
|
||||
env_logger = "0.10.0"
|
||||
image = "0.24.6"
|
||||
log = "0.4.17"
|
||||
mio = { version = "0.8.6", features = ["os-ext", "os-poll"] }
|
||||
swayipc = "3.0.1"
|
||||
|
||||
[dependencies.smithay-client-toolkit]
|
||||
git = "https://github.com/Smithay/client-toolkit.git"
|
||||
rev = "389a4f21872a99a3ba346cc3dabd55c4079ec191" # master branch as of 2023-04-07
|
177
LICENSE-APACHE
Normal file
177
LICENSE-APACHE
Normal file
|
@ -0,0 +1,177 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
19
LICENSE-MIT
Normal file
19
LICENSE-MIT
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright 2023 Gergő Sályi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the “Software”), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
4
README.md
Normal file
4
README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
# multibg-sway
|
||||
|
||||
Set a different wallpaper for the background of each Sway workspace
|
||||
|
8
src/cli.rs
Normal file
8
src/cli.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
/// directory with: wallpaper_dir/output/workspace_name.{jpg|png|...}
|
||||
pub wallpaper_dir: String,
|
||||
}
|
147
src/image.rs
Normal file
147
src/image.rs
Normal file
|
@ -0,0 +1,147 @@
|
|||
use std::{
|
||||
fs::read_dir,
|
||||
path::Path
|
||||
};
|
||||
|
||||
use log::error;
|
||||
use image::{ImageBuffer, Rgb, ImageError};
|
||||
use smithay_client_toolkit::shm::slot::{Buffer, SlotPool};
|
||||
use smithay_client_toolkit::reexports::client::protocol::wl_shm;
|
||||
|
||||
use crate::wayland::WorkspaceBackground;
|
||||
|
||||
pub fn workspace_bgs_from_output_image_dir(
|
||||
dir_path: impl AsRef<Path>,
|
||||
slot_pool: &mut SlotPool,
|
||||
format: wl_shm::Format,
|
||||
)
|
||||
-> Result<Vec<WorkspaceBackground>, String>
|
||||
{
|
||||
let mut buffers = Vec::new();
|
||||
|
||||
let dir = read_dir(&dir_path)
|
||||
.map_err(|e| format!("Failed to open directory: {}", e))?;
|
||||
|
||||
for entry_result in dir {
|
||||
|
||||
let entry = match entry_result {
|
||||
Ok(entry) => entry,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Skipping a directory entry in '{:?}' due to an error: {}",
|
||||
dir_path.as_ref(), e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Resolve symlinks
|
||||
let path = match entry.path().canonicalize() {
|
||||
Ok(file_type) => file_type,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Skipping '{:?}' in '{:?}' due to an error: {}",
|
||||
entry.path(), dir_path.as_ref(), e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Skip dirs
|
||||
if path.is_dir() { continue }
|
||||
|
||||
// Use the file stem as the name of the workspace for this wallpaper
|
||||
let workspace_name = path.file_stem().unwrap()
|
||||
.to_string_lossy().into_owned();
|
||||
|
||||
let raw_image = match image::io::Reader::open(&path)
|
||||
.map_err(ImageError::IoError)
|
||||
.and_then(|r| r.with_guessed_format()
|
||||
.map_err(ImageError::IoError)
|
||||
)
|
||||
.and_then(|r| r.decode())
|
||||
{
|
||||
Ok(raw_image) => raw_image,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to open image '{:?}': {}",
|
||||
path, e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let image = raw_image
|
||||
// It is possible to adjust the contrast and brightness here
|
||||
// .adjust_contrast(-25.0)
|
||||
// .brighten(-60)
|
||||
.into_rgb8();
|
||||
|
||||
let buffer = match format {
|
||||
wl_shm::Format::Xrgb8888 =>
|
||||
buffer_xrgb8888_from_image(image, slot_pool),
|
||||
wl_shm::Format::Bgr888 =>
|
||||
buffer_bgr888_from_image(image, slot_pool),
|
||||
_ => unreachable!()
|
||||
};
|
||||
|
||||
buffers.push(WorkspaceBackground { workspace_name, buffer });
|
||||
}
|
||||
|
||||
if buffers.is_empty() {
|
||||
Err("Found 0 suitable images in the directory".to_string())
|
||||
}
|
||||
else {
|
||||
Ok(buffers)
|
||||
}
|
||||
}
|
||||
|
||||
fn buffer_xrgb8888_from_image(
|
||||
image: ImageBuffer<Rgb<u8>, Vec<u8>>,
|
||||
slot_pool: &mut SlotPool,
|
||||
)
|
||||
-> Buffer
|
||||
{
|
||||
let (buffer, canvas) = slot_pool
|
||||
.create_buffer(
|
||||
image.width() as i32,
|
||||
image.height() as i32,
|
||||
image.width() as i32 * 4,
|
||||
wl_shm::Format::Xrgb8888
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let canvas_len = image.len() / 3 * 4;
|
||||
|
||||
let image_pixels = image.pixels();
|
||||
let canvas_pixels = canvas[..canvas_len].chunks_exact_mut(4);
|
||||
|
||||
for (image_pixel, canvas_pixel) in image_pixels.zip(canvas_pixels) {
|
||||
canvas_pixel[0] = image_pixel.0[2];
|
||||
canvas_pixel[1] = image_pixel.0[1];
|
||||
canvas_pixel[2] = image_pixel.0[0];
|
||||
}
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
fn buffer_bgr888_from_image(
|
||||
image: ImageBuffer<Rgb<u8>, Vec<u8>>,
|
||||
slot_pool: &mut SlotPool,
|
||||
)
|
||||
-> Buffer
|
||||
{
|
||||
let (buffer, canvas) = slot_pool
|
||||
.create_buffer(
|
||||
image.width() as i32,
|
||||
image.height() as i32,
|
||||
image.width() as i32 * 3,
|
||||
wl_shm::Format::Bgr888
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
canvas[..image.len()].copy_from_slice(&image);
|
||||
|
||||
buffer
|
||||
}
|
||||
|
182
src/main.rs
Normal file
182
src/main.rs
Normal file
|
@ -0,0 +1,182 @@
|
|||
mod cli;
|
||||
mod image;
|
||||
mod sway;
|
||||
mod wayland;
|
||||
|
||||
use std::{
|
||||
io,
|
||||
os::fd::AsRawFd,
|
||||
path::Path,
|
||||
sync::{
|
||||
Arc,
|
||||
mpsc::{channel, Receiver},
|
||||
}
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use log::error;
|
||||
use mio::{
|
||||
Events, Interest, Poll, Token, Waker,
|
||||
unix::SourceFd,
|
||||
};
|
||||
use smithay_client_toolkit::{
|
||||
compositor::CompositorState,
|
||||
output::OutputState,
|
||||
registry::RegistryState,
|
||||
shell::wlr_layer::LayerShell,
|
||||
shm::{Shm, slot::SlotPool},
|
||||
};
|
||||
use smithay_client_toolkit::reexports::client::{
|
||||
Connection, EventQueue,
|
||||
backend::{ReadEventsGuard, WaylandError},
|
||||
globals::registry_queue_init,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cli::Cli,
|
||||
sway::{SwayConnectionTask, WorkspaceVisible},
|
||||
wayland::State,
|
||||
};
|
||||
|
||||
fn main()
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
env_logger::Builder::from_env(
|
||||
env_logger::Env::default().default_filter_or(
|
||||
"warn,multibg_sway=trace"
|
||||
)
|
||||
).init();
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
env_logger::Builder::from_env(
|
||||
env_logger::Env::default().default_filter_or("warn")
|
||||
).init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
let wallpaper_dir = Path::new(&cli.wallpaper_dir).canonicalize().unwrap();
|
||||
|
||||
// ********************************
|
||||
// Initialize wayland client
|
||||
// ********************************
|
||||
|
||||
let conn = Connection::connect_to_env().unwrap();
|
||||
let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
|
||||
let qh = event_queue.handle();
|
||||
|
||||
let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
|
||||
let layer_shell = LayerShell::bind(&globals, &qh).unwrap();
|
||||
let shm = Shm::bind(&globals, &qh).unwrap();
|
||||
|
||||
// Initialize slot pool with a minimum size (0 is not allowed)
|
||||
// it will be automatically resized later
|
||||
let shm_slot_pool = SlotPool::new(1, &shm).unwrap();
|
||||
|
||||
// Sync tools for sway ipc tasks
|
||||
let mut poll = Poll::new().unwrap();
|
||||
let waker = Arc::new(Waker::new(poll.registry(), SWAY).unwrap());
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let mut state = State {
|
||||
compositor_state,
|
||||
registry_state: RegistryState::new(&globals),
|
||||
output_state: OutputState::new(&globals, &qh),
|
||||
shm,
|
||||
shm_slot_pool,
|
||||
layer_shell,
|
||||
wallpaper_dir,
|
||||
pixel_format: None,
|
||||
background_layers: Vec::new(),
|
||||
sway_connection_task: SwayConnectionTask::new(
|
||||
tx.clone(), Arc::clone(&waker)
|
||||
),
|
||||
};
|
||||
|
||||
event_queue.roundtrip(&mut state).unwrap();
|
||||
|
||||
// ********************************
|
||||
// Main event loop
|
||||
// ********************************
|
||||
|
||||
let mut events = Events::with_capacity(16);
|
||||
|
||||
const WAYLAND: Token = Token(0);
|
||||
let read_guard = event_queue.prepare_read().unwrap();
|
||||
let wayland_socket_fd = read_guard.connection_fd().as_raw_fd();
|
||||
poll.registry().register(
|
||||
&mut SourceFd(&wayland_socket_fd),
|
||||
WAYLAND,
|
||||
Interest::READABLE
|
||||
).unwrap();
|
||||
drop(read_guard);
|
||||
|
||||
const SWAY: Token = Token(1);
|
||||
SwayConnectionTask::new(tx, waker).spawn_subscribe_event_loop();
|
||||
|
||||
loop {
|
||||
event_queue.flush().unwrap();
|
||||
event_queue.dispatch_pending(&mut state).unwrap();
|
||||
let mut read_guard_option = Some(event_queue.prepare_read().unwrap());
|
||||
|
||||
poll.poll(&mut events, None).unwrap();
|
||||
|
||||
for event in events.iter() {
|
||||
match event.token() {
|
||||
WAYLAND => handle_wayland_event(
|
||||
&mut state,
|
||||
&mut read_guard_option,
|
||||
&mut event_queue
|
||||
),
|
||||
SWAY => handle_sway_event(&mut state, &rx),
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_wayland_event(
|
||||
state: &mut State,
|
||||
read_guard_option: &mut Option<ReadEventsGuard>,
|
||||
event_queue: &mut EventQueue<State>,
|
||||
) {
|
||||
if let Some(read_guard) = read_guard_option.take() {
|
||||
if let Err(e) = read_guard.read() {
|
||||
// WouldBlock is normal here because of epoll false wakeups
|
||||
if let WaylandError::Io(ref io_err) = e {
|
||||
if io_err.kind() == io::ErrorKind::WouldBlock {
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("Failed to read Wayland events: {}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = event_queue.dispatch_pending(state) {
|
||||
panic!("Failed to dispatch pending Wayland events: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_sway_event(
|
||||
state: &mut State,
|
||||
rx: &Receiver<WorkspaceVisible>,
|
||||
) {
|
||||
while let Ok(workspace) = rx.try_recv()
|
||||
{
|
||||
// Find the background layer that of the output where the workspace is
|
||||
if let Some(affected_bg_layer) = state.background_layers.iter_mut()
|
||||
.find(|bg_layer| bg_layer.output_name == workspace.output)
|
||||
{
|
||||
affected_bg_layer.draw_workspace_bg(&workspace.workspace_name);
|
||||
}
|
||||
else {
|
||||
error!(
|
||||
"Workspace '{}' is on an unknown output '{}', known outputs were: {}",
|
||||
workspace.workspace_name,
|
||||
workspace.output,
|
||||
state.background_layers.iter()
|
||||
.map(|bg_layer| bg_layer.output_name.as_str())
|
||||
.collect::<Vec<_>>().join(", ")
|
||||
);
|
||||
continue;
|
||||
};
|
||||
}
|
||||
}
|
119
src/sway.rs
Normal file
119
src/sway.rs
Normal file
|
@ -0,0 +1,119 @@
|
|||
use std::{
|
||||
sync::{Arc, mpsc::Sender},
|
||||
thread::spawn,
|
||||
};
|
||||
|
||||
use mio::Waker;
|
||||
use swayipc::{Connection, Event, EventType, WorkspaceChange};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WorkspaceVisible {
|
||||
pub output: String,
|
||||
pub workspace_name: String
|
||||
}
|
||||
|
||||
pub struct SwayConnectionTask {
|
||||
sway_conn: Connection,
|
||||
tx: Sender<WorkspaceVisible>,
|
||||
waker: Arc<Waker>,
|
||||
}
|
||||
impl SwayConnectionTask
|
||||
{
|
||||
pub fn new(tx: Sender<WorkspaceVisible>, waker: Arc<Waker>) -> Self {
|
||||
SwayConnectionTask {
|
||||
sway_conn: Connection::new()
|
||||
.expect("Failed to connect to sway socket"),
|
||||
tx,
|
||||
waker
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_visible_workspace(&mut self, output: &str) {
|
||||
if let Some(workspace) = self.sway_conn.get_workspaces().unwrap()
|
||||
.into_iter()
|
||||
.filter(|w| w.visible)
|
||||
.find(|w| w.output == output)
|
||||
{
|
||||
self.tx.send(WorkspaceVisible {
|
||||
output: workspace.output,
|
||||
workspace_name: workspace.name,
|
||||
}).unwrap();
|
||||
|
||||
self.waker.wake().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_visible_workspaces(&mut self) {
|
||||
for workspace in self.sway_conn.get_workspaces().unwrap()
|
||||
.into_iter().filter(|w| w.visible)
|
||||
{
|
||||
self.tx.send(WorkspaceVisible {
|
||||
output: workspace.output,
|
||||
workspace_name: workspace.name,
|
||||
}).unwrap();
|
||||
}
|
||||
self.waker.wake().unwrap();
|
||||
}
|
||||
|
||||
pub fn spawn_subscribe_event_loop(self) {
|
||||
spawn(|| self.subscribe_event_loop());
|
||||
}
|
||||
|
||||
fn subscribe_event_loop(self) {
|
||||
let event_stream = self.sway_conn.subscribe([EventType::Workspace])
|
||||
.unwrap();
|
||||
for event_result in event_stream {
|
||||
let event = event_result.unwrap();
|
||||
let Event::Workspace(workspace_event) = event else {continue};
|
||||
if let WorkspaceChange::Focus = workspace_event.change {
|
||||
let current_workspace = workspace_event.current.unwrap();
|
||||
|
||||
self.tx.send(WorkspaceVisible {
|
||||
output: current_workspace.output.unwrap(),
|
||||
workspace_name: current_workspace.name.unwrap(),
|
||||
}).unwrap();
|
||||
|
||||
self.waker.wake().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn spawn_sway_events_loop_task(tx: Sender<WorkspaceVisible>, waker: Waker)
|
||||
// {
|
||||
// spawn(|| sway_events_loop_task(tx, waker));
|
||||
// }
|
||||
//
|
||||
// pub fn sway_events_loop_task(tx: Sender<WorkspaceVisible>, waker: Waker)
|
||||
// {
|
||||
// let mut sway_conn = Connection::new()
|
||||
// .expect("Failed to connect to sway socket");
|
||||
//
|
||||
// // Send the initial workspaces
|
||||
// for workspace in sway_conn.get_workspaces().unwrap().into_iter() {
|
||||
// if workspace.visible {
|
||||
// tx.send(WorkspaceVisible {
|
||||
// output: workspace.output,
|
||||
// workspace_name: workspace.name,
|
||||
// }).unwrap();
|
||||
// }
|
||||
// }
|
||||
// waker.wake().unwrap();
|
||||
//
|
||||
// // Event loop
|
||||
// let event_stream = sway_conn.subscribe([EventType::Workspace]).unwrap();
|
||||
// for event_result in event_stream {
|
||||
// let event = event_result.unwrap();
|
||||
// let Event::Workspace(workspace_event) = event else {continue};
|
||||
// if let WorkspaceChange::Focus = workspace_event.change {
|
||||
// let current_workspace = workspace_event.current.unwrap();
|
||||
//
|
||||
// tx.send(WorkspaceVisible {
|
||||
// output: current_workspace.output.unwrap(),
|
||||
// workspace_name: current_workspace.name.unwrap(),
|
||||
// }).unwrap();
|
||||
//
|
||||
// waker.wake().unwrap();
|
||||
// }
|
||||
// }
|
||||
// }
|
386
src/wayland.rs
Normal file
386
src/wayland.rs
Normal file
|
@ -0,0 +1,386 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use log::{debug, error};
|
||||
use smithay_client_toolkit::{
|
||||
delegate_compositor, delegate_layer, delegate_output, delegate_registry,
|
||||
delegate_shm,
|
||||
compositor::{CompositorHandler, CompositorState},
|
||||
output::{OutputHandler, OutputState},
|
||||
registry::{ProvidesRegistryState, RegistryState},
|
||||
registry_handlers,
|
||||
shell::{
|
||||
WaylandSurface,
|
||||
wlr_layer::{
|
||||
KeyboardInteractivity, Layer, LayerShell,
|
||||
LayerShellHandler, LayerSurface, LayerSurfaceConfigure,
|
||||
},
|
||||
},
|
||||
shm::{
|
||||
Shm, ShmHandler,
|
||||
slot::{Buffer, SlotPool},
|
||||
},
|
||||
};
|
||||
use smithay_client_toolkit::reexports::client::{
|
||||
Connection, QueueHandle,
|
||||
protocol::{wl_output, wl_shm, wl_surface},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
image::workspace_bgs_from_output_image_dir,
|
||||
sway::SwayConnectionTask,
|
||||
};
|
||||
|
||||
pub struct State {
|
||||
pub compositor_state: CompositorState,
|
||||
pub registry_state: RegistryState,
|
||||
pub output_state: OutputState,
|
||||
pub shm: Shm,
|
||||
pub shm_slot_pool: SlotPool,
|
||||
pub layer_shell: LayerShell,
|
||||
pub wallpaper_dir: PathBuf,
|
||||
pub pixel_format: Option<wl_shm::Format>,
|
||||
pub background_layers: Vec<BackgroundLayer>,
|
||||
pub sway_connection_task: SwayConnectionTask,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn pixel_format(&mut self) -> wl_shm::Format {
|
||||
*self.pixel_format.get_or_insert_with(|| {
|
||||
// Consume less gpu memory by using Bgr888 if available,
|
||||
// fall back to the always supported Xrgb8888 otherwise
|
||||
for format in self.shm.formats() {
|
||||
if let wl_shm::Format::Bgr888 = format {
|
||||
debug!("Using pixel format: {:?}", format);
|
||||
return *format
|
||||
}
|
||||
// XXX: One may add Rgb888 and HDR support here
|
||||
}
|
||||
debug!("Using default pixel format: Xrgb8888");
|
||||
|
||||
wl_shm::Format::Xrgb8888
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CompositorHandler for State
|
||||
{
|
||||
fn scale_factor_changed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
_new_factor: i32,
|
||||
) {
|
||||
}
|
||||
|
||||
fn frame(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
_time: u32,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl LayerShellHandler for State
|
||||
{
|
||||
fn closed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_layer: &LayerSurface
|
||||
) {
|
||||
}
|
||||
|
||||
fn configure(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
layer: &LayerSurface,
|
||||
_configure: LayerSurfaceConfigure,
|
||||
_serial: u32,
|
||||
) {
|
||||
// The new layer is ready: request all the visible workspace from sway,
|
||||
// it will get picked up by the main event loop and be drawn from there
|
||||
let bg_layer = self.background_layers.iter_mut()
|
||||
.find(|bg_layer| &bg_layer.layer == layer).unwrap();
|
||||
|
||||
if !bg_layer.configured {
|
||||
bg_layer.configured = true;
|
||||
self.sway_connection_task
|
||||
.request_visible_workspace(&bg_layer.output_name);
|
||||
|
||||
debug!(
|
||||
"Background layer has been configured for output: {}",
|
||||
bg_layer.output_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputHandler for State {
|
||||
fn output_state(&mut self) -> &mut OutputState {
|
||||
&mut self.output_state
|
||||
}
|
||||
|
||||
fn new_output(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
output: wl_output::WlOutput,
|
||||
) {
|
||||
let Some(info) = self.output_state.info(&output)
|
||||
else {
|
||||
error!("New output has no output info, skipping");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(output_name) = info.name
|
||||
else {
|
||||
error!("New output has no name, skipping");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((width, height)) = info.modes.iter()
|
||||
.find(|mode| mode.current)
|
||||
.map(|mode| mode.dimensions)
|
||||
else {
|
||||
error!(
|
||||
"New output '{}' has no current mode set, skipping",
|
||||
output_name
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
debug!(
|
||||
"New output, name: {}, resolution: {}x{}",
|
||||
output_name, width, height
|
||||
);
|
||||
|
||||
let surface = self.compositor_state.create_surface(qh);
|
||||
|
||||
let layer = self.layer_shell.create_layer_surface(
|
||||
qh,
|
||||
surface,
|
||||
Layer::Background,
|
||||
layer_surface_name(&output_name),
|
||||
Some(&output)
|
||||
);
|
||||
|
||||
layer.set_exclusive_zone(-1); // Don't let the status bar push it around
|
||||
layer.set_keyboard_interactivity(KeyboardInteractivity::None);
|
||||
layer.set_size(width as u32, height as u32);
|
||||
|
||||
layer.commit();
|
||||
|
||||
let pixel_format = self.pixel_format();
|
||||
|
||||
let output_wallpaper_dir = self.wallpaper_dir.join(&output_name);
|
||||
|
||||
let workspace_backgrounds = match workspace_bgs_from_output_image_dir(
|
||||
&output_wallpaper_dir,
|
||||
&mut self.shm_slot_pool,
|
||||
pixel_format
|
||||
) {
|
||||
Ok(workspace_bgs) => {
|
||||
debug!(
|
||||
"Loaded {} wallpapers on new output for workspaces: {}",
|
||||
workspace_bgs.len(),
|
||||
workspace_bgs.iter()
|
||||
.map(|workspace_bg| workspace_bg.workspace_name.as_str())
|
||||
.collect::<Vec<_>>().join(", ")
|
||||
);
|
||||
workspace_bgs
|
||||
},
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to get wallpapers for new output '{}' form '{:?}': {}",
|
||||
output_name, output_wallpaper_dir, e
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.background_layers.push(BackgroundLayer {
|
||||
output_name,
|
||||
width,
|
||||
height,
|
||||
layer,
|
||||
configured: false,
|
||||
workspace_backgrounds,
|
||||
});
|
||||
}
|
||||
|
||||
fn update_output(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
// This will only be needed if we implement scaling the wallpapers
|
||||
// to the output resolution
|
||||
//
|
||||
// let Some(info) = self.output_state.info(&output)
|
||||
// else {
|
||||
// error!("Updated output has no output info, skipping");
|
||||
// return;
|
||||
// };
|
||||
//
|
||||
// let Some(name) = info.name
|
||||
// else {
|
||||
// error!("Updated output has no name, skipping");
|
||||
// return;
|
||||
// };
|
||||
//
|
||||
// let Some((width, height)) = info.modes.iter()
|
||||
// .find(|mode| mode.current)
|
||||
// .map(|mode| mode.dimensions)
|
||||
// else {
|
||||
// error!(
|
||||
// "Updated output '{}' has no current mode set, skipping",
|
||||
// name
|
||||
// );
|
||||
// return;
|
||||
// };
|
||||
//
|
||||
// if let Some(bg_layer) = self.background_layers.iter()
|
||||
// .find(|bg_layers| bg_layers.output_name == name)
|
||||
// {
|
||||
// if bg_layer.width == width && bg_layer.height == height {
|
||||
// // if a known output has its resolution unchanged
|
||||
// // then ignore this event
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // renew the output otherwise
|
||||
// self.output_destroyed(conn, qh, output.clone());
|
||||
// self.new_output(conn, qh, output)
|
||||
}
|
||||
|
||||
fn output_destroyed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
output: wl_output::WlOutput,
|
||||
) {
|
||||
let Some(info) = self.output_state.info(&output)
|
||||
else {
|
||||
error!("Destroyed output has no output info, skipping");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(name) = info.name
|
||||
else {
|
||||
error!("Destroyed output has no name, skipping");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(bg_layer_index) = self.background_layers.iter()
|
||||
.position(|bg_layers| bg_layers.output_name == name)
|
||||
{
|
||||
self.background_layers.swap_remove(bg_layer_index);
|
||||
|
||||
// Workspaces on the destroyed output may have been moved anywhere
|
||||
// so reset the wallpaper on all the visible workspaces
|
||||
self.sway_connection_task.request_visible_workspaces();
|
||||
|
||||
debug!("Destroyed output: {}", name);
|
||||
}
|
||||
else {
|
||||
error!(
|
||||
"Ignoring destroyed output with unknown name '{}', known outputs were: {}",
|
||||
name,
|
||||
self.background_layers.iter()
|
||||
.map(|bg_layer| bg_layer.output_name.as_str())
|
||||
.collect::<Vec<_>>().join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProvidesRegistryState for State {
|
||||
fn registry(&mut self) -> &mut RegistryState {
|
||||
&mut self.registry_state
|
||||
}
|
||||
registry_handlers![OutputState];
|
||||
}
|
||||
|
||||
impl ShmHandler for State {
|
||||
fn shm_state(&mut self) -> &mut Shm {
|
||||
&mut self.shm
|
||||
}
|
||||
}
|
||||
|
||||
delegate_compositor!(State);
|
||||
delegate_layer!(State);
|
||||
delegate_output!(State);
|
||||
delegate_registry!(State);
|
||||
delegate_shm!(State);
|
||||
|
||||
pub struct BackgroundLayer {
|
||||
pub output_name: String,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
pub layer: LayerSurface,
|
||||
pub configured: bool,
|
||||
pub workspace_backgrounds: Vec<WorkspaceBackground>,
|
||||
}
|
||||
impl BackgroundLayer
|
||||
{
|
||||
pub fn draw_workspace_bg(&mut self, workspace_name: &str)
|
||||
{
|
||||
if !self.configured {
|
||||
error!(
|
||||
"Cannot draw wallpaper image on the not yet configured layer for output: {}",
|
||||
self.output_name
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(workspace_bg) = self.workspace_backgrounds.iter()
|
||||
.find(|workspace_bg| workspace_bg.workspace_name == workspace_name)
|
||||
else {
|
||||
error!(
|
||||
"There is no wallpaper image on output '{}' for workspace '{}', only for: {}",
|
||||
self.output_name,
|
||||
workspace_name,
|
||||
self.workspace_backgrounds.iter()
|
||||
.map(|workspace_bg| workspace_bg.workspace_name.as_str())
|
||||
.collect::<Vec<_>>().join(", ")
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
// Attach and commit to new workspace background
|
||||
if let Err(e) = workspace_bg.buffer.attach_to(self.layer.wl_surface()) {
|
||||
error!(
|
||||
"Error attaching buffer of workspace '{}' on output '{}': {:#?}",
|
||||
workspace_name,
|
||||
self.output_name,
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Damage the entire surface
|
||||
self.layer.wl_surface().damage_buffer(0, 0, self.width, self.height);
|
||||
|
||||
self.layer.commit();
|
||||
|
||||
debug!(
|
||||
"Setting wallpaper on output '{}' for workspace: {}",
|
||||
self.output_name, workspace_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WorkspaceBackground {
|
||||
pub workspace_name: String,
|
||||
pub buffer: Buffer,
|
||||
}
|
||||
|
||||
fn layer_surface_name(output_name: &str) -> Option<String> {
|
||||
Some([env!("CARGO_PKG_NAME"), "_wallpaper_", output_name].concat())
|
||||
}
|
Loading…
Add table
Reference in a new issue