diff options
Diffstat (limited to '')
| -rw-r--r-- | src/main.rs | 413 | ||||
| -rw-r--r-- | src/shader.rs | 24 | ||||
| -rw-r--r-- | src/wayland.rs | 176 |
3 files changed, 613 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d58cae1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,413 @@ +mod wayland; + +use anyhow::Result; +use clap::{Arg, Command}; +use log::{error, info, warn}; +use std::mem; +use std::time::Instant; +use wgpu::util::DeviceExt; +use winit::{ + application::ApplicationHandler, + event::WindowEvent, + event_loop::{ActiveEventLoop, EventLoop}, + window::{Window, WindowAttributes, WindowId}, +}; + +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +struct Uniforms { + i_resolution: [f32; 3], + i_time: f32, + i_mouse: [f32; 4], +} + +struct WallpaperApp { + window: Option<Window>, + surface: Option<wgpu::Surface<'static>>, + device: Option<wgpu::Device>, + queue: Option<wgpu::Queue>, + config: Option<wgpu::SurfaceConfiguration>, + render_pipeline: Option<wgpu::RenderPipeline>, + uniform_buffer: Option<wgpu::Buffer>, + bind_group: Option<wgpu::BindGroup>, + start_time: Instant, + shader_path: String, + hyprland_config: wayland::HyprlandConfig, + wayland_configured: bool, +} + +impl WallpaperApp { + fn new(shader_path: String) -> Self { + Self { + window: None, + surface: None, + device: None, + queue: None, + config: None, + render_pipeline: None, + uniform_buffer: None, + bind_group: None, + start_time: Instant::now(), + shader_path, + hyprland_config: wayland::HyprlandConfig::default(), + wayland_configured: false, + } + } + + fn init_graphics(&mut self) -> Result<()> { + let window = self + .window + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Window not initialized"))?; + let size = window.inner_size(); + let instance = wgpu::Instance::default(); + + // Create surface + let surface = instance + .create_surface(window) + .map_err(|e| anyhow::anyhow!("Failed to create surface: {}", e))?; + let surface = + unsafe { mem::transmute::<wgpu::Surface<'_>, wgpu::Surface<'static>>(surface) }; + + let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + })) + .unwrap(); + + let (device, queue) = + pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor { + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::downlevel_defaults(), + memory_hints: wgpu::MemoryHints::Performance, + label: None, + experimental_features: wgpu::ExperimentalFeatures::disabled(), + trace: wgpu::Trace::Off, + }))?; + + let surface_caps = surface.get_capabilities(&adapter); + let surface_format = surface_caps + .formats + .iter() + .copied() + .find(|f| f.is_srgb()) + .unwrap_or(surface_caps.formats[0]); + + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface_format, + width: size.width, + height: size.height, + present_mode: surface_caps.present_modes[0], + alpha_mode: surface_caps.alpha_modes[0], + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + surface.configure(&device, &config); + + // Load and compile shader + let shader_source = std::fs::read_to_string(&self.shader_path).map_err(|e| { + anyhow::anyhow!("Failed to read shader file {}: {}", &self.shader_path, e) + })?; + + let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Wallpaper Shader"), + source: wgpu::ShaderSource::Wgsl(shader_source.as_str().into()), + }); + + // Create uniform buffer + let uniforms = Uniforms { + i_resolution: [size.width as f32, size.height as f32, 0.0], + i_time: 0.0, + i_mouse: [0.0, 0.0, 0.0, 0.0], + }; + + let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Uniform Buffer"), + contents: bytemuck::cast_slice(&[uniforms]), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: Some("uniform_bind_group_layout"), + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], + label: Some("uniform_bind_group"), + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Render Pipeline Layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Render Pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader_module, + entry_point: Some("vs_main"), + compilation_options: wgpu::PipelineCompilationOptions::default(), + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &shader_module, + entry_point: Some("fs_main"), + compilation_options: wgpu::PipelineCompilationOptions::default(), + targets: &[Some(wgpu::ColorTargetState { + format: config.format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }); + + self.surface = Some(surface); + self.device = Some(device); + self.queue = Some(queue); + self.config = Some(config); + self.render_pipeline = Some(render_pipeline); + self.uniform_buffer = Some(uniform_buffer); + self.bind_group = Some(bind_group); + + Ok(()) + } + + fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) { + if new_size.width > 0 && new_size.height > 0 { + if let (Some(surface), Some(device), Some(config)) = + (&self.surface, &self.device, &mut self.config) + { + config.width = new_size.width; + config.height = new_size.height; + surface.configure(device, config); + } + } + } + + fn render(&mut self) -> Result<()> { + let surface = self + .surface + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Surface not initialized"))?; + let device = self + .device + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Device not initialized"))?; + let queue = self + .queue + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Queue not initialized"))?; + let render_pipeline = self + .render_pipeline + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Render pipeline not initialized"))?; + let uniform_buffer = self + .uniform_buffer + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Uniform buffer not initialized"))?; + let bind_group = self + .bind_group + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bind group not initialized"))?; + let config = self + .config + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Config not initialized"))?; + + let frame = surface.get_current_texture()?; + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + // Update uniforms + let uniforms = Uniforms { + i_resolution: [config.width as f32, config.height as f32, 0.0], + i_time: self.start_time.elapsed().as_secs_f32(), + i_mouse: [0.0, 0.0, 0.0, 0.0], + }; + + queue.write_buffer(uniform_buffer, 0, bytemuck::cast_slice(&[uniforms])); + + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + depth_slice: None, + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + + render_pass.set_pipeline(render_pipeline); + render_pass.set_bind_group(0, bind_group, &[]); + render_pass.draw(0..3, 0..1); + } + + queue.submit(std::iter::once(encoder.finish())); + frame.present(); + + Ok(()) + } +} + +impl ApplicationHandler for WallpaperApp { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + if self.window.is_none() { + let window_attributes = WindowAttributes::default() + .with_title("Hyprland Live Wallpaper") + .with_transparent(true) + .with_decorations(false) + .with_fullscreen(Some(winit::window::Fullscreen::Borderless(None))); + + let window = match event_loop.create_window(window_attributes) { + Ok(window) => window, + Err(e) => { + error!("Failed to create window: {}", e); + return; + } + }; + + // Configure window for Hyprland + if let Err(e) = wayland::configure_hyprland_window(&window, &self.hyprland_config) { + warn!("Failed to configure Hyprland window: {}", e); + } + + // Store window first, then initialize graphics + self.window = Some(window); + + match self.init_graphics() { + Ok(()) => { + info!("Wallpaper service running"); + } + Err(e) => { + error!("Failed to initialize graphics: {}", e); + } + } + } + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + match event { + WindowEvent::CloseRequested => { + event_loop.exit(); + } + WindowEvent::Resized(size) => { + self.resize(size); + } + WindowEvent::RedrawRequested => { + // Apply Hyprland rules on first redraw (after window is mapped) + if !self.wayland_configured { + if let Some(window) = &self.window { + // Convert window ID to u64 for hyprctl + let window_id: u64 = window.id().into(); + if let Err(e) = + wayland::apply_hyprland_rules(window_id, &self.hyprland_config) + { + warn!("Failed to apply Hyprland rules: {}", e); + } + self.wayland_configured = true; + } + } + + if let Err(e) = self.render() { + error!("Render error: {}", e); + event_loop.exit(); + } + } + _ => {} + } + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + if let Some(window) = &self.window { + window.request_redraw(); + } + } +} + +fn main() -> Result<()> { + env_logger::init(); + + let matches = Command::new("hyprland-live-wallpaper") + .version("0.1.0") + .about("Live wallpaper service for Hyprland using wgpu") + .arg( + Arg::new("shader") + .short('s') + .long("shader") + .value_name("FILE") + .help("Path to WGSL shader file"), + ) + .get_matches(); + + info!("Starting Hyprland Live Wallpaper"); + + let shader_path = matches + .get_one::<String>("shader") + .map(|s| s.as_str()) + .unwrap_or("shaders/default.wgsl") + .to_string(); + + run_wallpaper(shader_path)?; + + Ok(()) +} + +fn run_wallpaper(shader_path: String) -> Result<()> { + let event_loop = EventLoop::new()?; + let mut app = WallpaperApp::new(shader_path); + + event_loop.run_app(&mut app)?; + Ok(()) +} diff --git a/src/shader.rs b/src/shader.rs new file mode 100644 index 0000000..47a1621 --- /dev/null +++ b/src/shader.rs @@ -0,0 +1,24 @@ +use anyhow::Result; +use std::path::Path; + +pub struct ShaderManager { + shader_dir: String, +} + +impl ShaderManager { + pub fn new(shader_dir: &str) -> Self { + Self { + shader_dir: shader_dir.to_string(), + } + } + + pub fn load_shader(&self, name: &str) -> Result<String> { + let path = Path::new(&self.shader_dir).join(name); + std::fs::read_to_string(path).map_err(|e| e.into()) + } + + pub fn watch_shaders(&self) -> Result<()> { + // TODO: Implement shader hot-reloading with notify crate + Ok(()) + } +} diff --git a/src/wayland.rs b/src/wayland.rs new file mode 100644 index 0000000..afb1486 --- /dev/null +++ b/src/wayland.rs @@ -0,0 +1,176 @@ +use anyhow::{Result, anyhow}; +use log::{info, warn}; +use std::process::Command; + +/// Hyprland-specific window configuration for better integration +pub struct HyprlandConfig { + pub floating: bool, + pub pin: bool, + pub no_focus: bool, +} + +impl Default for HyprlandConfig { + fn default() -> Self { + Self { + floating: true, + pin: true, + no_focus: true, + } + } +} + +/// Configure a window for optimal Hyprland wallpaper behavior +pub fn configure_hyprland_window( + _window: &winit::window::Window, + _config: &HyprlandConfig, +) -> Result<()> { + // On Wayland, we can set window properties for better integration + // For now, skip advanced Wayland protocol setup as it's not implemented + + // For Hyprland, we can use hyprctl to configure the window + // This requires the window to be created first, so we'll do this after creation + if let Ok(output) = Command::new("which").arg("hyprctl").output() { + if output.status.success() { + info!("Hyprland detected, will configure window properties"); + // We'll configure the window after it's mapped in the event loop + } + } + + Ok(()) +} + +/// Apply Hyprland-specific window rules after window creation +pub fn apply_hyprland_rules(window_id: u64, config: &HyprlandConfig) -> Result<()> { + #[cfg(target_os = "linux")] + { + use std::process::Command; + use std::thread; + use std::time::Duration; + + // Wait a bit for the window to be mapped + thread::sleep(Duration::from_millis(100)); + + // Use hyprctl to configure the window + let mut rules = Vec::new(); + + if config.floating { + rules.push("float"); + } + + if config.pin { + rules.push("pin"); + } + + if config.no_focus { + rules.push("nofocus"); + } + + if !rules.is_empty() { + let rules_str = rules.join(","); + let window_selector = format!("address:0x{:x}", window_id); + + let output = Command::new("hyprctl") + .args(&[ + "keyword", + "windowrule", + &format!("{},{}", rules_str, window_selector), + ]) + .output() + .map_err(|e| anyhow!("Failed to execute hyprctl: {}", e))?; + + if output.status.success() { + info!( + "Applied Hyprland rules: {} to window {}", + rules_str, window_selector + ); + } else { + let error_msg = String::from_utf8_lossy(&output.stderr); + warn!("Failed to apply Hyprland rules: {}", error_msg); + } + + // Also try to move it to the background layer + let workspace = format!("special:background,{}", window_selector); + let output = Command::new("hyprctl") + .args(&["dispatch", "movetoworkspace", &workspace]) + .output() + .map_err(|e| anyhow!("Failed to move window to background: {}", e))?; + + if output.status.success() { + info!("Moved wallpaper window to background workspace"); + } + } + } + + Ok(()) +} + +/// Get information about available outputs (monitors) +pub fn get_output_info() -> Result<Vec<OutputInfo>> { + #[cfg(target_os = "linux")] + { + use std::process::Command; + + let output = Command::new("hyprctl") + .args(&["monitors", "-j"]) + .output() + .map_err(|e| anyhow!("Failed to execute hyprctl: {}", e))?; + + if output.status.success() { + let output_str = String::from_utf8_lossy(&output.stdout); + let monitors: Vec<HyprlandMonitor> = serde_json::from_str(&output_str) + .map_err(|e| anyhow!("Failed to parse hyprctl output: {}", e))?; + + let output_info = monitors + .into_iter() + .map(|monitor| OutputInfo { + name: monitor.name, + width: monitor.width, + height: monitor.height, + x: monitor.x, + y: monitor.y, + }) + .collect(); + + Ok(output_info) + } else { + // Fallback to basic monitor detection + Ok(vec![OutputInfo::default()]) + } + } + + #[cfg(not(target_os = "linux"))] + { + Ok(vec![OutputInfo::default()]) + } +} + +#[derive(Debug, Clone)] +pub struct OutputInfo { + pub name: String, + pub width: i32, + pub height: i32, + pub x: i32, + pub y: i32, +} + +impl Default for OutputInfo { + fn default() -> Self { + Self { + name: "eDP-1".to_string(), + width: 1920, + height: 1080, + x: 0, + y: 0, + } + } +} + +#[cfg(target_os = "linux")] +#[derive(serde::Deserialize)] +struct HyprlandMonitor { + name: String, + width: i32, + height: i32, + x: i32, + y: i32, +} |
