├── LICENSE ├── README.md ├── main.c ├── wallpaper.py ├── wayland_stub.c ├── wayland_support.c └── wayland_support.h /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 techlm77 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GNOME Wallpaper Engine 2 | 3 | GNOME Wallpaper Engine is a lightweight, GPU-accelerated dynamic wallpaper solution for Linux. It uses GStreamer for video playback and GTK3 to automatically detect your desktop's workarea and scale the video to perfectly fit your screen—giving you a full-featured animated wallpaper experience similar to Wallpaper Engine. 4 | 5 | ## Features 6 | 7 | - **Dynamic Video Wallpaper:** Set a video as your desktop wallpaper. 8 | - **Automatic Scaling:** Detects your monitor's workarea and scales the video to fill your screen. 9 | - **GPU-Accelerated:** Leverages GStreamer for efficient, hardware-accelerated playback. 10 | - **Lightweight:** Designed to have minimal impact on system resources. 11 | - **Wayland Support:** Yes!! You have heard that right XD, this is the first time finally adding Wayland support. However... there's no multi-monitor support as it only works for a single monitor for now. 12 | 13 | ## Prerequisites 14 | 15 | Before you get started, ensure you have the following installed: 16 | 17 | - **Python 3.x** 18 | - **GStreamer 1.0** (and necessary plugins, e.g., `gst-plugins-base`, `gst-plugins-good`, etc.) 19 | - **GTK 3** 20 | - **Wayland development libraries** (only needed for Wayland users) 21 | 22 | For Ubuntu/Debian systems, you can install the required packages with: 23 | 24 | ```bash 25 | sudo apt install python3-gi python3-gst-1.0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gir1.2-gst-plugins-base-1.0 gir1.2-gtk-3.0 wayland-protocols libwayland-dev 26 | ``` 27 | 28 | ## Installation 29 | 30 | ### For Wayland Users: 31 | If you're using Wayland, you need to compile the Wayland stub before running the script: 32 | 33 | ```bash 34 | gcc main.c -o main $(pkg-config --cflags --libs wayland-client) 35 | gcc wayland_stub.c -o wayland_stub $(pkg-config --cflags --libs wayland-client) 36 | gcc -c wayland_support.c $(pkg-config --cflags wayland-client) 37 | ``` 38 | 39 | Then, make the script executable and run it: 40 | 41 | ```bash 42 | chmod +x wallpaper.py 43 | ./wallpaper.py 44 | ``` 45 | 46 | Alternatively, you can set it up to start automatically using a `.desktop` file (see below). 47 | 48 | ### For X11 Users: 49 | No additional compilation is needed. Just make the script executable: 50 | 51 | ```bash 52 | chmod +x wallpaper.py 53 | ``` 54 | 55 | Then run: 56 | 57 | ```bash 58 | ./wallpaper.py 59 | ``` 60 | 61 | ## Usage 62 | 63 | 1. **Place your video file:** 64 | 65 | Make sure you have your video file at `~/Videos/wallpaper.mp4`. You can change this path in the script if needed. 66 | 67 | 2. **Run the script:** 68 | 69 | ```bash 70 | ./wallpaper.py 71 | ``` 72 | 73 | 3. **Auto-Start on Login:** 74 | 75 | To run the wallpaper automatically on startup, copy the provided desktop entry (or create one) to your autostart directory: 76 | 77 | ```bash 78 | mkdir ~/.config/autostart/ && nano video-wallpaper.desktop && cp video-wallpaper.desktop ~/.config/autostart/ 79 | ``` 80 | 81 | *Example `video-wallpaper.desktop` content:* 82 | 83 | ```ini 84 | [Desktop Entry] 85 | Type=Application 86 | Name=Video Wallpaper 87 | Comment=Sets a video as the desktop wallpaper on startup 88 | Exec=/full/path/to/gnome-wallpaper-engine/wallpaper.py 89 | X-GNOME-Autostart-enabled=true 90 | Terminal=false 91 | ``` 92 | 93 | ## Contributing 94 | 95 | Contributions, issues, and feature requests are welcome! Please feel free to check out the [issues page](https://github.com/Techlm77/gnome-wallpaper-engine/issues) or submit a pull request. 96 | 97 | ## License 98 | 99 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 100 | 101 | --- 102 | 103 | Enjoy a dynamic, animated wallpaper on your GNOME desktop! 104 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #ifdef WAYLAND_SUPPORT 4 | #include "wayland_support.h" 5 | #endif 6 | 7 | #ifdef X11_SUPPORT 8 | #include 9 | #endif 10 | 11 | int main(int argc, char *argv[]) { 12 | const char* wayland_display = getenv("WAYLAND_DISPLAY"); 13 | if (wayland_display) { 14 | #ifdef WAYLAND_SUPPORT 15 | struct wl_display* display = init_wayland_display(); 16 | if (display) { 17 | cleanup_wayland_display(display); 18 | } 19 | #endif 20 | } 21 | else { 22 | #ifdef X11_SUPPORT 23 | 24 | #endif 25 | } 26 | return 0; 27 | } 28 | -------------------------------------------------------------------------------- /wallpaper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import subprocess 5 | import gi 6 | import socket 7 | import threading 8 | import tempfile 9 | 10 | gi.require_version('Gst', '1.0') 11 | gi.require_version('Gtk', '3.0') 12 | from gi.repository import Gst, Gtk, Gdk, GLib, GdkPixbuf 13 | 14 | SOCKET_PATH = "/tmp/wallpaper.sock" 15 | 16 | def send_ipc_command(cmd): 17 | """Send a command (string) to the wallpaper process via IPC.""" 18 | try: 19 | s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 20 | s.connect(SOCKET_PATH) 21 | s.sendall(cmd.encode('utf-8')) 22 | s.close() 23 | except Exception as e: 24 | print("Error sending IPC command:", e) 25 | 26 | class WallpaperIPCServer(threading.Thread): 27 | """A simple IPC server that listens on a Unix domain socket for commands.""" 28 | def __init__(self, wallpaper): 29 | threading.Thread.__init__(self) 30 | self.wallpaper = wallpaper 31 | self.daemon = True 32 | self.sock_path = SOCKET_PATH 33 | if os.path.exists(self.sock_path): 34 | os.remove(self.sock_path) 35 | self.server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 36 | self.server.bind(self.sock_path) 37 | self.server.listen(5) 38 | 39 | def run(self): 40 | while True: 41 | try: 42 | conn, _ = self.server.accept() 43 | data = conn.recv(1024) 44 | if data: 45 | cmd = data.decode('utf-8').strip() 46 | parts = cmd.split("|") 47 | if parts[0] == "update_video" and len(parts) > 1: 48 | self.wallpaper.update_video(parts[1]) 49 | elif parts[0] == "play": 50 | self.wallpaper.play() 51 | elif parts[0] == "pause": 52 | self.wallpaper.pause() 53 | elif parts[0] == "stop": 54 | self.wallpaper.stop() 55 | elif parts[0] == "volume" and len(parts) > 1: 56 | try: 57 | vol = float(parts[1]) 58 | self.wallpaper.player.set_property("volume", vol) 59 | except: 60 | pass 61 | conn.close() 62 | except Exception as e: 63 | print("IPC server error:", e) 64 | 65 | class VideoWallpaper(Gtk.Window): 66 | def __init__(self, video_path=None): 67 | Gtk.Window.__init__(self, title="Video Wallpaper") 68 | self.set_decorated(False) 69 | self.set_app_paintable(True) 70 | self.set_skip_taskbar_hint(True) 71 | self.set_skip_pager_hint(True) 72 | self.set_accept_focus(False) 73 | 74 | if os.environ.get("WAYLAND_DISPLAY"): 75 | try: 76 | subprocess.run(["./wayland_stub"], check=True, capture_output=True, text=True) 77 | except subprocess.CalledProcessError as e: 78 | print("Error executing Wayland stub:", e.stderr) 79 | self.set_type_hint(Gdk.WindowTypeHint.DESKTOP) 80 | else: 81 | self.set_type_hint(Gdk.WindowTypeHint.DESKTOP) 82 | 83 | display = Gdk.Display.get_default() 84 | monitor = display.get_monitor(0) 85 | workarea = monitor.get_geometry() 86 | self.width = workarea.width 87 | self.height = workarea.height 88 | self.set_default_size(self.width, self.height) 89 | self.move(workarea.x, workarea.y) 90 | self.set_keep_below(True) 91 | self.stick() 92 | GLib.timeout_add(500, self.push_below) 93 | 94 | self.fixed = Gtk.Fixed() 95 | self.add(self.fixed) 96 | 97 | Gst.init(None) 98 | self.player = Gst.ElementFactory.make("playbin", "player") 99 | if not self.player: 100 | print("Error: Could not create playbin element.") 101 | exit(-1) 102 | 103 | if video_path is None: 104 | video_path = os.path.expanduser("~/Videos/wallpaper.mp4") 105 | if not os.path.exists(video_path): 106 | print("Error: Video file not found:", video_path) 107 | exit(-1) 108 | self.player.set_property("uri", "file://" + video_path) 109 | 110 | video_filter_bin = Gst.Bin.new("video_filter_bin") 111 | videoscale = Gst.ElementFactory.make("videoscale", "videoscale") 112 | capsfilter = Gst.ElementFactory.make("capsfilter", "capsfilter") 113 | if not videoscale or not capsfilter: 114 | print("Error: Could not create videoscale or capsfilter") 115 | exit(-1) 116 | caps = Gst.Caps.from_string(f"video/x-raw, width={self.width}, height={self.height}, pixel-aspect-ratio=1/1") 117 | capsfilter.set_property("caps", caps) 118 | video_filter_bin.add(videoscale) 119 | video_filter_bin.add(capsfilter) 120 | if not videoscale.link(capsfilter): 121 | print("Error: Failed to link videoscale to capsfilter") 122 | exit(-1) 123 | ghost_sink = Gst.GhostPad.new("sink", videoscale.get_static_pad("sink")) 124 | video_filter_bin.add_pad(ghost_sink) 125 | ghost_src = Gst.GhostPad.new("src", capsfilter.get_static_pad("src")) 126 | video_filter_bin.add_pad(ghost_src) 127 | self.player.set_property("video-filter", video_filter_bin) 128 | 129 | possible_sinks = ["gtksink", "vaapisink", "glimagesink", "autovideosink"] 130 | videosink = None 131 | for sink_name in possible_sinks: 132 | videosink = Gst.ElementFactory.make(sink_name, "videosink") 133 | if videosink: 134 | break 135 | if videosink: 136 | if any(prop.name == "force-aspect-ratio" for prop in videosink.list_properties()): 137 | videosink.set_property("force-aspect-ratio", False) 138 | self.player.set_property("video-sink", videosink) 139 | else: 140 | print("Error: No suitable video sink found.") 141 | exit(-1) 142 | try: 143 | video_widget = videosink.props.widget 144 | video_widget.set_hexpand(True) 145 | video_widget.set_vexpand(True) 146 | video_widget.set_size_request(self.width, self.height) 147 | self.fixed.put(video_widget, 0, 0) 148 | except Exception as e: 149 | print("Error: Could not retrieve widget from videosink:", e) 150 | 151 | self.show_all() 152 | 153 | bus = self.player.get_bus() 154 | bus.add_signal_watch() 155 | bus.connect("message", self.on_bus_message) 156 | self.player.set_state(Gst.State.PLAYING) 157 | 158 | def start_ipc_server(self): 159 | self.ipc_server = WallpaperIPCServer(self) 160 | self.ipc_server.start() 161 | 162 | def push_below(self): 163 | if self.get_window(): 164 | self.get_window().lower() 165 | return True 166 | 167 | def on_bus_message(self, bus, message): 168 | t = message.type 169 | if t == Gst.MessageType.ERROR: 170 | err, debug = message.parse_error() 171 | print("GStreamer Error:", err, debug) 172 | self.player.set_state(Gst.State.NULL) 173 | elif t == Gst.MessageType.EOS: 174 | self.player.seek_simple(Gst.Format.TIME, 175 | Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, 176 | 0) 177 | return True 178 | 179 | def on_destroy(self, *args): 180 | self.player.set_state(Gst.State.NULL) 181 | if os.path.exists(SOCKET_PATH): 182 | os.remove(SOCKET_PATH) 183 | Gtk.main_quit() 184 | 185 | def update_video(self, video_path): 186 | if os.path.exists(video_path): 187 | self.player.set_state(Gst.State.NULL) 188 | self.player.set_property("uri", "file://" + video_path) 189 | self.player.set_state(Gst.State.PLAYING) 190 | else: 191 | print("Error: Video file not found:", video_path) 192 | 193 | def pause(self): 194 | self.player.set_state(Gst.State.PAUSED) 195 | 196 | def play(self): 197 | self.player.set_state(Gst.State.PLAYING) 198 | 199 | def stop(self): 200 | self.player.set_state(Gst.State.NULL) 201 | 202 | class ControlPanel(Gtk.Window): 203 | def __init__(self): 204 | Gtk.Window.__init__(self, title="Wallpaper Control Panel") 205 | self.set_border_width(10) 206 | self.set_default_size(600, 400) 207 | 208 | vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) 209 | self.add(vbox) 210 | 211 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) 212 | vbox.pack_start(hbox, False, False, 0) 213 | 214 | file_button = Gtk.Button(label="Select Video") 215 | file_button.connect("clicked", self.on_select_video) 216 | hbox.pack_start(file_button, False, False, 0) 217 | 218 | play_button = Gtk.Button(label="Play") 219 | play_button.connect("clicked", self.on_play) 220 | hbox.pack_start(play_button, False, False, 0) 221 | 222 | pause_button = Gtk.Button(label="Pause") 223 | pause_button.connect("clicked", self.on_pause) 224 | hbox.pack_start(pause_button, False, False, 0) 225 | 226 | stop_button = Gtk.Button(label="Stop") 227 | stop_button.connect("clicked", self.on_stop) 228 | hbox.pack_start(stop_button, False, False, 0) 229 | 230 | vol_label = Gtk.Label(label="Volume") 231 | hbox.pack_start(vol_label, False, False, 0) 232 | adjustment = Gtk.Adjustment(value=1, lower=0, upper=1, step_increment=0.1, page_increment=0.1, page_size=0) 233 | self.volume_scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, adjustment=adjustment) 234 | self.volume_scale.set_digits(1) 235 | self.volume_scale.set_value(1) 236 | self.volume_scale.connect("value-changed", self.on_volume_changed) 237 | hbox.pack_start(self.volume_scale, True, True, 0) 238 | 239 | separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) 240 | vbox.pack_start(separator, False, False, 5) 241 | 242 | scrolled = Gtk.ScrolledWindow() 243 | scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 244 | vbox.pack_start(scrolled, True, True, 0) 245 | 246 | self.flowbox = Gtk.FlowBox() 247 | self.flowbox.set_valign(Gtk.Align.START) 248 | self.flowbox.set_max_children_per_line(4) 249 | self.flowbox.set_selection_mode(Gtk.SelectionMode.NONE) 250 | scrolled.add(self.flowbox) 251 | 252 | self.populate_thumbnails() 253 | 254 | def on_select_video(self, widget): 255 | dialog = Gtk.FileChooserDialog(title="Select Video File", parent=self, 256 | action=Gtk.FileChooserAction.OPEN) 257 | dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, 258 | Gtk.STOCK_OPEN, Gtk.ResponseType.OK) 259 | filter_video = Gtk.FileFilter() 260 | filter_video.set_name("Video Files") 261 | filter_video.add_mime_type("video/mp4") 262 | filter_video.add_mime_type("video/x-matroska") 263 | filter_video.add_pattern("*.mp4") 264 | filter_video.add_pattern("*.mkv") 265 | dialog.add_filter(filter_video) 266 | response = dialog.run() 267 | if response == Gtk.ResponseType.OK: 268 | video_path = dialog.get_filename() 269 | send_ipc_command("update_video|" + video_path) 270 | dialog.destroy() 271 | 272 | def on_play(self, widget): 273 | send_ipc_command("play") 274 | 275 | def on_pause(self, widget): 276 | send_ipc_command("pause") 277 | 278 | def on_stop(self, widget): 279 | send_ipc_command("stop") 280 | 281 | def on_volume_changed(self, scale): 282 | vol = scale.get_value() 283 | send_ipc_command("volume|" + str(vol)) 284 | 285 | def populate_thumbnails(self): 286 | video_dir = os.path.expanduser("~/Videos") 287 | try: 288 | video_files = [os.path.join(video_dir, f) for f in os.listdir(video_dir) 289 | if f.lower().endswith(('.mp4', '.mkv'))] 290 | except Exception as e: 291 | print("Error scanning video directory:", e) 292 | video_files = [] 293 | for video in video_files: 294 | thumbnail = self.create_thumbnail(video) 295 | if thumbnail: 296 | event_box = Gtk.EventBox() 297 | image = Gtk.Image.new_from_pixbuf(thumbnail) 298 | event_box.add(image) 299 | event_box.connect("button-press-event", self.on_thumbnail_clicked, video) 300 | self.flowbox.add(event_box) 301 | self.flowbox.show_all() 302 | 303 | def create_thumbnail(self, video_path): 304 | try: 305 | tmp_dir = tempfile.gettempdir() 306 | thumb_path = os.path.join(tmp_dir, os.path.basename(video_path) + ".png") 307 | cmd = [ 308 | "ffmpeg", 309 | "-y", 310 | "-i", video_path, 311 | "-ss", "00:00:01.000", 312 | "-vframes", "1", 313 | thumb_path 314 | ] 315 | subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 316 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(thumb_path, width=150, height=100, preserve_aspect_ratio=True) 317 | return pixbuf 318 | except Exception as e: 319 | print("Error creating thumbnail for", video_path, ":", e) 320 | return None 321 | 322 | def on_thumbnail_clicked(self, widget, event, video_path): 323 | send_ipc_command("update_video|" + video_path) 324 | 325 | def is_wallpaper_running(): 326 | try: 327 | s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 328 | s.connect(SOCKET_PATH) 329 | s.close() 330 | return True 331 | except: 332 | return False 333 | 334 | def run_wallpaper_daemon(): 335 | wallpaper = VideoWallpaper() 336 | wallpaper.start_ipc_server() 337 | wallpaper.connect("destroy", wallpaper.on_destroy) 338 | Gtk.main() 339 | 340 | def run_control_panel(): 341 | control_panel = ControlPanel() 342 | control_panel.connect("destroy", Gtk.main_quit) 343 | control_panel.show_all() 344 | Gtk.main() 345 | 346 | if __name__ == "__main__": 347 | if len(sys.argv) > 1: 348 | if sys.argv[1] == "--daemon": 349 | run_wallpaper_daemon() 350 | elif sys.argv[1] == "--control": 351 | run_control_panel() 352 | else: 353 | print("Usage: {} [--daemon | --control]".format(sys.argv[0])) 354 | else: 355 | if not is_wallpaper_running(): 356 | subprocess.Popen([sys.executable, sys.argv[0], "--daemon"]) 357 | run_control_panel() 358 | -------------------------------------------------------------------------------- /wayland_stub.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char **argv) { 5 | if (!getenv("WAYLAND_DISPLAY")) 6 | return 1; 7 | return 0; 8 | } 9 | -------------------------------------------------------------------------------- /wayland_support.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "wayland_support.h" 4 | 5 | struct wl_display* init_wayland_display(void) { 6 | struct wl_display *display = wl_display_connect(NULL); 7 | if (!display) 8 | return NULL; 9 | return display; 10 | } 11 | 12 | void cleanup_wayland_display(struct wl_display* display) { 13 | if (display) 14 | wl_display_disconnect(display); 15 | } 16 | -------------------------------------------------------------------------------- /wayland_support.h: -------------------------------------------------------------------------------- 1 | #ifndef WAYLAND_SUPPORT_H 2 | #define WAYLAND_SUPPORT_H 3 | 4 | #include 5 | 6 | struct wl_display* init_wayland_display(void); 7 | 8 | void cleanup_wayland_display(struct wl_display* display); 9 | 10 | #endif 11 | --------------------------------------------------------------------------------