diff --git a/justfile b/justfile index 9cdd45e..af4243e 100644 --- a/justfile +++ b/justfile @@ -50,8 +50,12 @@ build-vendored *args: vendor-extract (build-release '--frozen --offline' args) cargo-check *args: cargo check --all-features {{args}} -# Runs clippy +# Runs clippy (used in CI - default warnings only) clippy *args: + cargo clippy --all-features {{args}} -- -D warnings + +# Runs clippy with pedantic warnings (for development) +clippy-pedantic *args: cargo clippy --all-features {{args}} -- -W clippy::pedantic # Runs clippy with JSON message format @@ -69,8 +73,8 @@ fmt-check: test *args: cargo test {{args}} -# Run all checks (format, cargo check, test) -check: fmt-check cargo-check test +# Run all checks (format, clippy, cargo check, test) +check: fmt-check clippy cargo-check test # ============================================================================ # Development diff --git a/src/app/camera_preview/widget.rs b/src/app/camera_preview/widget.rs index f3e11a8..da73be9 100644 --- a/src/app/camera_preview/widget.rs +++ b/src/app/camera_preview/widget.rs @@ -103,14 +103,16 @@ impl AppModel { let video_elem = video_widget::video_widget( frame.clone(), - video_id, - content_fit, - filter_mode, - 0.0, - should_mirror, - crop_uv, - zoom_level, - scroll_zoom_enabled, + video_widget::VideoWidgetConfig { + video_id, + content_fit, + filter_type: filter_mode, + corner_radius: 0.0, + mirror_horizontal: should_mirror, + crop_uv, + zoom_level, + scroll_zoom_enabled, + }, ); widget::container(video_elem) diff --git a/src/app/filter_picker/view.rs b/src/app/filter_picker/view.rs index b2ac71c..0d1e49a 100644 --- a/src/app/filter_picker/view.rs +++ b/src/app/filter_picker/view.rs @@ -68,14 +68,16 @@ impl AppModel { // The video widget fills its container and handles aspect ratio via Cover mode video_widget::video_widget( Arc::clone(frame), - 99, // Shared source texture ID for all filter previews - VideoContentFit::Cover, - filter_type, - corner_radius, - self.config.mirror_preview, - None, // No aspect ratio cropping in filter previews - 1.0, // No zoom for filter previews - false, // No scroll zoom for filter previews + video_widget::VideoWidgetConfig { + video_id: 99, // Shared source texture ID for all filter previews + content_fit: VideoContentFit::Cover, + filter_type, + corner_radius, + mirror_horizontal: self.config.mirror_preview, + crop_uv: None, // No aspect ratio cropping in filter previews + zoom_level: 1.0, // No zoom for filter previews + scroll_zoom_enabled: false, // No scroll zoom for filter previews + }, ) } else { // Fallback: colored placeholder when no camera frame diff --git a/src/app/handlers/camera.rs b/src/app/handlers/camera.rs index f81a4da..f9275fc 100644 --- a/src/app/handlers/camera.rs +++ b/src/app/handlers/camera.rs @@ -207,7 +207,7 @@ impl AppModel { }, |(controls, settings, color_settings)| { cosmic::Action::App(Message::ExposureControlsQueried( - controls, + Box::new(controls), settings, color_settings, )) diff --git a/src/app/handlers/capture.rs b/src/app/handlers/capture.rs index d8a4be6..7b306bf 100644 --- a/src/app/handlers/capture.rs +++ b/src/app/handlers/capture.rs @@ -757,6 +757,7 @@ impl AppModel { async move { use crate::pipelines::video::{ AudioChannels, AudioQuality, EncoderConfig, VideoQuality, VideoRecorder, + VideoRecorderConfig, }; let config = EncoderConfig { @@ -768,21 +769,21 @@ impl AppModel { bitrate_override_kbps: Some(bitrate_kbps), }; - let recorder = match VideoRecorder::new( - &device_path, - metadata_path.as_deref(), + let recorder = match VideoRecorder::new(VideoRecorderConfig { + device_path: &device_path, + metadata_path: metadata_path.as_deref(), width, height, framerate, - &pixel_format, - output_path.clone(), - config, - audio_device.is_some(), - audio_device.as_deref(), - None, - selected_encoder.as_ref(), - sensor_rotation, - ) { + pixel_format: &pixel_format, + output_path: output_path.clone(), + encoder_config: config, + enable_audio: audio_device.is_some(), + audio_device: audio_device.as_deref(), + preview_sender: None, + encoder_info: selected_encoder.as_ref(), + rotation: sensor_rotation, + }) { Ok(r) => r, Err(e) => return Err(e), }; diff --git a/src/app/handlers/exposure.rs b/src/app/handlers/exposure.rs index fb0cff1..2f12e16 100644 --- a/src/app/handlers/exposure.rs +++ b/src/app/handlers/exposure.rs @@ -493,7 +493,7 @@ impl AppModel { pub(crate) fn handle_exposure_controls_queried( &mut self, - controls: AvailableExposureControls, + controls: Box, settings: ExposureSettings, color_settings: ColorSettings, ) -> Task> { @@ -505,7 +505,7 @@ impl AppModel { has_iso = controls.iso.available, "Exposure controls queried" ); - self.available_exposure_controls = controls; + self.available_exposure_controls = *controls; self.exposure_settings = Some(settings); self.color_settings = Some(color_settings); Task::none() @@ -562,7 +562,7 @@ impl AppModel { }, |(controls, settings, color_settings)| { cosmic::Action::App(Message::ExposureControlsQueried( - controls, + Box::new(controls), settings, color_settings, )) diff --git a/src/app/handlers/system.rs b/src/app/handlers/system.rs index 5e4fc7f..8c00b33 100644 --- a/src/app/handlers/system.rs +++ b/src/app/handlers/system.rs @@ -165,10 +165,10 @@ impl AppModel { "Toggled record audio" ); - if let Some(handler) = self.config_handler.as_ref() { - if let Err(err) = self.config.write_entry(handler) { - error!(?err, "Failed to save record audio setting"); - } + if let Some(handler) = self.config_handler.as_ref() + && let Err(err) = self.config.write_entry(handler) + { + error!(?err, "Failed to save record audio setting"); } Task::none() } @@ -185,10 +185,10 @@ impl AppModel { info!(?encoder, "Selected audio encoder"); self.config.audio_encoder = encoder; - if let Some(handler) = self.config_handler.as_ref() { - if let Err(err) = self.config.write_entry(handler) { - error!(?err, "Failed to save audio encoder selection"); - } + if let Some(handler) = self.config_handler.as_ref() + && let Err(err) = self.config.write_entry(handler) + { + error!(?err, "Failed to save audio encoder selection"); } } Task::none() diff --git a/src/app/state.rs b/src/app/state.rs index 1a08be3..153d8f9 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -972,7 +972,6 @@ pub enum ContextPage { /// - **Settings**: Configuration, audio/video encoder selection /// - **System**: Bug reports, recovery, external URLs #[derive(Debug, Clone)] -#[allow(clippy::large_enum_variant)] pub enum Message { // ===== UI Navigation ===== /// Open external URL (repository, etc.) @@ -1019,8 +1018,12 @@ pub enum Message { SetMeteringMode(MeteringMode), /// Toggle auto exposure priority (allow frame rate variation) ToggleAutoExposurePriority, - /// Exposure controls queried from camera - ExposureControlsQueried(AvailableExposureControls, ExposureSettings, ColorSettings), + /// Exposure controls queried from camera (boxed to reduce enum size) + ExposureControlsQueried( + Box, + ExposureSettings, + ColorSettings, + ), /// Exposure control change applied successfully ExposureControlApplied, /// White balance toggled, with optional temperature value when switching to manual diff --git a/src/app/video_widget.rs b/src/app/video_widget.rs index afe0147..6c96881 100644 --- a/src/app/video_widget.rs +++ b/src/app/video_widget.rs @@ -29,6 +29,27 @@ pub enum VideoContentFit { Cover, } +/// Configuration for creating a video widget +#[derive(Debug, Clone)] +pub struct VideoWidgetConfig { + /// Unique identifier for this video stream + pub video_id: u64, + /// How to scale content within bounds + pub content_fit: VideoContentFit, + /// Filter to apply to the video + pub filter_type: FilterType, + /// Corner radius for rounded corners (0.0 for sharp corners) + pub corner_radius: f32, + /// Whether to mirror the video horizontally + pub mirror_horizontal: bool, + /// Optional crop UV coordinates (u_min, v_min, u_max, v_max) in 0-1 range + pub crop_uv: Option<(f32, f32, f32, f32)>, + /// Zoom level (1.0 = no zoom, 2.0 = 2x zoom) + pub zoom_level: f32, + /// Whether scroll wheel zoom is enabled + pub scroll_zoom_enabled: bool, +} + /// Video widget that renders camera frames using a custom GPU primitive pub struct VideoWidget { primitive: VideoPrimitive, @@ -44,30 +65,18 @@ impl VideoWidget { /// Create a new video widget from a camera frame /// /// # Arguments - /// * `crop_uv` - Optional crop UV coordinates (u_min, v_min, u_max, v_max) in 0-1 range - /// * `zoom_level` - Zoom level (1.0 = no zoom, 2.0 = 2x zoom) - /// * `scroll_zoom_enabled` - Whether scroll wheel zoom is enabled - #[allow(clippy::too_many_arguments)] - pub fn new( - frame: Arc, - video_id: u64, - content_fit: VideoContentFit, - filter_type: FilterType, - corner_radius: f32, - mirror_horizontal: bool, - crop_uv: Option<(f32, f32, f32, f32)>, - zoom_level: f32, - scroll_zoom_enabled: bool, - ) -> Self { - let mut primitive = VideoPrimitive::new(video_id); - primitive.filter_type = filter_type; - primitive.corner_radius = corner_radius; - primitive.mirror_horizontal = mirror_horizontal; - primitive.crop_uv = crop_uv; - primitive.zoom_level = zoom_level; + /// * `frame` - The camera frame to display + /// * `config` - Widget configuration options + pub fn new(frame: Arc, config: VideoWidgetConfig) -> Self { + let mut primitive = VideoPrimitive::new(config.video_id); + primitive.filter_type = config.filter_type; + primitive.corner_radius = config.corner_radius; + primitive.mirror_horizontal = config.mirror_horizontal; + primitive.crop_uv = config.crop_uv; + primitive.zoom_level = config.zoom_level; // Calculate aspect ratio from frame dimensions, adjusted for crop - let aspect_ratio = if let Some((u_min, v_min, u_max, v_max)) = crop_uv { + let aspect_ratio = if let Some((u_min, v_min, u_max, v_max)) = config.crop_uv { // Use cropped region's aspect ratio let crop_width = (u_max - u_min) * frame.width as f32; let crop_height = (v_max - v_min) * frame.height as f32; @@ -104,7 +113,7 @@ impl VideoWidget { }; let video_frame = VideoFrame { - id: video_id, + id: config.video_id, width: frame.width, height: frame.height, data: frame.data.clone(), // Clone FrameData - just refcount increment, no data copy @@ -121,8 +130,8 @@ impl VideoWidget { width: Length::Fill, height: Length::Fill, aspect_ratio, - content_fit, - scroll_zoom_enabled, + content_fit: config.content_fit, + scroll_zoom_enabled: config.scroll_zoom_enabled, } } } @@ -241,30 +250,11 @@ impl<'a> From for Element<'a, crate::app::Message, Theme, Renderer> /// Create a video widget from a camera frame /// /// # Arguments -/// * `crop_uv` - Optional crop UV coordinates (u_min, v_min, u_max, v_max) in 0-1 range -/// * `zoom_level` - Zoom level (1.0 = no zoom, 2.0 = 2x zoom) -/// * `scroll_zoom_enabled` - Whether scroll wheel zoom is enabled -#[allow(clippy::too_many_arguments)] +/// * `frame` - The camera frame to display +/// * `config` - Widget configuration options pub fn video_widget<'a>( frame: Arc, - video_id: u64, - content_fit: VideoContentFit, - filter_type: FilterType, - corner_radius: f32, - mirror_horizontal: bool, - crop_uv: Option<(f32, f32, f32, f32)>, - zoom_level: f32, - scroll_zoom_enabled: bool, + config: VideoWidgetConfig, ) -> Element<'a, crate::app::Message, Theme, Renderer> { - Element::new(VideoWidget::new( - frame, - video_id, - content_fit, - filter_type, - corner_radius, - mirror_horizontal, - crop_uv, - zoom_level, - scroll_zoom_enabled, - )) + Element::new(VideoWidget::new(frame, config)) } diff --git a/src/cli.rs b/src/cli.rs index 9009434..87d3d1a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -12,7 +12,7 @@ use camera::backends::camera::pipewire::{ }; use camera::backends::camera::types::{CameraFormat, CameraFrame}; use camera::pipelines::photo::PhotoPipeline; -use camera::pipelines::video::{EncoderConfig, VideoRecorder}; +use camera::pipelines::video::{EncoderConfig, VideoRecorder, VideoRecorderConfig}; use chrono::Local; use futures::channel::mpsc; use std::path::{Path, PathBuf}; @@ -244,21 +244,21 @@ pub fn record_video( let encoder_config = EncoderConfig::default(); // Create video recorder - let recorder = VideoRecorder::new( - &camera.path, - camera.metadata_path.as_deref(), - format.width, - format.height, + let recorder = VideoRecorder::new(VideoRecorderConfig { + device_path: &camera.path, + metadata_path: camera.metadata_path.as_deref(), + width: format.width, + height: format.height, framerate, - &format.pixel_format, - output_path.clone(), + pixel_format: &format.pixel_format, + output_path: output_path.clone(), encoder_config, enable_audio, - None, // Use default audio device - None, // No preview sender needed for CLI - None, // Auto-select encoder - camera.rotation, - )?; + audio_device: None, // Use default audio device + preview_sender: None, // No preview sender needed for CLI + encoder_info: None, // Auto-select encoder + rotation: camera.rotation, + })?; // Start recording println!(); diff --git a/src/pipelines/photo/burst_mode/mod.rs b/src/pipelines/photo/burst_mode/mod.rs index 6634f5b..f4de9d6 100644 --- a/src/pipelines/photo/burst_mode/mod.rs +++ b/src/pipelines/photo/burst_mode/mod.rs @@ -1732,7 +1732,6 @@ impl BurstModeGpuPipeline { // Align using pre-allocated buffers let gpu_frame = self .align_single_frame_pooled( - &ref_rgba_buffer, &ref_pyramids, frame, width, @@ -1906,7 +1905,6 @@ impl BurstModeGpuPipeline { #[allow(clippy::too_many_arguments)] async fn align_single_frame_pooled( &self, - _ref_rgba_buffer: &wgpu::Buffer, ref_pyramids: &ReferencePyramids, comparison: &CameraFrame, width: u32, diff --git a/src/pipelines/photo/encoding.rs b/src/pipelines/photo/encoding.rs index d40af5f..f09e31a 100644 --- a/src/pipelines/photo/encoding.rs +++ b/src/pipelines/photo/encoding.rs @@ -9,7 +9,7 @@ //! All encoding operations run asynchronously to avoid blocking. use super::processing::ProcessedImage; -use image::{ImageFormat, RgbImage}; +use image::RgbImage; use std::path::PathBuf; use tracing::{debug, error, info}; @@ -33,16 +33,6 @@ impl EncodingFormat { EncodingFormat::Dng => "dng", } } - - /// Convert to image crate's ImageFormat (returns None for DNG) - #[allow(clippy::wrong_self_convention)] - fn to_image_format(&self) -> Option { - match self { - EncodingFormat::Jpeg => Some(ImageFormat::Jpeg), - EncodingFormat::Png => Some(ImageFormat::Png), - EncodingFormat::Dng => None, // DNG uses separate encoding - } - } } impl From for EncodingFormat { diff --git a/src/pipelines/video/mod.rs b/src/pipelines/video/mod.rs index 9044376..5f4dfde 100644 --- a/src/pipelines/video/mod.rs +++ b/src/pipelines/video/mod.rs @@ -14,7 +14,7 @@ pub mod recorder; // Re-export commonly used types pub use encoder_selection::EncoderConfig; -pub use recorder::{VideoRecorder, check_available_encoders}; +pub use recorder::{VideoRecorder, VideoRecorderConfig, check_available_encoders}; // Re-export encoder types for convenience pub use crate::media::encoders::{AudioChannels, AudioQuality, VideoQuality}; diff --git a/src/pipelines/video/recorder.rs b/src/pipelines/video/recorder.rs index 63c6482..90c21e0 100644 --- a/src/pipelines/video/recorder.rs +++ b/src/pipelines/video/recorder.rs @@ -18,6 +18,36 @@ use std::path::PathBuf; use std::sync::Arc; use tracing::{debug, error, info, warn}; +/// Configuration for creating a video recorder +pub struct VideoRecorderConfig<'a> { + /// Camera device path + pub device_path: &'a str, + /// Optional metadata path for PipeWire + pub metadata_path: Option<&'a str>, + /// Video width + pub width: u32, + /// Video height + pub height: u32, + /// Video framerate + pub framerate: u32, + /// Pixel format (e.g., "NV12", "MJPEG") + pub pixel_format: &'a str, + /// Output file path + pub output_path: PathBuf, + /// Encoder configuration + pub encoder_config: EncoderConfig, + /// Whether to record audio + pub enable_audio: bool, + /// Optional audio device path + pub audio_device: Option<&'a str>, + /// Optional preview frame sender + pub preview_sender: Option>, + /// Specific encoder info (if None, auto-select) + pub encoder_info: Option<&'a crate::media::encoders::video::EncoderInfo>, + /// Sensor rotation to correct video orientation + pub rotation: SensorRotation, +} + /// Video recorder using the new pipeline architecture #[derive(Debug)] pub struct VideoRecorder { @@ -31,38 +61,28 @@ impl VideoRecorder { /// Create a new video recorder with intelligent encoder selection /// /// # Arguments - /// * `device_path` - Camera device path - /// * `metadata_path` - Optional metadata path for PipeWire - /// * `width` - Video width - /// * `height` - Video height - /// * `framerate` - Video framerate - /// * `pixel_format` - Pixel format (e.g., "NV12", "MJPEG") - /// * `output_path` - Output file path - /// * `config` - Encoder configuration - /// * `enable_audio` - Whether to record audio - /// * `audio_device` - Optional audio device path - /// * `preview_sender` - Optional preview frame sender - /// * `rotation` - Sensor rotation to correct video orientation + /// * `config` - Video recorder configuration /// /// # Returns /// * `Ok(VideoRecorder)` - Video recorder instance /// * `Err(String)` - Error message - #[allow(clippy::too_many_arguments)] - pub fn new( - device_path: &str, - metadata_path: Option<&str>, - width: u32, - height: u32, - framerate: u32, - pixel_format: &str, - output_path: PathBuf, - config: EncoderConfig, - enable_audio: bool, - audio_device: Option<&str>, - preview_sender: Option>, - encoder_info: Option<&crate::media::encoders::video::EncoderInfo>, - rotation: SensorRotation, - ) -> Result { + pub fn new(config: VideoRecorderConfig<'_>) -> Result { + let VideoRecorderConfig { + device_path, + metadata_path, + width, + height, + framerate, + pixel_format, + output_path, + encoder_config, + enable_audio, + audio_device, + preview_sender, + encoder_info, + rotation, + } = config; + info!( device = %device_path, metadata = ?metadata_path, @@ -80,9 +100,13 @@ impl VideoRecorder { // Select encoders (use specific encoder if provided, otherwise auto-select) let encoders = if let Some(enc_info) = encoder_info { - super::encoder_selection::select_encoders_with_video(&config, enc_info, enable_audio)? + super::encoder_selection::select_encoders_with_video( + &encoder_config, + enc_info, + enable_audio, + )? } else { - select_encoders(&config, enable_audio)? + select_encoders(&encoder_config, enable_audio)? }; info!( diff --git a/src/storage.rs b/src/storage.rs index f90702a..e47484e 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -27,12 +27,11 @@ pub async fn load_latest_thumbnail( let path = entry.path(); if let Some(ext) = path.extension() { let ext_str = ext.to_string_lossy().to_lowercase(); - if file_formats::is_image_extension(&ext_str) { - if let Ok(metadata) = entry.metadata() { - if let Ok(modified) = metadata.modified() { - files.push((path, modified)); - } - } + if file_formats::is_image_extension(&ext_str) + && let Ok(metadata) = entry.metadata() + && let Ok(modified) = metadata.modified() + { + files.push((path, modified)); } } } @@ -44,12 +43,11 @@ pub async fn load_latest_thumbnail( let path = entry.path(); if let Some(ext) = path.extension() { let ext_str = ext.to_string_lossy().to_lowercase(); - if file_formats::is_video_extension(&ext_str) { - if let Ok(metadata) = entry.metadata() { - if let Ok(modified) = metadata.modified() { - files.push((path, modified)); - } - } + if file_formats::is_video_extension(&ext_str) + && let Ok(metadata) = entry.metadata() + && let Ok(modified) = metadata.modified() + { + files.push((path, modified)); } } }