这是indexloc提供的服务,不要输入任何密码
Skip to content

Conversation

@plyght
Copy link
Owner

@plyght plyght commented Jul 24, 2025

Summary by cubic

Added event-driven tiling and snapping for windows on macOS, including live drag and resize detection, improved layout algorithms, and a new snapping engine for better window management.

  • New Features

    • Real-time window drag and resize detection using macOS accessibility and notification APIs.
    • Snap zones and snapping logic for intuitive window placement.
    • Enhanced tiling layouts with more accurate window positioning and focus handling.
  • Refactors

    • Major updates to layout, window management, and macOS integration code for better performance and reliability.

Summary by CodeRabbit

  • New Features

    • Introduced advanced window snapping and swapping when dragging or resizing windows, including edge, corner, and center zones.
    • Added real-time detection of window drag and resize events for seamless user interaction.
    • Implemented bulk window movement and improved workspace detection for multi-display setups.
    • Added dual logging to both console and file for enhanced troubleshooting.
    • Included a comprehensive project overview and new documentation.
    • Added macOS window drag and resize notification observer for enhanced event tracking.
  • Improvements

    • Enhanced macOS window management reliability and performance, including better window identification, cache handling, and workspace awareness.
    • Improved layout algorithms for more consistent window placement and robust handling of window addition/removal.
    • Refined hotkey validation and style consistency throughout the interface.
    • Expanded window metadata extraction for better workspace and process awareness.
    • Updated snapping logic with configurable zones and refined drag state management.
    • Improved window manager responsiveness with throttled layout application and drag event integration.
  • Bug Fixes

    • Fixed issues with window filtering, workspace assignment, and window rectangle validation to prevent layout errors.
  • Documentation

    • Updated and corrected README and added detailed architecture and development workflow documentation.
  • Chores

    • Updated dependencies for improved compatibility and stability.

@coderabbitai
Copy link

coderabbitai bot commented Jul 24, 2025

Walkthrough

This update introduces comprehensive enhancements to the macOS tiling window manager. Major changes include a robust window drag and snapping system, workspace detection, improved accessibility and Core Graphics integration, and a dual logger. New modules for snapping logic and window notifications are added, and significant refactoring occurs in layout, window management, and macOS-specific backends. Documentation and code style are also updated.

Changes

File(s) / Module(s) Change Summary
Cargo.toml Updated objc dependency from 0.2 to 0.2.7.
README.md Corrected project references and punctuation.
llms.txt Added comprehensive project overview and contributor guide.
src/bin/skew-cli.rs Removed unused use tokio; import.
src/config.rs Added position_threshold and size_threshold fields to GeneralConfig with defaults; refactored hotkey validation to use .is_empty() instead of len() < 1.
src/focus.rs Simplified focus cycling logic for backward navigation.
src/hotkeys.rs Code formatting and style improvements; no logic changes.
src/ipc.rs Replaced .get(0) with .first() for argument retrieval in command processing.
src/layout.rs Major BSP layout refactor: spiral insertion, window removal with tree pruning, rectangle validation, and robust layout computation. Added/modified public methods and traits.
src/lib.rs Added snap module; extended WindowId derives to include ordering traits.
src/macos/accessibility.rs Major overhaul: stable window ID generation, robust cache management, batch window moves, PID-aware enumeration, Core Foundation/Accessibility API integration, and improved error handling/logging. Many new public/private methods added.
src/macos/cgwindow.rs Refactored and enhanced window metadata extraction, filtering, and logging; added owner PID and dynamic workspace ID; implemented Default for WindowCache.
src/macos/mod.rs Added public window_notifications module.
src/macos/window_notifications.rs New module: implements macOS observer for window drag/resize events using Objective-C runtime, with async event delivery. Defines public observer struct and event enum.
src/macos/window_system.rs Added Core Graphics workspace detection, refactored display enumeration, improved window scanning/logging, added async multi-window move and workspace query methods.
src/main.rs Replaced env_logger with custom DualLogger for simultaneous console and file logging, with timestamps and formatting.
src/plugins.rs Added #[allow(dead_code)] to suppress warnings for unused struct fields.
src/snap.rs New module: implements window snapping system with zones, drag state tracking, zone detection, snapping/swapping logic, and public API for snap management.
src/window_manager.rs Major extension: integrates drag/resize observer and snap manager, tracks user/programmatic moves, implements snapping/swapping/restore logic, workspace detection, layout application, and event handling. Many new public/private fields and methods.
build.rs Added new build script emitting cargo configuration flags for Clippy linting detection.

Sequence Diagram(s)

Window Drag and Snap Handling

sequenceDiagram
    participant User
    participant WindowManager
    participant WindowDragNotificationObserver
    participant SnapManager
    participant AccessibilityManager

    User->>Window: Drag window begins
    Window-->>WindowDragNotificationObserver: NSWindowWillMoveNotification
    WindowDragNotificationObserver-->>WindowManager: DragStarted event

    User->>Window: Drag window ends
    Window-->>WindowDragNotificationObserver: NSWindowDidMoveNotification
    WindowDragNotificationObserver-->>WindowManager: DragEnded event
    WindowManager->>SnapManager: end_window_drag()
    alt Snap zone detected
        SnapManager-->>WindowManager: SnapToZone or SwapWithWindow
        WindowManager->>AccessibilityManager: move_window(s)
    else No snap zone
        SnapManager-->>WindowManager: ReturnToOriginal
        WindowManager->>AccessibilityManager: move_window
    end
Loading

Layout Application and Window Movement

sequenceDiagram
    participant WindowManager
    participant LayoutManager
    participant AccessibilityManager

    WindowManager->>LayoutManager: compute_layout()
    LayoutManager-->>WindowManager: window_rects
    WindowManager->>AccessibilityManager: move_all_windows(window_rects)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90 minutes

Poem

Skew’s windows now snap and dance,
With drag and drop, they find their chance.
Logs are written, clear and bright,
As layouts spiral left and right.
Mac’s APIs, we now embrace—
Each window finds its perfect place!
—🐇✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch tiling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (18)
README.md (1)

234-234: Minor wording nit – consider hyphenating “i3-compatible”.

Purely stylistic, but “i3-compatible” is the more common form.

- A Wayland compositor and i3 compatible window manager for Linux
+ A Wayland compositor and i3-compatible window manager for Linux
src/plugins.rs (1)

34-37: Avoid suppressing dead_code; mark unused fields explicitly.

Using #[allow(dead_code)] at field level hides potentially useful warnings project-wide.
If the fields are intentionally unused (for now), prefix them with an underscore instead; that keeps the intent clear without a blanket allow.

-    #[allow(dead_code)]
-    name: String,
-    #[allow(dead_code)]
-    script_path: PathBuf,
+    _name: String,
+    _script_path: PathBuf,
src/macos/mod.rs (1)

3-3: Export order nit.

Very minor: keeping the module list alphabetically sorted (accessibility, cgwindow, window_notifications, window_system) is helpful for quick scans in large crates.
No action needed if you don’t enforce alphabetical ordering elsewhere.

src/config.rs (1)

308-309: Good idiomatic change; one edge-case still slips through.

parts.is_empty() is clearer than len() < 1 – nice.
However, a combo like "alt+" yields ["alt", ""]; it passes this check but contains an empty key segment.

Consider additionally validating that the final part (actual key) is non-empty:

- if parts.is_empty() {
+ if parts.is_empty() || parts.last().unwrap().is_empty() {

That keeps malformed combos from silently entering the config.

src/main.rs (2)

33-82: Well-implemented dual logging system with room for optimization.

The DualLogger implementation provides comprehensive logging with timestamps and dual output. The design is solid with proper thread safety and buffering.

Consider these potential optimizations:

  • Flushing after every log entry (line 72) may impact performance under high log volume. Consider batched flushing or configurable flush intervals.
  • Silent error handling for file writes could mask storage issues. Consider logging write failures to stderr.

97-101: Consider using a standard log directory location.

The log file is created in the current working directory. Consider using a standard location like ~/.local/share/skew/ or /tmp/ for better system integration.

-    let log_file = std::env::current_dir()
-        .unwrap_or_else(|_| PathBuf::from("."))
-        .join("skew.log");
+    let log_file = PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()))
+        .join(".local/share/skew")
+        .join("skew.log");
+    
+    // Ensure directory exists
+    if let Some(parent) = log_file.parent() {
+        std::fs::create_dir_all(parent)?;
+    }
src/macos/window_system.rs (1)

124-128: Address the TODO comment for configurable window monitoring interval.

The TODO indicates this should be configurable via skew.toml. This would allow users to balance between responsiveness and CPU usage based on their needs.

Would you like me to implement the configuration option for window_monitor_interval_ms in the config system and open an issue to track this enhancement?

src/macos/window_notifications.rs (1)

422-439: Window owner PID extraction may fail silently.

The method attempts to get PID through window.app.runningApplication, but this chain may be nil at any point. The current implementation returns None gracefully, but consider logging when PID extraction fails for debugging purposes.

Add debug logging when PID extraction fails to help diagnose window identification issues.

src/layout.rs (1)

377-398: Consider optimizing BSP tree rebuilding for better performance.

Always rebuilding the entire tree from scratch ensures consistency but may impact performance with many windows. Consider implementing incremental updates in the future.

For now, this approach is fine for typical window counts. Monitor performance and consider optimization if users report lag with 10+ windows.

src/snap.rs (2)

58-59: Remove unnecessary #[allow(dead_code)] attribute.

The window_id field is used in debug logging at line 289, so the dead_code attribute is not needed.

-    #[allow(dead_code)]
     window_id: WindowId,

275-279: Update method doesn't track current position during drag.

The update_window_drag method receives current_rect but doesn't use it. Consider tracking the current position for more accurate drag handling.

-    pub fn update_window_drag(&mut self, window_id: WindowId, _current_rect: Rect) {
+    pub fn update_window_drag(&mut self, window_id: WindowId, current_rect: Rect) {
         if let Some(drag_state) = self.window_drag_states.get_mut(&window_id) {
             drag_state.is_dragging = true;
+            // Optionally track current position for drag visualization or analytics
+            // drag_state.current_rect = current_rect;
         }
     }
src/window_manager.rs (5)

159-165: Track the TODO for configurable intervals.

The TODO comment suggests making refresh and monitor intervals configurable, which would be a valuable enhancement for performance tuning.

Would you like me to create an issue to track this configuration enhancement?


603-603: Document or make the resize delay configurable.

The 10ms delay seems arbitrary. Consider documenting why this delay is needed or making it configurable.

-            // Use a small delay to ensure the resize has completed
-            tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
+            // Small delay to ensure macOS has completed the resize animation
+            // before we snap the window back to layout position
+            tokio::time::sleep(tokio::time::Duration::from_millis(
+                self.config.snap.resize_settle_delay_ms.unwrap_or(10)
+            )).await;

643-643: Very low movement threshold may cause performance issues.

The 0.5px threshold for detecting movement is extremely sensitive and might trigger too frequently, potentially causing performance issues or unwanted snapping behavior.

-            // Very low threshold to catch even small movements (>0.5px)
-            if distance > 0.5 {
+            // Reasonable threshold to detect intentional movements (>2.0px)
+            if distance > self.config.snap.movement_threshold.unwrap_or(2.0) {

885-1066: Consider extracting common swap logic to reduce duplication.

Both swap_windows and swap_windows_with_rects have nearly identical bulk move and fallback logic. Consider extracting this to a shared helper method.

+    async fn execute_window_swap(
+        &mut self,
+        swap_layouts: HashMap<WindowId, Rect>,
+        windows: Vec<Window>,
+    ) -> Result<()> {
+        // Mark all as programmatic moves
+        for window_id in swap_layouts.keys() {
+            self.programmatically_moving.insert(*window_id);
+        }
+
+        // Try bulk move first
+        match self.macos.move_all_windows(&swap_layouts, &windows).await {
+            Ok(_) => {
+                debug!("✅ Successfully swapped windows using bulk move");
+                // Update internal state
+                for (window_id, rect) in &swap_layouts {
+                    if let Some(w) = self.windows.get_mut(window_id) {
+                        w.rect = *rect;
+                    }
+                    self.last_window_positions.insert(*window_id, *rect);
+                }
+            }
+            Err(e) => {
+                warn!("Bulk swap failed, trying individual moves: {}", e);
+                // Fallback to individual moves
+                for (window_id, rect) in &swap_layouts {
+                    match self.macos.move_window(*window_id, *rect).await {
+                        Ok(_) => {
+                            if let Some(w) = self.windows.get_mut(window_id) {
+                                w.rect = *rect;
+                            }
+                            self.last_window_positions.insert(*window_id, *rect);
+                        }
+                        Err(e) => {
+                            warn!("Failed to move window {:?} during swap: {}", window_id, e)
+                        }
+                    }
+                }
+            }
+        }
+        Ok(())
+    }

1315-1315: Consider making batch size configurable.

The hardcoded BATCH_SIZE of 2 could be made configurable to allow tuning based on system performance.

-        const BATCH_SIZE: usize = 2; // Move 2 windows at a time
+        let batch_size = self.config.layout.batch_size.unwrap_or(2);
src/macos/accessibility.rs (2)

181-183: Consider selective cache refresh for better performance.

Refreshing the entire windows cache on every move operation could be expensive. Consider a more targeted refresh approach.

-        // Force refresh the cache to ensure we have current windows
-        if let Err(e) = self.refresh_all_windows_cache() {
-            warn!("Failed to refresh all windows cache: {}", e);
-        }
+        // Try to find in cache first, refresh only if needed
+        let needs_refresh = !self.window_cache.contains_key(&window_id) ||
+                           self.last_cache_update.elapsed() > std::time::Duration::from_secs(1);
+        
+        if needs_refresh {
+            if let Err(e) = self.refresh_all_windows_cache() {
+                warn!("Failed to refresh all windows cache: {}", e);
+            }
+        }

447-447: Document the rationale for 16K PID limit.

The cap of 16,384 PIDs seems reasonable but should be documented or made configurable for systems with many processes.

-                // Limit buffer growth to prevent excessive memory usage
-                let new_size = std::cmp::min(buffer.len() * 2, 16384); // Cap at 16K PIDs
+                // Limit buffer growth to prevent excessive memory usage
+                // 16K PIDs should be sufficient for most systems (typical: <1000 processes)
+                const MAX_PIDS: usize = 16384;
+                let new_size = std::cmp::min(buffer.len() * 2, MAX_PIDS);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 420dfb7 and a47f226.

⛔ Files ignored due to path filters (15)
  • final_improved_test.log is excluded by !**/*.log
  • improved_fixes_test.log is excluded by !**/*.log
  • manual_drag_test.log is excluded by !**/*.log
  • skew.log is excluded by !**/*.log
  • skew_debug_test.log is excluded by !**/*.log
  • skew_final_snapping_test.log is excluded by !**/*.log
  • skew_final_test.log is excluded by !**/*.log
  • skew_layout_snap_test.log is excluded by !**/*.log
  • skew_manual_detection_test.log is excluded by !**/*.log
  • skew_manual_test.log is excluded by !**/*.log
  • skew_position_fix_test.log is excluded by !**/*.log
  • skew_test.log is excluded by !**/*.log
  • test_complete_fix.log is excluded by !**/*.log
  • test_run.log is excluded by !**/*.log
  • test_run_fixed.log is excluded by !**/*.log
📒 Files selected for processing (19)
  • Cargo.toml (1 hunks)
  • README.md (2 hunks)
  • llms.txt (1 hunks)
  • src/bin/skew-cli.rs (0 hunks)
  • src/config.rs (1 hunks)
  • src/focus.rs (1 hunks)
  • src/hotkeys.rs (8 hunks)
  • src/ipc.rs (2 hunks)
  • src/layout.rs (15 hunks)
  • src/lib.rs (1 hunks)
  • src/macos/accessibility.rs (9 hunks)
  • src/macos/cgwindow.rs (8 hunks)
  • src/macos/mod.rs (1 hunks)
  • src/macos/window_notifications.rs (1 hunks)
  • src/macos/window_system.rs (7 hunks)
  • src/main.rs (2 hunks)
  • src/plugins.rs (1 hunks)
  • src/snap.rs (1 hunks)
  • src/window_manager.rs (14 hunks)
💤 Files with no reviewable changes (1)
  • src/bin/skew-cli.rs
🧰 Additional context used
🪛 LanguageTool
llms.txt

[style] ~9-~9: This phrase is redundant (‘I’ stands for ‘Interface’). Use simply “CLIInterface”.
Context: ...ssibility APIs. ## Key Components - CLI Interface (main.rs): Command-line entry point...

(ACRONYM_TAUTOLOGY)

🔇 Additional comments (20)
Cargo.toml (1)

26-26: Dependency bump looks good.

objc 0.2.7 is still within the semver-compatible 0.2 line, so no API breakage is expected.
Nothing else to flag here.

README.md (1)

3-3: Doc tweak LGTM.

Switching to “swaywm” clarifies the correct project name. 👍

src/focus.rs (1)

341-344: LGTM! Clean refactor of backward cycling logic.

The simplification removes redundant conditional checks while maintaining the correct behavior. The explicit handling of the zero index case makes the logic clearer and more readable.

src/lib.rs (2)

8-8: LGTM! New snap module addition.

Adding the snap module exposes the new window snapping management system that's part of the tiling enhancements.


16-16: Good addition of ordering traits to WindowId.

Adding PartialOrd and Ord traits enables ordering comparisons on WindowId instances, which supports consistent window identification and sorting operations across the window management system.

src/ipc.rs (1)

153-153: LGTM! More idiomatic argument access.

Replacing message.args.get(0) with message.args.first() is more idiomatic and readable while maintaining the same functionality and error handling behavior.

Also applies to: 173-173

llms.txt (1)

1-81: Excellent comprehensive project documentation.

This documentation provides a thorough overview of the Skew project architecture, components, dependencies, and development workflow. It will be valuable for both contributors and users understanding the system.

src/hotkeys.rs (1)

58-467: Formatting improvements enhance code readability.

The formatting changes throughout this file improve consistency and readability without altering functionality. The multi-line expression reformatting and simplified error handling make the code easier to follow.

src/macos/window_system.rs (4)

11-15: Use of private macOS APIs may cause compatibility issues.

The CGSGetActiveSpace and CGSMainConnectionID functions are private Apple APIs that could change or be removed in future macOS versions, potentially breaking this functionality.

Consider documenting this dependency clearly and implementing a fallback mechanism if these APIs become unavailable. Also, check if there are any public APIs that could provide similar functionality.


315-352: Unsafe workspace detection implementation looks correct with proper validation.

The unsafe block properly validates the connection ID and workspace ID, with reasonable bounds checking and fallback behavior. The safety comments clearly document the invariants.


82-82: Iterator usage improves code clarity.

Using iter().enumerate().take() is more idiomatic and safer than manual indexing.


299-305: New batch window movement method follows good design patterns.

The delegation to accessibility.move_all_windows maintains proper separation of concerns.

src/macos/cgwindow.rs (3)

105-108: Enhanced window metadata extraction improves window management capabilities.

Extracting owner_pid and workspace_id enables better window identification and workspace-aware operations. Good addition for the tiling window manager functionality.

Also applies to: 124-125


321-325: Default trait implementation follows Rust best practices.

Delegating to new() in the Default implementation is the standard pattern.


147-159: System application exclusion list verified

Verified that the entries “Menubar”, “Menu Bar”, and “SystemPreferences” only appear in the never_tile_apps array in src/macos/cgwindow.rs, and that the contains(&owner.as_str()) check uses exact string equality—so no other apps (e.g. “Menu Bar Helper” or similar) will be inadvertently filtered. No changes required.

src/macos/window_notifications.rs (1)

69-80: Raw pointer storage requires careful memory management.

The instance variable storage using Box::into_raw is correctly implemented, with corresponding cleanup in stop_observing. This pattern safely bridges Rust ownership with Objective-C runtime.

src/layout.rs (4)

87-139: Improved split calculation with better space distribution.

The enhanced update_child_rects method ensures minimum sizes are respected while distributing space more fairly. The 30%/70% limits prevent extreme splits that would create unusable panes.


141-181: Spiral insertion pattern creates predictable window placement.

The depth-based alternating split direction (horizontal at even depths, vertical at odd) creates an intuitive spiral pattern similar to i3/sway. This is a good UX improvement.


292-304: Window size constraints prevent edge cases.

The validation ensures windows have reasonable dimensions (150x100 to 5000x4000). These limits accommodate both small utility windows and large displays.


483-483: Using div_ceil improves code clarity.

The div_ceil method is more explicit than manual ceiling division calculation.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (9)
src/main.rs (2)

84-92: Make log level configurable instead of hardcoding Debug.

The current implementation always sets the log level to Debug, which may produce excessive logs in production environments.

Consider making the log level configurable:

-fn setup_dual_logging(log_file: &PathBuf) -> Result<()> {
+fn setup_dual_logging(log_file: &PathBuf, log_level: log::LevelFilter) -> Result<()> {
     let dual_logger = DualLogger::new(log_file)?;
 
     log::set_boxed_logger(Box::new(dual_logger))
-        .map(|()| log::set_max_level(log::LevelFilter::Debug))
+        .map(|()| log::set_max_level(log_level))
         .map_err(|e| anyhow::anyhow!("Failed to init dual logger: {}", e))?;
 
     Ok(())
 }

Then in main, determine the log level from environment variable or CLI args:

let log_level = std::env::var("RUST_LOG")
    .ok()
    .and_then(|s| s.parse().ok())
    .unwrap_or(log::LevelFilter::Info);
setup_dual_logging(&log_file, log_level)?;

97-101: Consider more robust log file location handling.

The current implementation writes logs to the current working directory, which may not be writable or appropriate in all environments (e.g., system services, read-only filesystems).

Consider using a more appropriate default location:

-    let log_file = std::env::current_dir()
-        .unwrap_or_else(|_| PathBuf::from("."))
-        .join("skew.log");
+    let log_file = if let Ok(log_dir) = std::env::var("LOG_DIR") {
+        PathBuf::from(log_dir).join("skew.log")
+    } else {
+        // Try standard locations in order
+        let candidates = [
+            dirs::cache_dir().map(|p| p.join("skew")),
+            dirs::home_dir().map(|p| p.join(".local/share/skew")),
+            Some(PathBuf::from("/tmp")),
+        ];
+        
+        candidates.into_iter()
+            .flatten()
+            .find(|p| p.exists() || std::fs::create_dir_all(p).is_ok())
+            .unwrap_or_else(|| PathBuf::from("."))
+            .join("skew.log")
+    };
src/layout.rs (2)

87-87: Complex minimum size enforcement could be simplified.

The current logic for enforcing minimum split sizes has magic numbers and complex conditional logic that could be hard to maintain.

Consider extracting constants and simplifying the logic:

+    const MIN_SPLIT_SIZE: f64 = 150.0;
+    const MIN_SPLIT_RATIO: f64 = 0.3;
+    const MAX_SPLIT_RATIO: f64 = 0.7;
+
     fn update_child_rects(&mut self) {
         if let (Some(ref mut left), Some(ref mut right)) = (&mut self.left, &mut self.right) {
-            const MIN_SPLIT_SIZE: f64 = 150.0; // Reduced minimum for better space usage
-
             let (left_rect, right_rect) = if self.is_horizontal {
-                let ideal_left_width = self.rect.width * self.split_ratio;
-                let ideal_right_width = self.rect.width - ideal_left_width;
-
-                // Ensure both sides meet minimum requirements with better distribution
-                let left_width = if ideal_left_width < MIN_SPLIT_SIZE {
-                    MIN_SPLIT_SIZE.min(self.rect.width * 0.3) // Max 30% for minimum
-                } else if ideal_right_width < MIN_SPLIT_SIZE {
-                    (self.rect.width - MIN_SPLIT_SIZE).max(self.rect.width * 0.7)
-                // Min 70% for main
-                } else {
-                    ideal_left_width
-                };
+                let left_width = self.calculate_split_dimension(
+                    self.rect.width,
+                    self.split_ratio
+                );
                 let right_width = self.rect.width - left_width;

With a helper method:

fn calculate_split_dimension(&self, total: f64, ratio: f64) -> f64 {
    let ideal_left = total * ratio;
    let ideal_right = total - ideal_left;
    
    if ideal_left < Self::MIN_SPLIT_SIZE && ideal_right < Self::MIN_SPLIT_SIZE {
        // Both would be too small, give equal space
        total / 2.0
    } else if ideal_left < Self::MIN_SPLIT_SIZE {
        Self::MIN_SPLIT_SIZE.min(total * Self::MIN_SPLIT_RATIO)
    } else if ideal_right < Self::MIN_SPLIT_SIZE {
        (total - Self::MIN_SPLIT_SIZE).max(total * Self::MAX_SPLIT_RATIO)
    } else {
        ideal_left
    }
}

Also applies to: 94-101, 118-125


379-393: Consider optimizing BSP tree rebuilding for better performance.

The current implementation always rebuilds the entire BSP tree on every layout computation, which could be inefficient for large numbers of windows.

While the comment explains this avoids synchronization issues, consider these alternatives:

  1. Incremental updates: Track window additions/removals and only rebuild when the window set changes
  2. Dirty flag: Set a flag when windows are added/removed, rebuild only when dirty
  3. Immutable tree: Use persistent data structures for lock-free updates

Example approach:

 pub struct LayoutManager {
     current_layout: LayoutType,
     bsp_root: Option<BSPNode>,
     split_ratio: f64,
+    bsp_dirty: bool,
+    last_window_ids: Vec<WindowId>,
 }

 fn compute_bsp_layout(...) -> HashMap<WindowId, Rect> {
+    let current_ids: Vec<_> = windows.iter().map(|w| w.id).collect();
+    if !self.bsp_dirty && self.last_window_ids == current_ids {
+        // Tree is up to date, just collect rects
+        if let Some(ref root) = self.bsp_root {
+            return root.collect_window_rects(general_config.gap);
+        }
+    }
     
     // Rebuild tree...
+    self.bsp_dirty = false;
+    self.last_window_ids = current_ids;
src/snap.rs (1)

541-565: Window overlap detection only considers center point.

The current implementation only checks if the dragged window's center point is within another window, which might miss cases where windows significantly overlap but centers don't align.

Consider using area overlap for more accurate detection:

 pub fn find_window_under_drag(
     &self,
     dragged_window_id: WindowId,
     dragged_rect: Rect,
     all_windows: &[&Window],
 ) -> Option<WindowId> {
-    let center_x = dragged_rect.x + dragged_rect.width / 2.0;
-    let center_y = dragged_rect.y + dragged_rect.height / 2.0;
-
-    for window in all_windows {
-        if window.id == dragged_window_id {
-            continue;
-        }
-
-        if self.point_in_rect(center_x, center_y, &window.rect) {
-            debug!(
-                "Found window {:?} under dragged window {:?}",
-                window.id, dragged_window_id
-            );
-            return Some(window.id);
-        }
-    }
+    let mut best_overlap = 0.0;
+    let mut best_window = None;
+    
+    for window in all_windows {
+        if window.id == dragged_window_id {
+            continue;
+        }
+        
+        let overlap = self.calculate_rect_overlap(&dragged_rect, &window.rect);
+        if overlap > best_overlap && overlap > 0.25 { // At least 25% overlap
+            best_overlap = overlap;
+            best_window = Some(window.id);
+        }
+    }
+    
+    if let Some(id) = best_window {
+        debug!(
+            "Found window {:?} under dragged window {:?} with {:.1}% overlap",
+            id, dragged_window_id, best_overlap * 100.0
+        );
+    }
+    best_window
 }
+
+fn calculate_rect_overlap(&self, rect1: &Rect, rect2: &Rect) -> f64 {
+    let x_overlap = (rect1.x + rect1.width).min(rect2.x + rect2.width) - rect1.x.max(rect2.x);
+    let y_overlap = (rect1.y + rect1.height).min(rect2.y + rect2.height) - rect1.y.max(rect2.y);
+    
+    if x_overlap > 0.0 && y_overlap > 0.0 {
+        let overlap_area = x_overlap * y_overlap;
+        let dragged_area = rect1.width * rect1.height;
+        overlap_area / dragged_area
+    } else {
+        0.0
+    }
+}
src/window_manager.rs (4)

82-89: Add missing imports for collections types.

The code uses std::collections::HashSet and std::collections::HashMap with fully qualified paths. Add these imports at the top of the file for better readability.

Add these imports at the top of the file:

use std::collections::{HashMap, HashSet};

Then simplify the field declarations:

-    programmatically_moving: std::collections::HashSet<WindowId>,
+    programmatically_moving: HashSet<WindowId>,
-    user_dragging_windows: std::collections::HashSet<WindowId>,
+    user_dragging_windows: HashSet<WindowId>,
-    last_window_positions: std::collections::HashMap<WindowId, Rect>,
+    last_window_positions: HashMap<WindowId, Rect>,

160-166: Address TODO: Make window monitoring intervals configurable.

The TODO comment indicates that the window refresh and monitor intervals should be configurable via skew.toml.

Would you like me to create an issue to track implementing configurable intervals for window_refresh_interval_ms and window_monitor_interval_ms?


831-835: Consider making overlap detection tolerance configurable.

The overlap detection uses a hardcoded 1.0px tolerance which might be too strict for high-DPI displays or cause issues with rounding.

Make the overlap tolerance configurable:

+                let overlap_tolerance = self.config.general.overlap_tolerance.unwrap_or(1.0);
                 let overlaps = !(current_rect.x + current_rect.width <= target_rect.x + overlap_tolerance
-                    || target_rect.x + target_rect.width <= current_rect.x + 1.0
-                    || current_rect.y + current_rect.height <= target_rect.y + 1.0
-                    || target_rect.y + target_rect.height <= current_rect.y + 1.0);
+                    || target_rect.x + target_rect.width <= current_rect.x + overlap_tolerance
+                    || current_rect.y + current_rect.height <= target_rect.y + overlap_tolerance
+                    || target_rect.y + target_rect.height <= current_rect.y + overlap_tolerance);

1300-1316: Consider making layout application parameters configurable.

The layout throttling (50ms), batch size (2), and inter-batch delay (1ms) are hardcoded. Making these configurable would allow performance tuning for different systems.

Add configuration options:

-                if now.duration_since(last_time).as_millis() < 50 {
+                if now.duration_since(last_time).as_millis() < self.config.layout.throttle_ms.unwrap_or(50) {

-        const BATCH_SIZE: usize = 2; // Move 2 windows at a time
+        let batch_size = self.config.layout.batch_size.unwrap_or(2);

-                tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
+                let batch_delay = self.config.layout.batch_delay_ms.unwrap_or(1);
+                tokio::time::sleep(tokio::time::Duration::from_millis(batch_delay)).await;

Also applies to: 1378-1400

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a47f226 and 7a34c77.

📒 Files selected for processing (8)
  • build.rs (1 hunks)
  • src/config.rs (4 hunks)
  • src/layout.rs (15 hunks)
  • src/macos/accessibility.rs (9 hunks)
  • src/macos/window_notifications.rs (1 hunks)
  • src/main.rs (2 hunks)
  • src/snap.rs (1 hunks)
  • src/window_manager.rs (14 hunks)
✅ Files skipped from review due to trivial changes (1)
  • build.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/config.rs
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/macos/accessibility.rs (3)
src/macos/cgwindow.rs (5)
  • CFArrayGetCount (25-25)
  • CFArrayGetValueAtIndex (26-26)
  • CFRelease (27-27)
  • new (328-334)
  • get_window_info_by_id (289-292)
src/lib.rs (1)
  • new (28-35)
src/macos/window_system.rs (3)
  • new (32-41)
  • move_window (295-297)
  • move_all_windows (299-305)
🔇 Additional comments (9)
src/window_manager.rs (3)

214-255: Well-structured window move detection logic.

The implementation correctly differentiates between programmatic moves, user drags, and manual moves with clear control flow and comprehensive debug logging.


439-642: Excellent drag event handling implementation.

The drag event handler properly manages window states, integrates with the snap manager, and uses configurable thresholds. The separation between drag and resize events is clear and well-structured.


1160-1194: Robust workspace detection implementation.

The workspace detection logic has excellent fallback handling: focused window → most common workspace → stored workspace. The debug logging helps with troubleshooting workspace issues.

src/macos/accessibility.rs (6)

60-67: Good improvement: Constants now follow Rust naming conventions.

The change from lowercase to uppercase constants follows Rust's standard naming conventions.


175-215: Well-implemented window movement with proper memory management.

The window movement implementation correctly:

  • Forces cache refresh to ensure current window state
  • Uses AXValue for position and size setting
  • Properly releases all Core Foundation objects
  • Has comprehensive error handling

Also applies to: 314-371


994-1031: Excellent improvement to window ID generation stability.

The window ID generation now uses stable properties (PID, title, rect) with pointer as a last resort, addressing the previous stability concerns. The warning message when using pointer-based IDs is helpful for debugging.


688-692: Proper Core Foundation memory management.

The code correctly releases all cached elements before clearing the cache, preventing memory leaks.

Also applies to: 710-714


421-462: Robust process enumeration with proper safeguards.

The implementation has good defensive programming:

  • Iteration limit prevents infinite loops
  • Buffer size cap (16K PIDs) prevents excessive memory usage
  • Proper error handling for system calls

1033-1059: Good collision detection and handling for window IDs.

The implementation properly detects ID collisions and generates unique fallback IDs using timestamps. The warning logs help identify when collisions occur.

Comment on lines +275 to +296
// For each window element from this PID, try to match with our windows
for (element_index, window_element) in app_window_elements.iter().enumerate() {
// Find the corresponding window by matching PID and index within PID
// This is more reliable than global ordering
let windows_for_pid: Vec<&crate::Window> =
windows.iter().filter(|w| w.owner_pid == pid).collect();

if element_index < windows_for_pid.len() {
let window_id = windows_for_pid[element_index].id;

// Look up the rect for this specific window ID
if let Some(rect) = window_id_to_rect.get(&window_id) {
debug!(
"Moving window {:?} (PID {}, index {}) to {:?}",
window_id, pid, element_index, rect
);
if let Err(e) = self.set_window_rect(*window_element, *rect) {
warn!("Failed to move window {:?}: {}", window_id, e);
}
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential issue: Window ordering assumption may not be reliable.

The code assumes that windows for a given PID maintain the same order between enumeration and the provided windows array. This could lead to windows being moved to incorrect positions if the ordering changes.

Consider matching windows by more stable properties:

-                        if element_index < windows_for_pid.len() {
-                            let window_id = windows_for_pid[element_index].id;
+                        // Match by position or title instead of index
+                        if let Ok(Some(elem_rect)) = self.get_window_rect(*window_element) {
+                            // Find matching window by comparing positions
+                            if let Some(matching_window) = windows_for_pid.iter().find(|w| {
+                                (w.rect.x - elem_rect.x).abs() < 5.0 && 
+                                (w.rect.y - elem_rect.y).abs() < 5.0
+                            }) {
+                                let window_id = matching_window.id;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/macos/accessibility.rs around lines 275 to 296, the current code matches
windows by assuming the order of windows for a given PID is consistent between
app_window_elements and the windows array, which may cause incorrect window
positioning. To fix this, replace the index-based matching with a more stable
property such as window ID or another unique identifier to find the
corresponding window element for each window, ensuring accurate matching
regardless of order changes.

Comment on lines +168 to +203
unsafe fn create_observer_class(&self) -> Result<*const Class, Box<dyn std::error::Error>> {
let superclass = class!(NSObject);
let mut decl = objc::declare::ClassDecl::new("WindowDragObserver", superclass)
.ok_or("Failed to create class declaration")?;

// Add instance variables to store our callback data
decl.add_ivar::<*const std::ffi::c_void>("dragging_windows");
decl.add_ivar::<*const std::ffi::c_void>("resizing_windows");
decl.add_ivar::<*const std::ffi::c_void>("event_sender");

// Add windowWillMove: method
decl.add_method(
sel!(windowWillMove:),
window_will_move_callback as extern "C" fn(&mut Object, Sel, id),
);

// Add windowDidMove: method
decl.add_method(
sel!(windowDidMove:),
window_did_move_callback as extern "C" fn(&mut Object, Sel, id),
);

// Add windowWillStartLiveResize: method
decl.add_method(
sel!(windowWillStartLiveResize:),
window_will_start_live_resize_callback as extern "C" fn(&mut Object, Sel, id),
);

// Add windowDidEndLiveResize: method
decl.add_method(
sel!(windowDidEndLiveResize:),
window_did_end_live_resize_callback as extern "C" fn(&mut Object, Sel, id),
);

Ok(decl.register())
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential race condition in Objective-C class creation.

The create_observer_class method creates a new Objective-C class at runtime, but multiple instances could race to register the same class name, causing a panic.

Add a check to see if the class already exists:

 unsafe fn create_observer_class(&self) -> Result<*const Class, Box<dyn std::error::Error>> {
+    // Check if class already exists
+    if let Some(existing_class) = Class::get("WindowDragObserver") {
+        return Ok(existing_class);
+    }
+
     let superclass = class!(NSObject);
     let mut decl = objc::declare::ClassDecl::new("WindowDragObserver", superclass)
         .ok_or("Failed to create class declaration")?;

Alternatively, use a unique class name per instance or a static registration with std::sync::Once.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
unsafe fn create_observer_class(&self) -> Result<*const Class, Box<dyn std::error::Error>> {
let superclass = class!(NSObject);
let mut decl = objc::declare::ClassDecl::new("WindowDragObserver", superclass)
.ok_or("Failed to create class declaration")?;
// Add instance variables to store our callback data
decl.add_ivar::<*const std::ffi::c_void>("dragging_windows");
decl.add_ivar::<*const std::ffi::c_void>("resizing_windows");
decl.add_ivar::<*const std::ffi::c_void>("event_sender");
// Add windowWillMove: method
decl.add_method(
sel!(windowWillMove:),
window_will_move_callback as extern "C" fn(&mut Object, Sel, id),
);
// Add windowDidMove: method
decl.add_method(
sel!(windowDidMove:),
window_did_move_callback as extern "C" fn(&mut Object, Sel, id),
);
// Add windowWillStartLiveResize: method
decl.add_method(
sel!(windowWillStartLiveResize:),
window_will_start_live_resize_callback as extern "C" fn(&mut Object, Sel, id),
);
// Add windowDidEndLiveResize: method
decl.add_method(
sel!(windowDidEndLiveResize:),
window_did_end_live_resize_callback as extern "C" fn(&mut Object, Sel, id),
);
Ok(decl.register())
}
unsafe fn create_observer_class(&self) -> Result<*const Class, Box<dyn std::error::Error>> {
// Check if class already exists
if let Some(existing_class) = Class::get("WindowDragObserver") {
return Ok(existing_class);
}
let superclass = class!(NSObject);
let mut decl = objc::declare::ClassDecl::new("WindowDragObserver", superclass)
.ok_or("Failed to create class declaration")?;
// Add instance variables to store our callback data
decl.add_ivar::<*const std::ffi::c_void>("dragging_windows");
decl.add_ivar::<*const std::ffi::c_void>("resizing_windows");
decl.add_ivar::<*const std::ffi::c_void>("event_sender");
// Add windowWillMove: method
decl.add_method(
sel!(windowWillMove:),
window_will_move_callback as extern "C" fn(&mut Object, Sel, id),
);
// Add windowDidMove: method
decl.add_method(
sel!(windowDidMove:),
window_did_move_callback as extern "C" fn(&mut Object, Sel, id),
);
// Add windowWillStartLiveResize: method
decl.add_method(
sel!(windowWillStartLiveResize:),
window_will_start_live_resize_callback as extern "C" fn(&mut Object, Sel, id),
);
// Add windowDidEndLiveResize: method
decl.add_method(
sel!(windowDidEndLiveResize:),
window_did_end_live_resize_callback as extern "C" fn(&mut Object, Sel, id),
);
Ok(decl.register())
}
🤖 Prompt for AI Agents
In src/macos/window_notifications.rs around lines 168 to 203, the
create_observer_class method registers an Objective-C class at runtime without
checking if it already exists, which can cause a race condition and panic if
multiple instances try to register the same class. To fix this, first check if
the class with the given name already exists using objc::runtime::Class::get and
return it if found. If not, proceed to create and register the class.
Alternatively, implement a static registration using std::sync::Once to ensure
the class is registered only once safely.

Comment on lines +246 to +248
if let Err(e) = event_sender.try_send(event) {
warn!("Failed to send drag started event: {}", e);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Events may be silently dropped on channel overflow.

Using try_send can drop important window events if the channel is full, potentially causing the window manager to miss drag/resize operations.

Consider using bounded channel with backpressure or logging dropped events:

-                if let Err(e) = event_sender.try_send(event) {
-                    warn!("Failed to send drag started event: {}", e);
-                }
+                match event_sender.try_send(event) {
+                    Ok(()) => {},
+                    Err(mpsc::error::TrySendError::Full(event)) => {
+                        warn!("Event channel full, dropping event: {:?}", event);
+                        // Consider metrics/alerting here
+                    },
+                    Err(mpsc::error::TrySendError::Closed(_)) => {
+                        warn!("Event channel closed, observer should stop");
+                        // Consider stopping the observer
+                    }
+                }

Also applies to: 292-294, 339-341, 389-391

🤖 Prompt for AI Agents
In src/macos/window_notifications.rs around lines 246 to 248 (and similarly at
292-294, 339-341, 389-391), the use of try_send on the event_sender channel can
silently drop events if the channel is full. To fix this, replace try_send with
a send operation that awaits or blocks until there is capacity, implementing
backpressure to prevent event loss. Alternatively, if using a bounded channel,
add explicit logging for dropped events to ensure no important window events are
missed.

Comment on lines +440 to +457
unsafe fn get_window_owner_pid(window: id) -> Option<i32> {
let window_number: i32 = msg_send![window, windowNumber];
if window_number <= 0 {
return None;
}

// Use NSRunningApplication to get the PID from the window's owning app
let app: id = msg_send![window, app];
if app != nil {
let running_app: id = msg_send![app, runningApplication];
if running_app != nil {
let pid: i32 = msg_send![running_app, processIdentifier];
return Some(pid);
}
}

None
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Using potentially unstable private APIs to get window app.

The msg_send![window, app] call appears to use undocumented NSWindow methods that may not be stable across macOS versions.

Consider using CGWindowListCopyWindowInfo with kCGWindowOwnerPID as a more stable alternative if the private API breaks.


🏁 Script executed:

#!/bin/bash
# Check if this API usage is documented or if there are alternatives
echo "Searching for NSWindow app method usage..."
rg -A 5 -B 5 "msg_send.*window.*app" 

# Look for alternative approaches to get window owner
echo "Searching for alternative window owner detection..."
rg -A 10 "processIdentifier|owningApplication|NSRunningApplication"

Length of output: 32812


Avoid undocumented ObjC selectors for window owner PID

The current use of

let app: id = msg_send![window, app];
let running_app: id = msg_send![app, runningApplication];
let pid: i32  = msg_send![running_app, processIdentifier];

relies on private/undocumented NSWindow APIs that may break in future macOS releases. Please replace this with a stable CoreGraphics-based lookup.

• File: src/macos/window_notifications.rs
• Lines: ~440–457

Suggested diff:

-    // Use NSRunningApplication to get the PID from the window's owning app
-    let app: id = msg_send![window, app];
-    if app != nil {
-        let running_app: id = msg_send![app, runningApplication];
-        if running_app != nil {
-            let pid: i32 = msg_send![running_app, processIdentifier];
-            return Some(pid);
-        }
-    }
-
-    None
+    // Use CGWindowListCopyWindowInfo to get the PID from this window
+    if let Some(pid) = get_pid_from_window_number(window_number) {
+        return Some(pid);
+    }
+
+    None

Implement get_pid_from_window_number(window_number: i32) -> Option<i32> to call CGWindowListCopyWindowInfo and extract the kCGWindowOwnerPID field. This ensures compatibility with public, stable macOS APIs.

🤖 Prompt for AI Agents
In src/macos/window_notifications.rs around lines 440 to 457, the current
implementation of get_window_owner_pid uses undocumented Objective-C selectors
on NSWindow, which risks breaking on future macOS versions. Replace this by
implementing a new function get_pid_from_window_number that uses the public
CoreGraphics API CGWindowListCopyWindowInfo to retrieve window info for the
given window number and extract the kCGWindowOwnerPID field. Then modify
get_window_owner_pid to call this new function with the window number to obtain
the PID, ensuring the code relies only on stable, documented macOS APIs.

Comment on lines +54 to +75
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
let log_line = format!(
"[{} {} {}:{}] {}\n",
timestamp,
record.level(),
record.file().unwrap_or("unknown"),
record.line().unwrap_or(0),
record.args()
);

// Write to stderr (console)
eprint!("{}", log_line);

// Write to file
if let Ok(mut file) = self.file.lock() {
let _ = file.write_all(log_line.as_bytes());
let _ = file.flush();
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider error handling improvements for logging operations.

The current implementation silently ignores write failures, which could lead to lost logs during critical errors. Additionally, writing to stderr could block if the output pipe is full.

Consider these improvements:

     fn log(&self, record: &Record) {
         if self.enabled(record.metadata()) {
             let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
             let log_line = format!(
                 "[{} {} {}:{}] {}\n",
                 timestamp,
                 record.level(),
                 record.file().unwrap_or("unknown"),
                 record.line().unwrap_or(0),
                 record.args()
             );
 
             // Write to stderr (console)
-            eprint!("{}", log_line);
+            // Use write! to handle potential blocking
+            use std::io::Write;
+            let _ = std::io::stderr().write_all(log_line.as_bytes());
 
             // Write to file
             if let Ok(mut file) = self.file.lock() {
-                let _ = file.write_all(log_line.as_bytes());
-                let _ = file.flush();
+                if let Err(e) = file.write_all(log_line.as_bytes()) {
+                    eprintln!("Failed to write to log file: {}", e);
+                }
+                if let Err(e) = file.flush() {
+                    eprintln!("Failed to flush log file: {}", e);
+                }
             }
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
let log_line = format!(
"[{} {} {}:{}] {}\n",
timestamp,
record.level(),
record.file().unwrap_or("unknown"),
record.line().unwrap_or(0),
record.args()
);
// Write to stderr (console)
eprint!("{}", log_line);
// Write to file
if let Ok(mut file) = self.file.lock() {
let _ = file.write_all(log_line.as_bytes());
let _ = file.flush();
}
}
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
let log_line = format!(
"[{} {} {}:{}] {}\n",
timestamp,
record.level(),
record.file().unwrap_or("unknown"),
record.line().unwrap_or(0),
record.args()
);
// Write to stderr (console)
// Use write! to handle potential blocking
use std::io::Write;
let _ = std::io::stderr().write_all(log_line.as_bytes());
// Write to file
if let Ok(mut file) = self.file.lock() {
if let Err(e) = file.write_all(log_line.as_bytes()) {
eprintln!("Failed to write to log file: {}", e);
}
if let Err(e) = file.flush() {
eprintln!("Failed to flush log file: {}", e);
}
}
}
}
🤖 Prompt for AI Agents
In src/main.rs between lines 54 and 75, the log function currently ignores
errors from writing to the log file and writing to stderr, which risks losing
important log messages. Modify the code to handle errors returned by
file.write_all and file.flush by logging or reporting them appropriately. Also,
consider handling potential blocking on eprint by using non-blocking writes or
buffering strategies to avoid blocking the main thread during logging.

Comment on lines +115 to +117
let screen_rect = macos.get_screen_rect().await?;
let snap_manager = SnapManager::new(screen_rect, 50.0, 20.0, 100.0, 10.0); // 50px snap threshold, 20px edge zone, 100px corner, 10px margin

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make snap manager parameters configurable.

The snap manager is initialized with hardcoded values. These should be configurable through the Config struct to allow users to customize snapping behavior.

Add snap configuration to your config structure and use it here:

-        let snap_manager = SnapManager::new(screen_rect, 50.0, 20.0, 100.0, 10.0); // 50px snap threshold, 20px edge zone, 100px corner, 10px margin
+        let snap_manager = SnapManager::new(
+            screen_rect,
+            self.config.snap.threshold.unwrap_or(50.0),
+            self.config.snap.edge_zone.unwrap_or(20.0),
+            self.config.snap.corner_size.unwrap_or(100.0),
+            self.config.snap.margin.unwrap_or(10.0),
+        );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let screen_rect = macos.get_screen_rect().await?;
let snap_manager = SnapManager::new(screen_rect, 50.0, 20.0, 100.0, 10.0); // 50px snap threshold, 20px edge zone, 100px corner, 10px margin
let screen_rect = macos.get_screen_rect().await?;
let snap_manager = SnapManager::new(
screen_rect,
self.config.snap.threshold.unwrap_or(50.0),
self.config.snap.edge_zone.unwrap_or(20.0),
self.config.snap.corner_size.unwrap_or(100.0),
self.config.snap.margin.unwrap_or(10.0),
);
🤖 Prompt for AI Agents
In src/window_manager.rs around lines 115 to 117, the SnapManager is initialized
with hardcoded parameters for snap threshold, edge zone, corner size, and
margin. To fix this, add corresponding snap configuration fields to the Config
struct, then update this code to read those values from the Config instance
instead of using fixed literals. This will allow users to customize the snapping
behavior via configuration.

Comment on lines +665 to +685
if distance > 0.5 {
debug!(
"🔄 User move detected for window {:?}: {:.1}px from {:?} to {:?}",
window_id, distance, prev_rect, new_rect
);

// For manual moves, snap back to proper layout position instead of snap zones
let layout_target = self.get_layout_target_for_window(window_id).await;

if let Some(target_rect) = layout_target {
let layout_distance = ((target_rect.x - new_rect.x).powi(2)
+ (target_rect.y - new_rect.y).powi(2))
.sqrt();

debug!(
"🎯 Found layout target for window {:?}: {:?} (distance: {:.1}px)",
window_id, target_rect, layout_distance
);

// More responsive snapping - reduced threshold
if layout_distance > 5.0 {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make movement detection thresholds configurable.

Several hardcoded thresholds for movement and size detection should be configurable:

  • Movement detection threshold (0.5px)
  • Layout snapping distance (5.0px)
  • Size change detection (5.0px)

Replace hardcoded values with configurable ones:

-            if distance > 0.5 {
+            if distance > self.config.general.movement_threshold.unwrap_or(0.5) {

-            if layout_distance > 5.0 {
+            if layout_distance > self.config.general.layout_snap_distance.unwrap_or(5.0) {

-            if width_change > 5.0 || height_change > 5.0 {
+            let size_threshold = self.config.general.size_change_threshold.unwrap_or(5.0);
+            if width_change > size_threshold || height_change > size_threshold {

Also applies to: 709-722

🤖 Prompt for AI Agents
In src/window_manager.rs around lines 665 to 685 and also lines 709 to 722,
replace the hardcoded threshold values for movement detection (0.5px), layout
snapping distance (5.0px), and size change detection (5.0px) with configurable
parameters. Introduce configuration fields or constants for these thresholds,
load or set them appropriately, and use these variables instead of the fixed
numeric literals to allow easy adjustment without code changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants