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

Conversation

@plyght
Copy link
Owner

@plyght plyght commented Jun 6, 2025

Summary by CodeRabbit

  • New Features

    • Introduced window snapping and drag-and-drop management, allowing windows to snap to screen edges, corners, or swap positions with other windows during drag operations.
    • Added comprehensive macOS window drag detection, enabling real-time response to user-initiated window moves.
    • Enhanced workspace detection and management for more accurate multi-workspace support.
    • Added macOS workspace detection via private APIs for current active workspace retrieval.
    • Added a dual logger to output logs simultaneously to console and file for improved diagnostics.
    • Introduced a detailed snap zone system with drag state tracking and snap target detection.
    • Added macOS window drag notification observer for tracking window move start and end events.
  • Improvements

    • Improved window layout algorithms, especially for BSP layouts, with dynamic tree synchronization, window removal, and rectangle validation enforcing size constraints.
    • Enhanced window management and metadata extraction on macOS, including better filtering, stable window IDs, and batch window movement.
    • Bulk move operations for windows, increasing efficiency and reliability during layout changes and window swaps.
    • Refined logging and debug information for window enumeration, drag events, and layout application.
    • Streamlined window focus cycling logic and key combination validation.
    • Improved workspace detection by deriving effective workspace from focused or visible windows.
    • Enhanced window move and resize handling to snap windows back to layout positions when moved manually.
    • Improved internal state tracking to avoid feedback loops during programmatic and user window moves.
    • Added validation of window rectangles to enforce minimum and maximum sizes in layouts.
    • Enhanced macOS accessibility manager with full window enumeration, stable window IDs, and batch window movement.
  • Documentation

    • Added a detailed project overview and updated documentation for architecture, development workflow, and macOS-specific requirements.
  • Style

    • Applied formatting and readability improvements across hotkey and configuration handling.
  • Bug Fixes

    • Reduced potential window ID collisions and improved cache management for more robust window tracking.

@cubic-dev-ai
Copy link

cubic-dev-ai bot commented Jun 6, 2025

Your cubic subscription is currently inactive. Please reactivate your subscription to receive AI reviews and use cubic.

@coderabbitai
Copy link

coderabbitai bot commented Jun 6, 2025

Walkthrough

This update introduces a comprehensive window drag-and-snap system for macOS, including new modules for snap management and drag notifications, and major enhancements to window management, accessibility, and workspace detection. The changes add robust tracking of user and programmatic window moves, dynamic snap regions, improved window identification, and more reliable bulk window operations.

Changes

File(s) Change Summary
README.md Minor wording edit in sway references and hyphen removal in description.
llms.txt Added a detailed project overview and architecture documentation.
src/bin/skew-cli.rs Removed an unused import statement.
src/config.rs Refactored key combination validation to use is_empty() for clarity.
src/focus.rs Simplified logic for cycling focus backwards.
src/hotkeys.rs Formatting and style improvements for readability; no logic changes.
src/ipc.rs Replaced .get(0) with .first() for argument extraction in IPC message handling.
src/layout.rs Major BSP layout refactor: spiral insertion, window removal, dynamic tree sync, and new helper methods.
src/lib.rs Added public module declaration for snap.
src/plugins.rs Added #[allow(dead_code)] to suppress warnings on unused LuaPlugin fields.
src/main.rs Replaced logger setup with a custom dual logger writing to console and file with timestamps.
src/macos/accessibility.rs Overhauled window enumeration, caching, and manipulation: stable IDs, full-app enumeration, batch moves, and improved Core Foundation handling.
src/macos/cgwindow.rs Enhanced window metadata extraction, filtering, and logging; added owner PID and workspace ID support.
src/macos/mod.rs Added window_notifications module declaration.
src/macos/window_notifications.rs New module implementing NSWindow drag notification observer and event system.
src/macos/window_system.rs Added workspace detection, slower window polling, bulk move support, and debug logging.
src/snap.rs New module implementing snap region management, drag tracking, and snap/swap logic.
src/window_manager.rs Integrated drag/snap system, workspace awareness, enhanced move/swap logic, and event handling.
Cargo.toml Updated objc dependency version from 0.2 to 0.2.7.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant NSWindow
    participant WindowDragNotificationObserver
    participant WindowManager
    participant SnapManager
    participant AccessibilityManager
    participant MacOSWindowSystem

    User->>NSWindow: Drag starts
    NSWindow-->>WindowDragNotificationObserver: NSWindowWillMoveNotification
    WindowDragNotificationObserver->>WindowManager: DragStarted event

    User->>NSWindow: Drag ends
    NSWindow-->>WindowDragNotificationObserver: NSWindowDidMoveNotification
    WindowDragNotificationObserver->>WindowManager: DragEnded event (with final rect)
    WindowManager->>SnapManager: Evaluate drag result
    alt Snap to zone
        SnapManager-->>WindowManager: SnapToZone(rect)
        WindowManager->>AccessibilityManager: Move window to snap rect
    else Swap with window
        SnapManager-->>WindowManager: SwapWithWindow(target_id, rect)
        WindowManager->>AccessibilityManager: Swap window positions
    else Return to original
        SnapManager-->>WindowManager: ReturnToOriginal(rect)
        WindowManager->>AccessibilityManager: Move window back
    else No action
        SnapManager-->>WindowManager: NoAction
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇
Windows glide and windows snap,
Corners, edges—map by map!
Drag and drop, then swap or stick,
Zones and workspaces—choose your pick.
With every move, the system sings,
A tiling dance on Cocoa wings!
— Skew Rabbit

Note

⚡️ Unit Test Generation is now available in beta!

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


📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0bae4b9 and a47f226.

⛔ Files ignored due to path filters (3)
  • final_improved_test.log is excluded by !**/*.log
  • improved_fixes_test.log is excluded by !**/*.log
  • skew.log is excluded by !**/*.log
📒 Files selected for processing (3)
  • src/lib.rs (1 hunks)
  • src/macos/window_notifications.rs (1 hunks)
  • src/window_manager.rs (14 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib.rs
🔇 Additional comments (15)
src/macos/window_notifications.rs (7)

11-33: Well-designed event enum structure.

The WindowDragEvent enum comprehensively covers all necessary drag and resize events with appropriate contextual data (window ID, rectangle, and owner PID). This provides a clean abstraction for the drag notification system.


35-50: Clean struct design with proper state separation.

The separation of dragging_windows and resizing_windows tracking is well-designed, and the use of Arc<Mutex<HashMap>> enables thread-safe sharing with the Objective-C callback system.


52-126: Complex but well-structured Objective-C interop setup.

The method properly registers for all four necessary NSWindow notification types and correctly sets up the dynamic observer class with callback data. The pattern of boxing Rust data as raw pointers for Objective-C ivar storage is appropriate for this callback mechanism.


128-164: Proper memory cleanup implementation.

The method correctly addresses the memory leak issue by retrieving the boxed pointers from instance variables and converting them back to Box::from_raw for proper deallocation. The null checks and observer release ensure clean shutdown.


166-202: Standard Objective-C runtime class creation.

The method properly creates a dynamic class with the necessary instance variables and callback methods. The approach is appropriate for this notification observer pattern.


210-386: Consistent and well-structured callback implementations.

All four callback functions follow a consistent pattern with proper error handling, comprehensive logging, and appropriate event generation. The code correctly extracts window information and manages state transitions between drag/resize start and end events.


388-439: Well-implemented helper functions with important coordinate conversion.

The helper functions properly extract window information with appropriate error handling. The coordinate system conversion in get_window_rect correctly transforms from Cocoa's bottom-left origin to the expected top-left origin system.

src/window_manager.rs (8)

69-93: Well-designed struct additions for drag notification system.

The new fields appropriately separate concerns with clear naming and proper types. The separation of programmatically_moving and user_dragging_windows is particularly important for preventing feedback loops in the window management system.


107-139: Proper initialization of drag notification system.

The constructor correctly sets up the drag notification infrastructure with appropriate error handling. The snap manager initialization is sound, though the 50px snap threshold could potentially be made configurable in the future for different user preferences.


179-183: Clean integration of drag event handling.

The drag event handling is properly integrated into the main event loop with consistent error handling and logging patterns matching the existing code structure.


214-301: Sophisticated window event handling with proper state management.

The enhanced logic properly distinguishes between programmatic moves, user drags, and manual moves, which is crucial for preventing feedback loops. The state tracking, cleanup, and comprehensive logging significantly improve the system's reliability and debuggability.


436-622: Comprehensive and well-structured drag event handling.

The method properly handles the complete drag lifecycle with appropriate integration to the snap manager. The workspace filtering, state management, and handling of different drag results (snap, swap, return to original) demonstrate solid understanding of the drag-and-drop workflow requirements.


705-737: Pragmatic solution for layout target computation.

The method uses a temporary layout manager copy to work around Rust borrowing constraints, which is a reasonable approach for this use case. The workspace filtering and layout computation logic is sound.


739-831: Sophisticated displacement logic prevents window overlaps.

The method elegantly solves the complex problem of moving windows to layout positions while preventing overlaps by detecting affected windows and moving them simultaneously. The overlap detection and state management are well-implemented.


1103-1136: Improved workspace detection with intelligent fallbacks.

The enhanced workspace detection logic using the focused window's workspace, then falling back to the most common workspace among visible windows, provides much more reliable workspace identification than simple stored state. The debugging output aids in understanding the detection strategy.

✨ 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 (17)
src/macos/window_system.rs (1)

137-143: Consider using trace level for verbose window scan logging.

The debug logging for every window during each scan (every 200ms) could generate excessive log output in production environments with many windows.

-debug!("Window scan found {} windows", current_windows.len());
-for window in &current_windows {
-    debug!(
-        "Window: {} ({}), workspace: {}, rect: {:?}",
-        window.title, window.owner, window.workspace_id, window.rect
-    );
-}
+debug!("Window scan found {} windows", current_windows.len());
+for window in &current_windows {
+    trace!(
+        "Window: {} ({}), workspace: {}, rect: {:?}",
+        window.title, window.owner, window.workspace_id, window.rect
+    );
+}
llms.txt (3)

9-9: Remove redundant "Interface" from "CLI Interface".

The acronym CLI already stands for "Command-Line Interface", making "CLI Interface" redundant.

-- **CLI Interface** (`main.rs`): Command-line entry point
+- **CLI** (`main.rs`): Command-line entry point
🧰 Tools
🪛 LanguageTool

[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)


17-17: Fix macOS capitalization.

The correct capitalization for Apple's operating system is "macOS", not "macos".

-- **macOS Integration** (`macos/`): Accessibility API bindings, Core Graphics window ops
+- **macOS Integration** (`macOS/`): Accessibility API bindings, Core Graphics window ops
🧰 Tools
🪛 LanguageTool

[grammar] ~17-~17: The operating system from Apple is written “macOS”.
Context: ...ng integration - macOS Integration (macos/): Accessibility API bindings, Core Gr...

(MAC_OS)


56-56: Add missing article "the".

-- Integrates with macOS window system through Accessibility bindings
+- Integrates with the macOS window system through Accessibility bindings
🧰 Tools
🪛 LanguageTool

[uncategorized] ~56-~56: You might be missing the article “the” here.
Context: ...for window operations - Integrates with macOS window system through Accessibility bin...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

src/macos/cgwindow.rs (2)

105-107: Consider using a more appropriate default for owner_pid.

Using -1 as a default for a process ID could be confusing since PIDs are typically non-negative. Consider using 0 or an Option<i32> instead.

-// Extract owner PID
-let owner_pid =
-    Self::get_number_from_dict(dict, "kCGWindowOwnerPID").unwrap_or(-1.0) as i32;
+// Extract owner PID
+let owner_pid =
+    Self::get_number_from_dict(dict, "kCGWindowOwnerPID").unwrap_or(0.0) as i32;

130-144: Consider using trace level for window filtering logs.

These debug logs could generate excessive output in production when many windows are being filtered. Consider using trace! level instead.

-debug!(
+trace!(
     "Filtering out window {} ({}): layer={}, on_screen={}, alpha={}",
     title, owner, layer, is_on_screen, alpha
 );

-debug!(
+trace!(
     "Filtering out small window {} ({}): {}x{}",
     title, owner, rect.width, rect.height
 );
src/macos/window_notifications.rs (2)

106-128: Handle potential class registration conflicts.

The hardcoded class name "WindowDragObserver" could cause registration failures if the class already exists. Consider using a unique class name or checking if the class exists first.

 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")?;

171-173: Consider propagating channel send errors.

Failed event sends are only logged as warnings. If the channel is full or closed, drag events will be lost silently, potentially causing the UI to be out of sync.

Consider using send().await with proper async handling or increasing the channel buffer size to reduce the likelihood of dropped events. Alternatively, implement a retry mechanism or error callback to handle send failures more gracefully.

Also applies to: 217-219

src/snap.rs (3)

56-63: Remove unused window_id field.

The window_id field in WindowDragState is marked with #[allow(dead_code)] and appears to be unused. Since the window ID is already used as the HashMap key, storing it again in the value is redundant.

 #[derive(Debug, Clone)]
 struct WindowDragState {
-    #[allow(dead_code)]
-    window_id: WindowId,
     initial_rect: Rect,
     is_dragging: bool,
     drag_start_time: std::time::Instant,
 }

Also update the initialization:

 self.window_drag_states.insert(
     window_id,
     WindowDragState {
-        window_id,
         initial_rect: current_rect,
         is_dragging: true,
         drag_start_time: std::time::Instant::now(),
     },
 );

82-87: Consider making snap zone sizes configurable.

The hardcoded zone sizes (150px for edges, 100px for corners) may not work well across different screen sizes and resolutions. Consider making these values configurable or scale them based on screen dimensions.

 fn update_snap_zones(&mut self, screen_rect: Rect) {
     self.snap_zones.clear();

-    let edge_zone_width = 150.0; // Wider edge zones for easier targeting
-    let corner_size = 100.0; // Corner zones at edges
+    // Scale zone sizes based on screen dimensions
+    let edge_zone_width = (screen_rect.width * 0.1).min(150.0);
+    let corner_size = (screen_rect.width * 0.06).min(100.0);
     let margin = 8.0; // Small margin from screen edges

307-309: Make drag thresholds configurable.

The hardcoded minimum drag time (100ms) might not suit all users' preferences. Some users might want more responsive snapping while others might prefer more deliberate gestures.

Consider adding these as configurable fields:

 pub struct SnapManager {
     screen_rect: Rect,
     snap_zones: Vec<SnapZone>,
     snap_threshold: f64,
+    min_drag_time_ms: u128,
+    min_drag_distance: f64,
     window_drag_states: HashMap<WindowId, WindowDragState>,
 }

And in the drag handling:

-let min_time_ms = 100u128; // 100ms minimum drag time
-let min_distance = self.snap_threshold; // Use snap_threshold directly for distance
+let min_time_ms = self.min_drag_time_ms;
+let min_distance = self.min_drag_distance;
src/layout.rs (1)

149-157: Consider BSP tree balance for performance.

The spiral insertion pattern always inserts into the rightmost position, which can create highly unbalanced trees. With many windows, this could lead to O(n) depth and impact performance.

Consider implementing a rebalancing mechanism or using a different insertion strategy that maintains better balance. Alternatively, document the expected maximum number of windows to ensure performance remains acceptable.

src/window_manager.rs (4)

108-109: Consider making the snap threshold configurable.

The 50px snap threshold is hardcoded. Consider adding this to the Config struct to allow users to customize snap sensitivity.

-        let snap_manager = SnapManager::new(screen_rect, 50.0); // 50px snap threshold
+        let snap_threshold = config.snap.threshold.unwrap_or(50.0);
+        let snap_manager = SnapManager::new(screen_rect, snap_threshold);

147-153: Good documentation of timer intervals with actionable TODO.

The detailed explanation of the refresh and monitor intervals is helpful. The TODO clearly identifies what needs to be configurable.

Would you like me to implement the configurable intervals by adding window_refresh_interval_ms and window_monitor_interval_ms to the config structure?


502-502: Consider making movement thresholds configurable.

The method uses two hardcoded thresholds:

  • 20px for detecting "significant" moves (line 502)
  • 10px for avoiding redundant snap moves (line 539)

Consider making these configurable for different user preferences and display densities.

Also applies to: 539-539


578-719: Consider extracting common swap logic to reduce duplication.

Both swap_windows and swap_windows_with_rects share similar bulk move and fallback logic. Consider extracting the common pattern.

+    async fn perform_window_swap(&mut self, swap_layouts: HashMap<WindowId, Rect>, windows: Vec<Window>) -> Result<()> {
+        // Mark windows 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.previous_window_positions.insert(window_id, rect);
+                }
+                Ok(())
+            }
+            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.previous_window_positions.insert(window_id, rect);
+                        }
+                        Err(e) => warn!("Failed to move window {:?} during swap: {}", window_id, e),
+                    }
+                }
+                Ok(())
+            }
+        }
+    }
src/macos/accessibility.rs (1)

782-782: Consider making position matching tolerance configurable.

The 5-pixel tolerance for window position matching is hardcoded. Different display configurations might benefit from different tolerances.

Consider adding a constant or configuration option:

+const WINDOW_POSITION_MATCH_TOLERANCE: f64 = 5.0;
+
-                        if dx < 5.0 && dy < 5.0 && dw < 5.0 && dh < 5.0 {
+                        if dx < WINDOW_POSITION_MATCH_TOLERANCE && dy < WINDOW_POSITION_MATCH_TOLERANCE && 
+                           dw < WINDOW_POSITION_MATCH_TOLERANCE && dh < WINDOW_POSITION_MATCH_TOLERANCE {
📜 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 00b6a28.

📒 Files selected for processing (18)
  • README.md (1 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 (8 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 (15 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)


[grammar] ~17-~17: The operating system from Apple is written “macOS”.
Context: ...ng integration - macOS Integration (macos/): Accessibility API bindings, Core Gr...

(MAC_OS)


[uncategorized] ~56-~56: You might be missing the article “the” here.
Context: ...for window operations - Integrates with macOS window system through Accessibility bin...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

🔇 Additional comments (31)
README.md (1)

234-234: LGTM! Improved readability.

The removal of the hyphen makes the text more readable and consistent.

src/plugins.rs (1)

34-37: LGTM! Appropriate use of dead code allowance.

The #[allow(dead_code)] attributes are reasonable here since these fields store important plugin metadata that may be used for debugging or future functionality, even if not currently accessed directly.

src/lib.rs (1)

8-8: LGTM! Proper module addition for snapping functionality.

The addition of the snap module aligns well with the tiling window manager objectives and follows the established modular structure.

src/macos/mod.rs (1)

3-3: LGTM! Appropriate macOS-specific module addition.

The window_notifications module is properly placed within the macOS-specific modules and supports the enhanced window management functionality described in the PR.

src/config.rs (1)

308-308: LGTM! More idiomatic Rust code.

Using is_empty() instead of parts.len() < 1 is more idiomatic and clearly expresses the intent. This follows Rust best practices for checking empty collections.

src/main.rs (2)

2-2: LGTM: Import cleanup.

Good practice to remove unused imports. The error import is no longer needed in this file.


31-35: Excellent logging enhancement.

The detailed logger configuration with debug level and timestamps will significantly improve debugging capabilities for the window management system. This aligns well with the comprehensive window drag-and-snap features being introduced.

src/ipc.rs (1)

153-153: LGTM: Idiomatic Rust improvement.

Using .first() instead of .get(0) is more idiomatic and readable in Rust. The functionality remains identical.

Also applies to: 173-173

src/focus.rs (1)

341-345: LGTM: Simplified backward cycling logic.

The refactored conditional logic maintains the correct backward cycling behavior while being more readable. The explicit handling of the wrap-around case (index 0 → last index) is clearer.

src/hotkeys.rs (1)

58-58: LGTM: Code formatting improvements.

The formatting enhancements improve readability through:

  • Consistent line breaks and spacing
  • Better formatting of multi-line expressions
  • Simplified error handling with .is_err()

These changes maintain all existing functionality while making the code more maintainable.

Also applies to: 87-90, 189-192, 467-467

src/macos/window_system.rs (1)

82-82: LGTM!

Good use of iterator methods for cleaner and more idiomatic Rust code.

src/macos/cgwindow.rs (2)

147-159: LGTM! Comprehensive system app filtering.

The extended list of system applications to exclude from tiling is thorough and includes the newly added entries like "Menubar", "Menu Bar", and "SystemPreferences".


321-325: LGTM! Proper Default trait implementation.

Good addition of the Default trait implementation delegating to new().

src/layout.rs (1)

460-460: Verify Rust version compatibility for div_ceil.

The div_ceil method requires Rust 1.73.0+. Ensure this is compatible with your project's MSRV.

If you need to support older Rust versions, you can replace it with:

-let rows = window_count.div_ceil(cols);
+let rows = (window_count + cols - 1) / cols;
src/window_manager.rs (7)

5-8: Good addition of drag and snap functionality imports and Window enhancement.

The new imports for WindowDragNotificationObserver and SnapManager are well-organized, and adding owner_pid to the Window struct provides valuable process identification for the enhanced window management features.

Also applies to: 20-20


69-88: Well-structured additions for drag state management.

The new fields properly support the drag-and-snap system with clear separation of concerns:

  • snap_manager for snap zone logic
  • Drag observer for NSWindow notifications
  • Separate tracking for programmatic vs user-initiated moves
  • Previous position tracking for restoration

The documentation comments are helpful for understanding the purpose of each tracking collection.


196-219: Excellent move event differentiation logic.

The three-way branching correctly handles:

  1. Programmatic moves (avoiding feedback loops)
  2. NSWindow-tracked drags (deferring to drag end handler)
  3. Other user moves (triggering immediate positioning)

This prevents conflicts between different move sources and ensures appropriate handling for each scenario.


388-473: Comprehensive drag event handling with excellent state management.

The implementation properly handles all drag scenarios:

  • Tracks drag start with initial position storage
  • Processes drag end with snap/swap/return logic based on final position
  • Cleans up drag state consistently
  • Uses clear emoji indicators in logs for easy debugging

The integration with SnapManager and proper state updates ensure reliable drag-and-drop behavior.


745-778: Excellent workspace detection strategy with robust fallbacks.

The three-tier approach ensures accurate workspace detection:

  1. Focused window's workspace (most reliable)
  2. Most common workspace among visible windows (handles focus edge cases)
  3. Stored workspace (final fallback)

This addresses potential staleness in workspace state and provides better accuracy for workspace-aware operations.


917-979: Robust layout application with intelligent retry logic.

The implementation includes several improvements:

  • Bulk window movement for better performance and atomicity
  • Automatic fallback to individual moves on failure
  • Retry logic (3 attempts with 50ms delays) for handling timing issues
  • Detailed logging for debugging layout application

This should significantly improve the reliability of layout operations.


1164-1188: Good placeholder for future BSP tree integration.

The method clearly documents its intended purpose with actionable TODOs. The current implementation maintains consistency by calling apply_layout while leaving room for the more sophisticated BSP tree updates mentioned in the comments.

src/macos/accessibility.rs (10)

9-54: Well-structured additions for enhanced window positioning.

The new imports and type definitions properly support the enhanced accessibility features:

  • proc_listpids enables comprehensive process enumeration
  • CGPoint and CGSize structs correctly match Core Foundation types
  • AXValue functions enable precise window positioning
  • Consistent K_ prefix for constants improves code clarity

174-214: Robust window movement with intelligent fallback strategy.

The enhanced move_window implementation provides excellent reliability:

  1. Refreshes cache to ensure current window state
  2. Attempts direct lookup via find_window_element
  3. Falls back to brute force search matching CGWindow data
  4. Proper CFRelease calls prevent memory leaks

This two-tier approach ensures windows can be moved even when the accessibility cache is incomplete.


241-311: Excellent bulk window movement implementation.

The PID-based approach for bulk window moves is well-designed:

  • Groups windows by PID for accurate matching
  • Uses index order within PID groups for reliable window identification
  • Isolates errors per PID to maximize successful moves
  • Comprehensive CFRelease cleanup prevents memory leaks

This should significantly improve performance for layout operations affecting multiple windows.


313-370: Properly implemented window rect setting with excellent resource management.

The method correctly uses the AXValue API:

  • Creates CGPoint and CGSize values with proper type constants
  • Handles errors at each step with appropriate cleanup
  • Releases all allocated resources in both success and error paths
  • Provides detailed error information for debugging

420-461: Well-implemented process enumeration with proper safeguards.

The implementation includes important defensive measures:

  • Buffer growth strategy handles unknown process counts
  • Iteration limit (5) prevents infinite loops
  • Size cap (16K PIDs) prevents excessive memory usage
  • Filters invalid PIDs for cleaner results

These safeguards ensure reliable operation across different system loads.


463-487: Good deprecation strategy for legacy methods.

The deprecated methods are properly marked with:

  • Clear #[deprecated] attributes with version information
  • Helpful messages indicating replacement methods
  • Non-breaking implementation that logs warnings
  • Planned removal timeline (v2.0)

This allows smooth migration for existing code.


489-549: Robust window enumeration with proper filtering.

The implementation includes several good practices:

  • Skips system processes that cannot have windows
  • Validates windows are tileable (have position/size attributes)
  • Properly retains elements that will be returned
  • Comprehensive cleanup ensures no memory leaks

971-988: Excellent improvement to window ID generation.

The new hash-based approach is much more robust than simple pointer casting:

  • Uses proven hash constants (golden ratio multiplier)
  • Combines multiple inputs (pointer, PID, index) to reduce collisions
  • Bit operations ensure good distribution across ID space
  • Produces stable IDs that persist better across operations

990-1016: Good defensive handling of window ID collisions.

The collision detection and fallback mechanism ensures reliable window identification:

  • Detects collisions before insertion
  • Generates unique fallback IDs using timestamp
  • Preserves PID and index information in fallback format
  • Logs collisions for monitoring

This prevents window identification issues in edge cases.


1045-1055: Proper cleanup in Drop implementation.

The Drop trait correctly releases all Core Foundation resources:

  • Iterates through cached windows to release each element
  • Releases the system-wide element
  • Prevents memory leaks when AccessibilityManager is destroyed

Comment on lines +11 to +15
// macOS workspace detection
extern "C" {
fn CGSGetActiveSpace(connection: u32) -> u32;
fn CGSMainConnectionID() -> u32;
}
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

Verify the stability and availability of private macOS APIs.

The code uses private Core Graphics APIs (CGSGetActiveSpace and CGSMainConnectionID) which are undocumented and may change or be removed in future macOS versions. This could lead to runtime failures or undefined behavior.


🌐 Web query:

Are CGSGetActiveSpace and CGSMainConnectionID private APIs in macOS? What are the risks of using them?

💡 Result:

Status of CGSGetActiveSpace and CGSMainConnectionID APIs

CGSGetActiveSpace and CGSMainConnectionID are both private (undocumented) APIs in macOS. They are part of the CoreGraphics Services (CGS) layer—an internal framework used by macOS for window management and workspace (Spaces) handling.

  • These functions are not included in Apple’s official public documentation, and their signatures and behavior are only discoverable via reverse engineering or community sources[2][8].
  • For example, you can see them explicitly noted as “undocumented private API” in open-source projects, such as the Phoenix window manager[2].
  • Apple has historically kept such APIs private, and public support for Spaces and window management through official APIs has been limited since macOS 10.7[5].

Risks of Using Private APIs

Using private APIs in macOS software development brings several key risks:

1. Stability and Compatibility Risks

  • Private APIs can change or be removed without notice in any macOS update, potentially breaking your application across minor or major OS versions[5][7][8].
  • They may work in one version (e.g., macOS 12.1) but break or behave differently in others (e.g., macOS 12.3 or later)[1].

2. App Store Rejection

  • Apple’s App Store guidelines prohibit the use of private APIs in apps distributed via the Mac App Store. Applications using these APIs are likely to be rejected during the review process[5].

3. Security and System Integrity Risks

  • Some private API usage may require disabling macOS security features, such as System Integrity Protection (SIP), which exposes the system to additional security risks[6].

4. Maintenance Burden

  • Updates to macOS can radically alter or eliminate private APIs, forcing developers to reimplement major parts of their codebase to restore functionality[5][7].

5. Legal and Policy Concerns

  • Apple’s terms of service may restrict the reverse engineering and use of private APIs, with potential legal implications for violating these terms[7].

Summary Table

Aspect Private API Usage (CGSGetActiveSpace, CGSMainConnectionID)
Documentation/Support None (undocumented, Apple does not support)
Stability Risk High (subject to change/removal with no notice)
App Store Eligibility Not allowed (may lead to app rejection)
Security/System Integrity May require disabling protections (e.g., SIP)
Maintenance Burden High (may break on OS updates, frequent rework needed)
Legal/Policy Risks Possible violation of Apple’s terms of service, potential legal risks

Conclusion

CGSGetActiveSpace and CGSMainConnectionID are private, undocumented macOS APIs. Using them in your project introduces significant risks, including instability, incompatibility across OS versions, App Store rejection, potential security issues, and possible violation of Apple’s terms of service[2][5][7][8]. Developers should avoid relying on these APIs for production or distributed applications.

Citations:


Private CoreGraphics API Usage Warning

The FFI bindings to CGSGetActiveSpace and CGSMainConnectionID in src/macos/window_system.rs (lines 11–15) rely on undocumented, private CoreGraphics Services. These APIs carry significant risks:

  • Stability & Compatibility: Apple may change or remove them in any macOS update, causing runtime failures.
  • App Store Rejection: Use of private APIs violates App Store guidelines.
  • Security Implications: May require disabling System Integrity Protection (SIP).
  • Maintenance Burden: Future macOS releases can break your implementation, forcing major rewrites.
  • Legal/Policy Concerns: Possible violation of Apple’s terms of service around private API usage.

Action items:

  • Evaluate official, public alternatives (e.g., NSWorkspace notifications or Accessibility APIs).
  • If private APIs are unavoidable, isolate and guard their usage behind runtime checks or feature flags.
  • Document this decision and plan for fallback behavior in unsupported macOS versions.
🤖 Prompt for AI Agents
In src/macos/window_system.rs lines 11 to 15, the code uses private,
undocumented CoreGraphics APIs CGSGetActiveSpace and CGSMainConnectionID, which
pose risks of instability, App Store rejection, and security issues. To fix
this, replace these private API calls with official public macOS APIs such as
NSWorkspace notifications or Accessibility APIs if possible. If private APIs
must be used, isolate their usage behind runtime checks or feature flags,
document this clearly, and implement fallback behavior for unsupported macOS
versions to mitigate risks.

Comment on lines 124 to 129
// Window monitoring at 200ms provides responsive detection of window changes
// TODO: Make this configurable via skew.toml with key 'window_monitor_interval_ms'
// Recommended range: 100-500ms (lower = more responsive, higher = less CPU usage)
// Note: This interval should be configurable in production as it can be
// performance-intensive with CGWindowListCopyWindowInfo calls
let mut interval = interval(Duration::from_millis(200));
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 adding the configuration option immediately.

While the TODO comment is helpful, it would be better to implement the configurability now to avoid performance issues in production. The 200ms interval could be resource-intensive on systems with many windows.

Consider adding a configuration option in your config structure:

+// In your config struct
+pub struct WindowMonitorConfig {
+    pub interval_ms: u64, // Default: 200, Range: 100-500
+}

Then use it here:

-let mut interval = interval(Duration::from_millis(200));
+let interval_ms = config.window_monitor.interval_ms.clamp(100, 500);
+let mut interval = interval(Duration::from_millis(interval_ms));
📝 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
// Window monitoring at 200ms provides responsive detection of window changes
// TODO: Make this configurable via skew.toml with key 'window_monitor_interval_ms'
// Recommended range: 100-500ms (lower = more responsive, higher = less CPU usage)
// Note: This interval should be configurable in production as it can be
// performance-intensive with CGWindowListCopyWindowInfo calls
let mut interval = interval(Duration::from_millis(200));
// Window monitoring at 200ms provides responsive detection of window changes
// TODO: Make this configurable via skew.toml with key 'window_monitor_interval_ms'
// Recommended range: 100-500ms (lower = more responsive, higher = less CPU usage)
// Note: This interval should be configurable in production as it can be
// performance-intensive with CGWindowListCopyWindowInfo calls
let interval_ms = config.window_monitor.interval_ms.clamp(100, 500);
let mut interval = interval(Duration::from_millis(interval_ms));
🤖 Prompt for AI Agents
In src/macos/window_system.rs around lines 124 to 129, the window monitoring
interval is hardcoded to 200ms, which can be performance-intensive. To fix this,
add a configuration option named 'window_monitor_interval_ms' to the
application's config structure, allowing users to set this interval. Then,
replace the hardcoded 200ms duration with the configured value, defaulting to
200ms if the config is not set. This will make the interval adjustable and
improve performance in production environments.

Comment on lines +182 to +189
let removed_from_left = self
.left
.as_mut()
.is_some_and(|left| left.remove_window(window_id));
let removed_from_right = self
.right
.as_mut()
.is_some_and(|right| right.remove_window(window_id));
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

Verify Rust version compatibility for newer language features.

The code uses several newer Rust features:

  • is_some_and (lines 185, 188, 227-232) requires Rust 1.70.0+
  • is_none_or (lines 200-201) requires Rust 1.82.0+

Ensure your project's MSRV (Minimum Supported Rust Version) is compatible with these features.

Also applies to: 200-201, 226-233


🏁 Script executed:

#!/bin/bash
# Check for Rust version requirements in the project
fd -e toml -x grep -l "rust-version\|msrv" {} \;
cat Cargo.toml | grep -E "rust-version|edition"

Length of output: 120


Ensure MSRV ≥ 1.82.0 and Declare It in Cargo.toml

Your Cargo.toml currently only specifies

edition = "2021"

and doesn’t declare a rust-version (MSRV). To guarantee support for:

  • is_some_and (stabilized in Rust 1.70.0)
  • is_none_or (stabilized in Rust 1.82.0)

please add under [package]:

rust-version = "1.82.0"

(or, if you don’t need is_none_or yet, set "1.70.0" and defer using is_none_or until you bump to 1.82.0)


Revisit Spiral Insertion for Tree Balance

The spiral‐pattern insertion can lead to skewed BSP trees over time. Consider:

  • Introducing a rebalancing step (e.g., midpoint splits or rotation heuristics)
  • Writing tests or benchmarks that measure tree depth after many insertions to validate the shape
🤖 Prompt for AI Agents
In src/layout.rs around lines 182 to 189 and also lines 200-201 and 226-233, the
code uses Rust features `is_some_and` and `is_none_or` which require Rust
1.70.0+ and 1.82.0+ respectively. To fix this, update your Cargo.toml file by
adding `rust-version = "1.82.0"` under the `[package]` section to declare the
minimum supported Rust version and ensure compatibility with these features.
Additionally, consider implementing a rebalancing mechanism for the BSP tree
insertion to prevent skewed trees over time.

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: 3

♻️ Duplicate comments (2)
src/macos/window_system.rs (2)

11-15: Private API usage already flagged in previous review.


124-129: Monitoring interval configuration already flagged in previous review.

🧹 Nitpick comments (3)
src/macos/window_notifications.rs (1)

275-292: Add error handling for missing running application.

The function may return None when runningApplication is not available, but this could indicate a legitimate system state rather than an error.

Consider adding debug logging when PID cannot be retrieved:

 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);
+        } else {
+            debug!("No running application found for window {}", window_number);
         }
+    } else {
+        debug!("No app found for window {}", window_number);
     }

     None
 }
src/window_manager.rs (2)

103-133: Consider error handling for drag observer initialization.

The drag observer setup properly handles initialization errors, but consider what happens if the observer fails during runtime.

Add monitoring for observer health:

         let mut drag_observer = WindowDragNotificationObserver::new(drag_event_tx);
         drag_observer
             .start_observing()
             .map_err(|e| anyhow::anyhow!("Failed to start drag observer: {}", e))?;
+        
+        // TODO: Add periodic health check for drag observer
+        // Consider restarting observer if it becomes unresponsive

1101-1124: Remove or implement placeholder method.

The update_layout_for_manual_move method is marked as dead code and contains only placeholder comments. Either implement it or remove it.

-    #[allow(dead_code)]
-    async fn update_layout_for_manual_move(
-        &mut self,
-        window_id: WindowId,
-        new_rect: Rect,
-    ) -> Result<()> {
-        // For now, we'll just apply the existing layout logic
-        // In a more sophisticated implementation, we might update the BSP tree
-        // to reflect the manual positioning
-        debug!(
-            "Window {:?} manually moved to {:?}, updating layout",
-            window_id, new_rect
-        );
-
-        // You could implement logic here to:
-        // 1. Remove the window from its current position in the BSP tree
-        // 2. Find where it should be placed based on its new position
-        // 3. Rebuild the tree structure accordingly
-
-        // For now, just ensure the layout is consistent
-        self.apply_layout().await?;
-
-        Ok(())
-    }

Do you want me to help implement the BSP tree update logic or remove this placeholder method?

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 51b431b and aac5b44.

📒 Files selected for processing (5)
  • Cargo.toml (1 hunks)
  • src/macos/window_notifications.rs (1 hunks)
  • src/macos/window_system.rs (7 hunks)
  • src/snap.rs (1 hunks)
  • src/window_manager.rs (15 hunks)
✅ Files skipped from review due to trivial changes (1)
  • Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/snap.rs
🔇 Additional comments (13)
src/macos/window_system.rs (5)

82-82: Good refactoring to use iterator methods.

The change to use display_list.iter().enumerate().take(display_count as usize) improves code readability and follows Rust idioms better than the previous loop implementation.


178-178: Good optimization removing unnecessary clone.

Simplifying the event sending by passing new_window.rect directly instead of cloning improves performance.


208-208: Good optimization returning reference instead of clone.

Returning main_display.rect directly instead of cloning reduces unnecessary memory allocation.


300-306: LGTM: Clean delegation to accessibility manager.

The move_all_windows method properly delegates to the accessibility manager with appropriate parameters and error propagation.


316-353: Excellent improvement addressing previous feedback.

The workspace validation has been significantly enhanced compared to the previous review comment suggestions:

  • Added comprehensive safety documentation
  • Implemented upper bounds validation (workspace > 1000)
  • Improved error messages and debug logging
  • Proper fallback handling
src/macos/window_notifications.rs (2)

100-113: Memory leak issue has been properly addressed.

The cleanup code correctly retrieves and drops the boxed pointers, addressing the memory leak concern raised in the previous review.


251-273: Coordinate system conversion looks correct.

The conversion from Cocoa's bottom-left origin to top-left origin coordinate system is properly implemented by flipping the Y coordinate using screen height.

src/window_manager.rs (6)

20-20: LGTM: Adding owner_pid field.

The addition of owner_pid field to the Window struct aligns with the enhanced window metadata extraction and drag event handling.


69-89: Well-structured state management additions.

The new fields for snap management, drag tracking, and position tracking are well-organized and clearly documented. The separation of programmatic vs user drag tracking will help prevent feedback loops.


200-225: Excellent window move event handling logic.

The three-way distinction between programmatic moves, NSWindow-tracked drags, and other user moves is well-implemented and should prevent feedback loops while enabling proper snap detection.


644-728: Robust window swapping with fallback strategy.

The swap implementation correctly tries bulk moves first and falls back to individual moves, with proper state tracking and error handling.


862-895: Intelligent workspace detection strategy.

The effective workspace detection using focused window first, then most common workspace, with fallback to stored value is a smart approach that should be more reliable than single-source detection.


1028-1095:

❓ Verification inconclusive

Verify bulk move performance with many windows.

The bulk move implementation with individual move fallback is well-structured, but consider the performance impact when dealing with many windows.

Test bulk move performance with varying window counts:


🏁 Script executed:

#!/bin/bash
# Search for any existing performance tests or benchmarks
rg -i "benchmark|performance.*test|move.*window.*test" --type rust

Length of output: 68


Add benchmarks to verify bulk vs. individual window‐move performance

I ran a repository‐wide search and didn’t find any existing benchmark or performance tests for window moves. To ensure move_all_windows scales and to catch regressions in large layouts, please add dedicated benchmarks—e.g., using Criterion—to measure:

  • Bulk move throughput for various window counts (small/medium/large)
  • Fallback path latency when falling back to individual move_window calls
  • Comparative timings between the two approaches

Suggested action items:

  • Create a new benches/window_move.rs (or similar) using Criterion
  • Define benchmarks for window counts such as 10, 100, and 1000
  • Report and review results to guide any necessary optimizations

fix_required

Comment on lines +137 to +143
debug!("Window scan found {} windows", current_windows.len());
for window in &current_windows {
debug!(
"Window: {} ({}), workspace: {}, rect: {:?}",
window.title, window.owner, window.workspace_id, window.rect
);
}
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

Verify debug log volume in production.

The debug logging provides valuable insights for development, but the window scan logs could be quite verbose in production environments with many windows.

Consider adding a debug level check or making this logging configurable to avoid performance impact in production:


🏁 Script executed:

#!/bin/bash
# Search for similar debug logging patterns that might need volume control
rg -A 2 -B 2 "debug.*Window.*scan|debug.*found.*windows" --type rust

Length of output: 486


Guard verbose debug logging in macOS window scan

It looks like this is the only occurrence of “Window scan found…” debug logs:

• File: src/macos/window_system.rs
Lines: ~137–143

To prevent unbounded log volume (and the cost of formatting all window metadata) in production, wrap these debug calls in a runtime log-level check. For example:

 match CGWindowInfo::get_all_windows() {
     Ok(current_windows) => {
-        debug!("Window scan found {} windows", current_windows.len());
-        for window in &current_windows {
-            debug!(
-                "Window: {} ({}), workspace: {}, rect: {:?}",
-                window.title, window.owner, window.workspace_id, window.rect
-            );
-        }
+        if log::log_enabled!(log::Level::Debug) {
+            debug!("Window scan found {} windows", current_windows.len());
+            for window in &current_windows {
+                debug!(
+                    "Window: {} ({}), workspace: {}, rect: {:?}",
+                    window.title, window.owner, window.workspace_id, window.rect
+                );
+            }
+        }
     }
     Err(err) => error!("Failed to enumerate windows: {}", err),
 }

Alternatively, consider demoting to trace! or making this entire block configurable via your logging framework.

📝 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
debug!("Window scan found {} windows", current_windows.len());
for window in &current_windows {
debug!(
"Window: {} ({}), workspace: {}, rect: {:?}",
window.title, window.owner, window.workspace_id, window.rect
);
}
match CGWindowInfo::get_all_windows() {
Ok(current_windows) => {
if log::log_enabled!(log::Level::Debug) {
debug!("Window scan found {} windows", current_windows.len());
for window in &current_windows {
debug!(
"Window: {} ({}), workspace: {}, rect: {:?}",
window.title, window.owner, window.workspace_id, window.rect
);
}
}
}
Err(err) => error!("Failed to enumerate windows: {}", err),
}
🤖 Prompt for AI Agents
In src/macos/window_system.rs around lines 137 to 143, the debug logging for
window scanning is potentially too verbose for production use and may impact
performance. To fix this, wrap the debug! calls inside a runtime check that
verifies if the debug log level is enabled before executing the logging
statements. Alternatively, you can change the log level to trace! or make the
logging configurable through the logging framework to control verbosity in
production environments.

Comment on lines 294 to 314
unsafe fn get_dragging_windows(observer: &Object) -> Option<Arc<Mutex<HashMap<WindowId, Rect>>>> {
let ptr: *const std::ffi::c_void = *observer.get_ivar("dragging_windows");
if ptr.is_null() {
return None;
}
let boxed = Box::from_raw(ptr as *mut Arc<Mutex<HashMap<WindowId, Rect>>>);
let result = Some((*boxed).clone());
let _ = Box::into_raw(boxed); // Don't drop it
result
}

unsafe fn get_event_sender(observer: &Object) -> Option<mpsc::Sender<WindowDragEvent>> {
let ptr: *const std::ffi::c_void = *observer.get_ivar("event_sender");
if ptr.is_null() {
return None;
}
let boxed = Box::from_raw(ptr as *mut mpsc::Sender<WindowDragEvent>);
let result = Some((*boxed).clone());
let _ = Box::into_raw(boxed); // Don't drop it
result
}
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

Verify memory safety of helper functions.

The helper functions get_dragging_windows and get_event_sender temporarily convert raw pointers back to boxes but immediately leak them again with Box::into_raw. While this preserves the original pointer for the observer, it's a complex pattern that could be error-prone.

Consider documenting why the temporary Box conversion is necessary and ensure this pattern is safe:


🏁 Script executed:

#!/bin/bash
# Search for similar patterns in the codebase to ensure consistency
rg -A 3 -B 3 "Box::from_raw.*Box::into_raw" --type rust

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Verify Box::into_raw/Box::from_raw pairing by locating where these ivars are initialized.
rg -n "set_ivar(\"dragging_windows\"" --type rust -A3 -B3
rg -n "set_ivar(\"event_sender\"" --type rust -A3 -B3

Length of output: 281


🏁 Script executed:

#!/bin/bash
# Find where raw pointers are boxed back for ivar initialization
rg -n 'Box::into_raw' --type rust -A3 -B3

# Locate all set_ivar calls to see how these ivars are first set
rg -n 'set_ivar\(' --type rust -A3 -B3 --type rust

Length of output: 2734


Ensure memory‐safety and document the boxed‐pointer pattern

The pairing of Box::into_raw (lines 58, 62) and Box::from_raw / Box::into_raw inside get_dragging_windows (299–301) and get_event_sender (310–312) is safe—the steps are:

  • We only ever call Box::from_raw on the original raw pointer stored in the ivar.
  • We clone the inner Arc<Mutex<…>> or mpsc::Sender<…>.
  • We immediately call Box::into_raw again, leaking the Box so the heap allocation remains owned and never freed here.

However, it’s easy to misapply this pattern. Please:

  • Add a comment at the set_ivar site (lines 56–63) explaining why we box the data for the Objective-C ivar.
  • Document in get_dragging_windows and get_event_sender why we reconstruct the Box just to clone its contents and then re‐leak it.
  • Note explicitly that the Box is never dropped here, so there’s no double‐free.

Optionally, for clarity you could replace:

Box::into_raw(Box::new(dragging_windows)) as *const _ as *const c_void

with:

let ptr = Box::leak(Box::new(dragging_windows)) as *mut _ as *const c_void;

which makes the “leaking” intent more explicit.

Let me know if you’d like to explore that refactor!

🤖 Prompt for AI Agents
In src/macos/window_notifications.rs around lines 294 to 314, add comments in
get_dragging_windows and get_event_sender explaining that the Box is temporarily
reconstructed from the raw pointer only to clone the inner Arc or Sender, then
immediately leaked again with Box::into_raw to avoid dropping and double-free.
Also, at the set_ivar site around lines 56 to 63, add a comment clarifying why
the data is boxed before storing as an Objective-C ivar, emphasizing ownership
and memory safety. Optionally, consider replacing Box::into_raw(Box::new(...))
with Box::leak(Box::new(...)) to make the leaking intent clearer.

Comment on lines 515 to 642
async fn handle_immediate_window_positioning(
&mut self,
window_id: WindowId,
new_rect: Rect,
) -> Result<()> {
// Skip immediate positioning if this window is being dragged via NSWindow notifications
// The NSWindow drag system will handle the positioning when the drag ends
if self.user_dragging_windows.contains(&window_id) {
debug!(
"Skipping immediate positioning for window {:?} - NSWindow drag in progress",
window_id
);
// Still update our internal state
self.previous_window_positions.insert(window_id, new_rect);
if let Some(window) = self.windows.get_mut(&window_id) {
window.rect = new_rect;
}
return Ok(());
}

let previous_rect = self.previous_window_positions.get(&window_id).copied();

// Update our records first
self.previous_window_positions.insert(window_id, new_rect);
if let Some(window) = self.windows.get_mut(&window_id) {
window.rect = new_rect;
}

if let Some(prev_rect) = previous_rect {
// Check if this is a significant move that suggests user repositioning
let dx = (new_rect.x - prev_rect.x).abs();
let dy = (new_rect.y - prev_rect.y).abs();
let distance = (dx * dx + dy * dy).sqrt();

// If window moved significantly, immediately check for snap zones
if distance > 20.0 {
// Higher threshold for immediate snapping
debug!(
"Window {:?} moved significantly from {:?} to {:?}, checking snap zones",
window_id, prev_rect, new_rect
);

// Check if the window center is in a snap zone
let center_x = new_rect.x + new_rect.width / 2.0;
let center_y = new_rect.y + new_rect.height / 2.0;

// Check which zone the window is in
let current_zone = self.snap_manager.find_zone_at_point(center_x, center_y);

match current_zone {
Some(crate::snap::SnapRegion::Center) => {
// Center zone: check for window swap first
let effective_workspace = self.get_effective_current_workspace();
let workspace_windows: Vec<&Window> = self
.windows
.values()
.filter(|w| w.workspace_id == effective_workspace && !w.is_minimized)
.collect();

if let Some(target_window_id) = self.snap_manager.find_window_under_drag(
window_id,
new_rect,
&workspace_windows,
) {
debug!(
"🔄 Window in center zone over another window, swapping positions"
);
self.swap_windows(window_id, target_window_id).await?;
} else {
debug!("↩️ Window in center zone but no target, returning to original");
self.return_window_to_original(window_id, prev_rect).await?;
}
}
Some(_) => {
// Edge or corner zone: snap to that zone
if let Some(snap_rect) = self.snap_manager.find_snap_target(new_rect) {
// Check if we need to snap (avoid redundant moves)
let snap_dx = (snap_rect.x - new_rect.x).abs();
let snap_dy = (snap_rect.y - new_rect.y).abs();
let snap_dw = (snap_rect.width - new_rect.width).abs();
let snap_dh = (snap_rect.height - new_rect.height).abs();

if snap_dx > 10.0 || snap_dy > 10.0 || snap_dw > 10.0 || snap_dh > 10.0
{
debug!(
"📍 Snapping window {:?} to zone at {:?}",
window_id, snap_rect
);

// Mark as programmatic move to avoid feedback loop
self.programmatically_moving.insert(window_id);

// Move the window to snap position
match self.macos.move_window(window_id, snap_rect).await {
Ok(_) => {
debug!("✅ Successfully snapped window {:?}", window_id);
// Update our internal state
if let Some(window) = self.windows.get_mut(&window_id) {
window.rect = snap_rect;
}
self.previous_window_positions.insert(window_id, snap_rect);
}
Err(e) => {
warn!("❌ Failed to snap window {:?}: {}, returning to original", window_id, e);
self.return_window_to_original(window_id, prev_rect)
.await?;
}
}
}
}
}
None => {
// Outside any zone: return to original
debug!("🚫 Window outside all zones, returning to original");
self.return_window_to_original(window_id, prev_rect).await?;
}
}
}
} else {
// First time seeing this window
debug!(
"Recording initial position for window {:?}: {:?}",
window_id, new_rect
);
}

Ok(())
}
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 distance threshold configurability.

The immediate positioning logic uses hardcoded thresholds (20.0 for movement detection, 10.0 for snap detection). These should be configurable for different user preferences and display sizes.

Make thresholds configurable:

+            // TODO: Make these thresholds configurable via skew.toml:
+            // - 'movement_detection_threshold' (current: 20.0px)
+            // - 'snap_detection_threshold' (current: 10.0px)
             if distance > 20.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
async fn handle_immediate_window_positioning(
&mut self,
window_id: WindowId,
new_rect: Rect,
) -> Result<()> {
// Skip immediate positioning if this window is being dragged via NSWindow notifications
// The NSWindow drag system will handle the positioning when the drag ends
if self.user_dragging_windows.contains(&window_id) {
debug!(
"Skipping immediate positioning for window {:?} - NSWindow drag in progress",
window_id
);
// Still update our internal state
self.previous_window_positions.insert(window_id, new_rect);
if let Some(window) = self.windows.get_mut(&window_id) {
window.rect = new_rect;
}
return Ok(());
}
let previous_rect = self.previous_window_positions.get(&window_id).copied();
// Update our records first
self.previous_window_positions.insert(window_id, new_rect);
if let Some(window) = self.windows.get_mut(&window_id) {
window.rect = new_rect;
}
if let Some(prev_rect) = previous_rect {
// Check if this is a significant move that suggests user repositioning
let dx = (new_rect.x - prev_rect.x).abs();
let dy = (new_rect.y - prev_rect.y).abs();
let distance = (dx * dx + dy * dy).sqrt();
// If window moved significantly, immediately check for snap zones
if distance > 20.0 {
// Higher threshold for immediate snapping
debug!(
"Window {:?} moved significantly from {:?} to {:?}, checking snap zones",
window_id, prev_rect, new_rect
);
// Check if the window center is in a snap zone
let center_x = new_rect.x + new_rect.width / 2.0;
let center_y = new_rect.y + new_rect.height / 2.0;
// Check which zone the window is in
let current_zone = self.snap_manager.find_zone_at_point(center_x, center_y);
match current_zone {
Some(crate::snap::SnapRegion::Center) => {
// Center zone: check for window swap first
let effective_workspace = self.get_effective_current_workspace();
let workspace_windows: Vec<&Window> = self
.windows
.values()
.filter(|w| w.workspace_id == effective_workspace && !w.is_minimized)
.collect();
if let Some(target_window_id) = self.snap_manager.find_window_under_drag(
window_id,
new_rect,
&workspace_windows,
) {
debug!(
"🔄 Window in center zone over another window, swapping positions"
);
self.swap_windows(window_id, target_window_id).await?;
} else {
debug!("↩️ Window in center zone but no target, returning to original");
self.return_window_to_original(window_id, prev_rect).await?;
}
}
Some(_) => {
// Edge or corner zone: snap to that zone
if let Some(snap_rect) = self.snap_manager.find_snap_target(new_rect) {
// Check if we need to snap (avoid redundant moves)
let snap_dx = (snap_rect.x - new_rect.x).abs();
let snap_dy = (snap_rect.y - new_rect.y).abs();
let snap_dw = (snap_rect.width - new_rect.width).abs();
let snap_dh = (snap_rect.height - new_rect.height).abs();
if snap_dx > 10.0 || snap_dy > 10.0 || snap_dw > 10.0 || snap_dh > 10.0
{
debug!(
"📍 Snapping window {:?} to zone at {:?}",
window_id, snap_rect
);
// Mark as programmatic move to avoid feedback loop
self.programmatically_moving.insert(window_id);
// Move the window to snap position
match self.macos.move_window(window_id, snap_rect).await {
Ok(_) => {
debug!("✅ Successfully snapped window {:?}", window_id);
// Update our internal state
if let Some(window) = self.windows.get_mut(&window_id) {
window.rect = snap_rect;
}
self.previous_window_positions.insert(window_id, snap_rect);
}
Err(e) => {
warn!("❌ Failed to snap window {:?}: {}, returning to original", window_id, e);
self.return_window_to_original(window_id, prev_rect)
.await?;
}
}
}
}
}
None => {
// Outside any zone: return to original
debug!("🚫 Window outside all zones, returning to original");
self.return_window_to_original(window_id, prev_rect).await?;
}
}
}
} else {
// First time seeing this window
debug!(
"Recording initial position for window {:?}: {:?}",
window_id, new_rect
);
}
Ok(())
}
let distance = (dx * dx + dy * dy).sqrt();
// If window moved significantly, immediately check for snap zones
+ // TODO: Make these thresholds configurable via skew.toml:
+ // - 'movement_detection_threshold' (current: 20.0px)
+ // - 'snap_detection_threshold' (current: 10.0px)
if distance > 20.0 {
// Higher threshold for immediate snapping
debug!(
"Window {:?} moved significantly from {:?} to {:?}, checking snap zones",
window_id, prev_rect, new_rect
);
🤖 Prompt for AI Agents
In src/window_manager.rs around lines 515 to 642, the distance thresholds for
movement detection (20.0) and snap detection (10.0) are hardcoded. Refactor the
code to replace these magic numbers with configurable parameters, such as fields
in the struct or constants that can be set via configuration. Update the
function to use these configurable thresholds instead of fixed values, allowing
users to adjust them based on preferences or display characteristics.

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: 9

♻️ Duplicate comments (2)
src/layout.rs (1)

185-185: Ensure MSRV ≥ 1.82.0 and declare it in Cargo.toml.

Your code uses newer Rust features:

  • is_some_and (lines 185, 227-232) requires Rust 1.70.0+
  • is_none_or (line 200-201) requires Rust 1.82.0+

Without declaring the MSRV, users with older Rust versions will encounter compilation errors.

Add to your Cargo.toml under [package]:

rust-version = "1.82.0"
#!/bin/bash
# Check current MSRV declaration
grep -E "rust-version|edition" Cargo.toml

Also applies to: 200-201, 227-232

src/window_manager.rs (1)

575-575: Make distance thresholds configurable.

Multiple hardcoded distance thresholds (0.5px, 10.0px) are used throughout the code. These should be configurable for different display characteristics and user preferences.

Consider adding threshold configuration:

pub struct WindowConfig {
    pub movement_detection_threshold: f64,  // Default: 0.5
    pub snap_detection_threshold: f64,      // Default: 10.0
    // ... other fields
}

Also applies to: 589-589, 694-694

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

97-99: Make log file path configurable.

The log file path is hardcoded to skew.log in the current directory. This should be configurable and respect standard logging conventions.

Consider making the log file path configurable:

-let log_file = std::env::current_dir()
-    .unwrap_or_else(|_| PathBuf::from("."))
-    .join("skew.log");
+let log_file = std::env::var("SKEW_LOG_FILE")
+    .map(PathBuf::from)
+    .unwrap_or_else(|_| {
+        std::env::current_dir()
+            .unwrap_or_else(|_| PathBuf::from("."))
+            .join("skew.log")
+    });

33-47: Consider log rotation and file size management.

The current implementation appends indefinitely to the log file without any rotation or size limits, which could lead to very large log files over time.

Consider implementing log rotation features:

  • Maximum file size limits
  • Rotating to numbered backup files
  • Configurable retention policies

You might also consider using established logging crates like tracing-appender or log4rs that provide these features out of the box.

src/snap.rs (4)

307-307: Make minimum drag distance configurable.

The minimum drag distance of 20.0 pixels is hardcoded. This should be configurable as user preferences for drag sensitivity vary.

Consider making this configurable:

-let min_distance = 20.0; // Fixed minimum distance - simpler than using snap_threshold
+let min_distance = self.snap_threshold * 0.4; // Scale with snap threshold

Or add it as a separate configurable parameter.


352-352: Consider floating-point precision for snap detection.

The hardcoded threshold of 10.0 for snap detection could be problematic on high-DPI displays or with floating-point precision issues.

Consider using a relative threshold:

-if dx > 10.0 || dy > 10.0 || dw > 10.0 || dh > 10.0 {
+let epsilon = 1.0; // Or make configurable
+if dx > epsilon || dy > epsilon || dw > epsilon || dh > epsilon {

92-216: Simplify zone configuration with a data structure.

The zone configuration is complex and repetitive. Consider using a more structured approach to define zones.

Consider using a more structured approach:

struct ZoneConfig {
    region: SnapRegion,
    bounds_fn: fn(Rect, f64, f64, f64) -> (f64, f64, f64, f64),
    snap_fn: fn(Rect, f64) -> (f64, f64, f64, f64),
}

const ZONE_CONFIGS: &[ZoneConfig] = &[
    ZoneConfig {
        region: SnapRegion::Center,
        bounds_fn: |screen, _, _, _| (0.2, 0.2, 0.6, 0.6),
        snap_fn: |screen, _| (0.25, 0.25, 0.5, 0.5),
    },
    // ... other zones
];

This would reduce duplication and make the zone definitions more maintainable.


58-59: Remove unused field warning.

The window_id field in WindowDragState is marked with #[allow(dead_code)] but could be useful for debugging or validation.

Consider either using this field for validation or removing it entirely:

struct WindowDragState {
-    #[allow(dead_code)]
-    window_id: WindowId,
    initial_rect: Rect,
    is_dragging: bool,
    drag_start_time: std::time::Instant,
}
src/layout.rs (3)

293-296: Validate window size constants against screen dimensions.

The hardcoded minimum and maximum window sizes may not be appropriate for all screen sizes, especially smaller displays or very large monitors.

Consider making these relative to screen size or configurable:

-const MIN_WINDOW_WIDTH: f64 = 150.0;  // Reduced for better space usage
-const MIN_WINDOW_HEIGHT: f64 = 100.0; // Reduced for better space usage
-const MAX_WINDOW_WIDTH: f64 = 5000.0; // Increased for larger monitors
-const MAX_WINDOW_HEIGHT: f64 = 4000.0; // Increased for larger monitors
+fn get_window_constraints(screen_rect: Rect) -> (f64, f64, f64, f64) {
+    let min_width = (screen_rect.width * 0.1).max(150.0);
+    let min_height = (screen_rect.height * 0.1).max(100.0);
+    let max_width = screen_rect.width * 0.9;
+    let max_height = screen_rect.height * 0.9;
+    (min_width, min_height, max_width, max_height)
+}

145-181: Consider BSP tree rebalancing for better space utilization.

The spiral insertion pattern always inserts into the rightmost/bottommost position, which can create unbalanced trees over time, leading to poor space utilization.

Consider implementing a rebalancing mechanism or alternative insertion strategies:

fn insert_window_balanced(&mut self, window_id: WindowId, split_ratio: f64) {
    // Find the leaf with the largest area for splitting
    // Or implement periodic tree rebalancing
}

This would help maintain better window size distribution, especially with many windows.


377-398: Optimize BSP layout computation.

The BSP layout always rebuilds the entire tree from scratch, which could be inefficient for large numbers of windows.

Consider implementing incremental updates:

fn update_bsp_tree_incrementally(&mut self, added: &[WindowId], removed: &[WindowId]) {
    // Only modify affected parts of the tree
    // This would be more efficient than full rebuilds
}

However, the current approach is simpler and more reliable, so this optimization should only be considered if performance becomes an issue.

src/window_manager.rs (2)

82-92: Consider using more descriptive field names.

The boolean tracking fields could be more descriptive to clarify their purpose.

Consider renaming for clarity:

-programmatically_moving: std::collections::HashSet<WindowId>,
-user_dragging_windows: std::collections::HashSet<WindowId>,
+windows_being_moved_programmatically: std::collections::HashSet<WindowId>,
+windows_being_dragged_by_user: std::collections::HashSet<WindowId>,

1172-1198: Add exponential backoff for retry logic.

The retry logic uses a fixed 50ms delay between attempts. Consider implementing exponential backoff for better resilience.

-tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
+let delay = 50 * (1 << attempt); // Exponential backoff: 50ms, 100ms, 200ms
+tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between aac5b44 and 0bae4b9.

⛔ Files ignored due to path filters (13)
  • 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 (5)
  • src/layout.rs (15 hunks)
  • src/macos/window_system.rs (7 hunks)
  • src/main.rs (2 hunks)
  • src/snap.rs (1 hunks)
  • src/window_manager.rs (14 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/macos/window_system.rs

Comment on lines +94 to 100
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 {
Rect::new(
self.rect.x,
self.rect.y,
self.rect.width,
self.rect.height * self.split_ratio,
)
ideal_left_width
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve minimum size constraint logic.

The minimum size constraint logic is complex and could be simplified. The nested conditionals make it hard to understand the intended behavior.

Consider simplifying the logic:

-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 = ideal_left_width
+    .max(MIN_SPLIT_SIZE.min(self.rect.width * 0.3))
+    .min(self.rect.width - MIN_SPLIT_SIZE.min(self.rect.width * 0.3));

This would be more readable and easier to reason about.

Also applies to: 117-123

🤖 Prompt for AI Agents
In src/layout.rs around lines 94 to 100, the minimum size constraint logic for
left_width is overly complex with nested conditionals, making it hard to
understand. Simplify the logic by using clearer, more straightforward conditions
or by breaking the calculation into smaller steps or helper functions to improve
readability and maintainability. Apply the same simplification approach to the
similar code block at lines 117 to 123.

@@ -1,7 +1,11 @@
use clap::{Parser, Subcommand};
use log::{error, info};
use log::info;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add missing chrono dependency import.

The code uses chrono::Local::now() on line 56 but doesn't import the chrono crate. This will cause a compilation error.

Add the missing import:

 use clap::{Parser, Subcommand};
 use log::info;
+use chrono;
 use skew::{Config, Result, WindowManager};

Also ensure chrono is added to your Cargo.toml dependencies if not already present.

📝 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
use log::info;
use clap::{Parser, Subcommand};
use log::info;
use chrono;
use skew::{Config, Result, WindowManager};
🤖 Prompt for AI Agents
In src/main.rs at line 2, the chrono crate is used in the code but not imported,
causing a compilation error. Add the line `use chrono::Local;` near the top of
the file to import the necessary chrono module. Also verify that chrono is
listed as a dependency in Cargo.toml to ensure it compiles correctly.

Comment on lines +70 to +73
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

Improve error handling in logging operations.

The logging operations silently ignore write failures with let _ = file.write_all(...) and let _ = file.flush(). This could mask important errors like disk full conditions.

Consider improving error handling:

 // 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!("Warning: Failed to write to log file: {}", e);
+    }
+    if let Err(e) = file.flush() {
+        eprintln!("Warning: Failed to flush log file: {}", e);
+    }
 }
🤖 Prompt for AI Agents
In src/main.rs around lines 70 to 73, the current logging code ignores errors
from write_all and flush calls, which can hide critical issues like disk full
errors. Modify the code to properly handle these errors by checking the Result
returned from write_all and flush, and logging or returning an appropriate error
message instead of silently ignoring failures.

Comment on lines +85 to +87
let edge_zone_width = 100.0; // Narrower zones to reduce interference
let corner_size = 80.0; // Smaller corner zones
let margin = 5.0; // Smaller margin for more screen space
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 zone dimensions configurable.

The zone dimensions are hardcoded with magic numbers (100.0, 80.0, 5.0). These should be configurable for different screen sizes and user preferences.

Consider making these configurable:

-let edge_zone_width = 100.0; // Narrower zones to reduce interference
-let corner_size = 80.0; // Smaller corner zones
-let margin = 5.0; // Smaller margin for more screen space
+let edge_zone_width = self.snap_threshold * 2.0; // Scale with snap threshold
+let corner_size = self.snap_threshold * 1.6; // Proportional to threshold
+let margin = self.snap_threshold * 0.1; // Small relative margin

Or add these as parameters to the SnapManager constructor.

📝 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 edge_zone_width = 100.0; // Narrower zones to reduce interference
let corner_size = 80.0; // Smaller corner zones
let margin = 5.0; // Smaller margin for more screen space
let edge_zone_width = self.snap_threshold * 2.0; // Scale with snap threshold
let corner_size = self.snap_threshold * 1.6; // Proportional to threshold
let margin = self.snap_threshold * 0.1; // Small relative margin
🤖 Prompt for AI Agents
In src/snap.rs around lines 85 to 87, the zone dimension values are hardcoded as
magic numbers. To fix this, refactor the code to make edge_zone_width,
corner_size, and margin configurable by either adding them as parameters to the
SnapManager constructor or by loading them from a configuration source. Replace
the hardcoded values with these configurable parameters to allow flexibility for
different screen sizes and user preferences.

Comment on lines +281 to +382
pub fn end_window_drag(
&mut self,
window_id: WindowId,
final_rect: Rect,
all_windows: &[&Window],
) -> DragResult {
debug!(
"🎬 SnapManager::end_window_drag called for window {:?}",
window_id
);

if let Some(drag_state) = self.window_drag_states.remove(&window_id) {
// Check if the window was dragged for a meaningful amount of time/distance
let drag_duration = drag_state.drag_start_time.elapsed();
let drag_distance = self.calculate_drag_distance(&drag_state.initial_rect, &final_rect);

debug!(
"🕐 Drag ended for window {:?}: duration={}ms, distance={:.1}px, initial={:?}, final={:?}",
window_id,
drag_duration.as_millis(),
drag_distance,
drag_state.initial_rect,
final_rect
);

// Use simpler thresholds to reduce complexity
let min_distance = 20.0; // Fixed minimum distance - simpler than using snap_threshold

if drag_distance > min_distance {
debug!("✅ Drag qualifies for processing, checking targets...");

let center_x = final_rect.x + final_rect.width / 2.0;
let center_y = final_rect.y + final_rect.height / 2.0;

// Determine which zone we're in
let current_zone = self.find_zone_at_point(center_x, center_y);

match current_zone {
Some(SnapRegion::Center) => {
// In center swap zone - check for window to swap with
if let Some(target_window_id) =
self.find_window_under_drag(window_id, final_rect, all_windows)
{
debug!("🔄 Window dropped over another window in swap zone, initiating swap");
return DragResult::SwapWithWindow(
target_window_id,
drag_state.initial_rect,
);
} else {
debug!("📍 In center zone but no window to swap with, returning to original");
return DragResult::ReturnToOriginal(drag_state.initial_rect);
}
}
Some(
SnapRegion::North
| SnapRegion::South
| SnapRegion::East
| SnapRegion::West
| SnapRegion::NorthEast
| SnapRegion::NorthWest
| SnapRegion::SouthEast
| SnapRegion::SouthWest,
) => {
// In warp/corner zone - snap to that zone regardless of other windows
if let Some(snap_rect) = self.find_snap_target(final_rect) {
debug!("🎯 Found warp/corner target: {:?}", snap_rect);
let dx = (snap_rect.x - final_rect.x).abs();
let dy = (snap_rect.y - final_rect.y).abs();
let dw = (snap_rect.width - final_rect.width).abs();
let dh = (snap_rect.height - final_rect.height).abs();

if dx > 10.0 || dy > 10.0 || dw > 10.0 || dh > 10.0 {
debug!("📌 Warping to zone");
return DragResult::SnapToZone(snap_rect);
} else {
debug!("↩️ Already close to warp target, returning to original");
return DragResult::ReturnToOriginal(drag_state.initial_rect);
}
} else {
debug!("🎯❌ No warp target found");
return DragResult::ReturnToOriginal(drag_state.initial_rect);
}
}
None => {
// Outside all zones - return to original position
debug!("🚫 Outside all snap zones, returning to original position");
return DragResult::ReturnToOriginal(drag_state.initial_rect);
}
}
} else {
debug!(
"⏱️ Drag too small for processing (distance: {:.1}px < {:.1}px), returning to original",
drag_distance,
min_distance
);
return DragResult::ReturnToOriginal(drag_state.initial_rect);
}
} else {
debug!("❌ No drag state found for window {:?}", window_id);
}
DragResult::NoAction
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Break down the complex end_window_drag method.

This method is quite long (100+ lines) and handles multiple concerns. Consider breaking it into smaller, focused methods.

Consider extracting helper methods:

fn process_center_zone_drag(&mut self, window_id: WindowId, final_rect: Rect, all_windows: &[&Window]) -> DragResult {
    // Handle center zone logic
}

fn process_warp_zone_drag(&mut self, window_id: WindowId, final_rect: Rect, snap_rect: Rect) -> DragResult {
    // Handle warp zone logic
}

This would improve readability and testability.

🤖 Prompt for AI Agents
In src/snap.rs around lines 281 to 382, the end_window_drag method is too long
and handles multiple concerns, making it hard to read and maintain. Refactor by
extracting the center zone drag logic into a new method named
process_center_zone_drag that takes window_id, final_rect, and all_windows as
parameters and returns DragResult. Similarly, extract the warp/corner zone drag
logic into another method named process_warp_zone_drag that takes window_id,
final_rect, and snap_rect as parameters and returns DragResult. Replace the
corresponding match arms in end_window_drag with calls to these new helper
methods to improve readability and testability.

Comment on lines +161 to +165
// TODO: Make both intervals configurable via skew.toml:
// - 'window_refresh_interval_ms' (current: 1000ms, recommended: 500-2000ms)
// - 'window_monitor_interval_ms' (current: 200ms, recommended: 100-500ms)
// The slower refresh prevents excessive API calls while maintaining accuracy.
let mut refresh_timer = interval(Duration::from_millis(1000));
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 timer intervals configurable.

The hardcoded intervals (1000ms and 200ms) should be configurable as mentioned in the TODO comments.

Consider adding these to your configuration structure:

// In your config structure
pub struct GeneralConfig {
    pub window_refresh_interval_ms: u64,  // Default: 1000
    pub window_monitor_interval_ms: u64,  // Default: 200
    // ... other fields
}

Then use them in the code:

-let mut refresh_timer = interval(Duration::from_millis(1000));
+let mut refresh_timer = interval(Duration::from_millis(self.config.general.window_refresh_interval_ms));
🤖 Prompt for AI Agents
In src/window_manager.rs around lines 161 to 165, the timer intervals are
hardcoded to 1000ms and 200ms but should be configurable as per the TODO
comment. To fix this, add two fields window_refresh_interval_ms and
window_monitor_interval_ms with default values 1000 and 200 respectively to your
configuration struct (e.g., GeneralConfig). Then replace the hardcoded
Duration::from_millis values with these config values when creating the interval
timers.

Comment on lines 556 to 621
async fn handle_user_window_move(
&mut self,
window_id: WindowId,
new_rect: Rect,
) -> Result<()> {
debug!("🎯 handle_user_window_move called for window {:?} at {:?}", window_id, new_rect);

// Get previous position to check for significant movement
let prev_rect = self.last_window_positions.get(&window_id).copied();
debug!(" -> Previous position: {:?}", prev_rect);

if let Some(prev_rect) = prev_rect {
let dx = (new_rect.x - prev_rect.x).abs();
let dy = (new_rect.y - prev_rect.y).abs();
let distance = (dx * dx + dy * dy).sqrt();

debug!(" -> Movement distance: {:.1}px (dx: {:.1}, dy: {:.1})", distance, dx, dy);

// Very low threshold to catch even small movements (>0.5px)
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);

// If window is significantly displaced from its layout position, snap it back
if layout_distance > 10.0 {
info!("📌 Snapping window {:?} back to layout at {:?}", window_id, target_rect);
self.programmatically_moving.insert(window_id);

if let Err(e) = self.macos.move_window(window_id, target_rect).await {
warn!("❌ Failed to snap window back to layout: {}", e);
} else {
// Update our internal state
if let Some(window) = self.windows.get_mut(&window_id) {
window.rect = target_rect;
}
debug!("✅ Successfully snapped window {:?} back to layout", window_id);
}
return Ok(());
} else {
debug!("📍 Window already close to layout position: {:.1}px", layout_distance);
}
} else {
debug!("🚫 No layout target found for window {:?}", window_id);
}

// If no snapping occurred, just note the move - don't reapply layout immediately
// This prevents feedback loops where layout application triggers more moves
debug!("Manual window move detected - layout may be disrupted");
} else {
debug!("⚠️ Movement too small ({:.1}px) - ignoring", distance);
}
} else {
debug!("📍 First position recorded for window {:?} - no snapping available yet", window_id);
}

Ok(())
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify complex user move handling.

The handle_user_window_move method is quite complex with nested conditions and multiple concerns. Consider breaking it down.

Consider extracting helper methods:

fn should_snap_to_layout(&self, window_id: WindowId, current_rect: Rect, target_rect: Rect) -> bool {
    // Extract snapping decision logic
}

fn perform_layout_snap(&mut self, window_id: WindowId, target_rect: Rect) -> Result<()> {
    // Extract snapping execution logic
}

This would improve readability and testability.

🤖 Prompt for AI Agents
In src/window_manager.rs around lines 556 to 621, the handle_user_window_move
function is complex with nested conditions handling movement detection and
snapping logic. Refactor by extracting the snapping decision logic into a helper
method like should_snap_to_layout that takes window_id, current_rect, and
target_rect and returns a boolean indicating if snapping is needed. Also extract
the snapping execution logic into another helper method perform_layout_snap that
performs the move and updates state, returning a Result. Replace the inlined
logic in handle_user_window_move with calls to these helpers to improve
readability and testability.

Comment on lines +717 to +801
async fn swap_windows(&mut self, window1_id: WindowId, window2_id: WindowId) -> Result<()> {
// Get the most current window positions, not cached ones
let current_windows = self.macos.get_windows().await?;

let window1_current = current_windows.iter().find(|w| w.id == window1_id);
let window2_current = current_windows.iter().find(|w| w.id == window2_id);

if let (Some(window1), Some(window2)) = (window1_current, window2_current) {
let window1_rect = window1.rect;
let window2_rect = window2.rect;

debug!(
"🔄 Swapping positions of windows {:?} (at {:?}) and {:?} (at {:?})",
window1_id, window1_rect, window2_id, window2_rect
);

// Mark both as programmatic moves to avoid feedback loops
self.programmatically_moving.insert(window1_id);
self.programmatically_moving.insert(window2_id);

// Create swap layout
let mut swap_layouts = HashMap::new();
swap_layouts.insert(window1_id, window2_rect);
swap_layouts.insert(window2_id, window1_rect);

// Try bulk move first (more reliable)
let both_windows = vec![window1.clone(), window2.clone()];
match self
.macos
.move_all_windows(&swap_layouts, &both_windows)
.await
{
Ok(_) => {
debug!("✅ Successfully swapped windows using bulk move");
// Update our internal state
if let Some(w) = self.windows.get_mut(&window1_id) {
w.rect = window2_rect;
}
if let Some(w) = self.windows.get_mut(&window2_id) {
w.rect = window1_rect;
}
self.last_window_positions
.insert(window1_id, window2_rect);
self.last_window_positions
.insert(window2_id, window1_rect);
}
Err(e) => {
warn!("Bulk swap failed, trying individual moves: {}", e);

// Fallback to individual moves
match self.macos.move_window(window1_id, window2_rect).await {
Ok(_) => {
if let Some(w) = self.windows.get_mut(&window1_id) {
w.rect = window2_rect;
}
self.last_window_positions
.insert(window1_id, window2_rect);
}
Err(e) => {
warn!("Failed to move window {:?} during swap: {}", window1_id, e)
}
}

match self.macos.move_window(window2_id, window1_rect).await {
Ok(_) => {
if let Some(w) = self.windows.get_mut(&window2_id) {
w.rect = window1_rect;
}
self.last_window_positions
.insert(window2_id, window1_rect);
}
Err(e) => {
warn!("Failed to move window {:?} during swap: {}", window2_id, e)
}
}
}
}
} else {
warn!(
"Could not find current positions for windows {:?} and {:?}",
window1_id, window2_id
);
}
Ok(())
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Reduce code duplication in swap methods.

The swap_windows and swap_windows_with_rects methods have significant code duplication in their bulk move and fallback logic.

Consider extracting the common swap logic:

async fn execute_swap(&mut self, window1_id: WindowId, window2_id: WindowId, rect1: Rect, rect2: Rect) -> Result<()> {
    // Common swap execution logic
}

Then call this from both methods to reduce duplication.

Also applies to: 803-898

🤖 Prompt for AI Agents
In src/window_manager.rs around lines 717 to 801, the swap_windows and
swap_windows_with_rects methods duplicate the bulk move and fallback logic for
swapping windows. To fix this, extract the common swap execution code into a new
async method, e.g., execute_swap, that takes the two window IDs and their target
rectangles as parameters. Then refactor both swap_windows and
swap_windows_with_rects to call this new method, passing the appropriate
arguments, thereby reducing code duplication and improving maintainability.

Comment on lines 1074 to 1212
async fn apply_layout_internal(&mut self, force: bool) -> Result<()> {
// Throttle layout applications to prevent rapid fire updates, unless forced
if !force {
let now = std::time::Instant::now();
if let Some(last_time) = self.last_layout_time {
if now.duration_since(last_time).as_millis() < 200 {
debug!("Layout application throttled - too soon since last application");
return Ok(());
}
}
self.last_layout_time = Some(now);
} else {
self.last_layout_time = Some(std::time::Instant::now());
debug!("🚀 Forcing layout application");
}

// Use effective workspace detection for more reliable filtering
let effective_workspace = self.get_effective_current_workspace();

// Get windows in the effective current workspace - collect to owned to avoid borrow issues
let workspace_windows: Vec<Window> = self
.windows
.values()
.filter(|w| w.workspace_id == self.current_workspace && !w.is_minimized)
.filter(|w| w.workspace_id == effective_workspace && !w.is_minimized)
.cloned()
.collect();

if workspace_windows.is_empty() {
debug!("No windows to layout");
debug!("No windows to layout in workspace {}", effective_workspace);
return Ok(());
}

debug!("Applying layout to {} windows using {:?}", workspace_windows.len(), self.layout_manager.get_current_layout());
debug!("=== LAYOUT APPLICATION START ===");
debug!("Found {} windows to layout in workspace {}", workspace_windows.len(), effective_workspace);


debug!(
"Applying layout to {} windows in workspace {} using {:?}",
workspace_windows.len(),
effective_workspace,
self.layout_manager.get_current_layout()
);

for window in &workspace_windows {
debug!(
" Window to layout: {} ({}) at {:?}",
window.title, window.owner, window.rect
);
}

let screen_rect = self.macos.get_screen_rect().await?;
let workspace_refs: Vec<&Window> = workspace_windows.iter().collect();
let layouts = self.layout_manager.compute_layout(
&workspace_windows,
&workspace_refs,
screen_rect,
&self.config.general,
);

for (window_id, rect) in layouts {
debug!("Applying layout: moving window {:?} to {:?}", window_id, rect);
self.macos.move_window(window_id, rect).await?;
// Update our internal window state
if let Some(window) = self.windows.get_mut(&window_id) {
window.rect = rect;
debug!("Layout computation complete: {} positions calculated", layouts.len());
for (window_id, rect) in &layouts {
debug!(" -> Window {:?} should be at {:?}", window_id, rect);
}

// Mark all windows as being moved programmatically
for window_id in layouts.keys() {
self.programmatically_moving.insert(*window_id);
}

// Use the new move_all_windows method to handle all windows at once
let workspace_windows_vec: Vec<Window> = workspace_windows.clone();
match self
.macos
.move_all_windows(&layouts, &workspace_windows_vec)
.await
{
Ok(_) => {
debug!("Successfully applied layout to all windows");
// Update our internal window state AND position tracking
for (window_id, rect) in layouts {
if let Some(window) = self.windows.get_mut(&window_id) {
window.rect = rect;
}
// Update position tracking to prevent feedback loops
self.last_window_positions.insert(window_id, rect);
}
}
Err(e) => {
warn!(
"Failed to apply layout to all windows: {}, falling back to individual moves",
e
);

// Fall back to individual window moves
for (window_id, rect) in layouts {
debug!(
"Applying layout: moving window {:?} to {:?}",
window_id, rect
);
for attempt in 0..3 {
match self.macos.move_window(window_id, rect).await {
Ok(_) => {
debug!(
"Successfully moved window {:?} on attempt {}",
window_id,
attempt + 1
);
break;
}
Err(e) if attempt < 2 => {
debug!(
"Failed to move window {:?} on attempt {}: {}, retrying",
window_id,
attempt + 1,
e
);
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}
Err(e) => {
warn!(
"Failed to move window {:?} after 3 attempts: {}",
window_id, e
);
}
}
}

// Update our internal window state and position tracking
if let Some(window) = self.windows.get_mut(&window_id) {
window.rect = rect;
}
// Update position tracking to prevent feedback loops
self.last_window_positions.insert(window_id, rect);
}
}
}

debug!("=== LAYOUT APPLICATION END ===");
Ok(())
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Break down the large apply_layout_internal method.

This method is over 130 lines and handles multiple concerns: throttling, workspace detection, layout computation, and bulk operations.

Consider breaking it into focused methods:

async fn should_apply_layout(&self, force: bool) -> bool {
    // Throttling logic
}

async fn compute_workspace_layout(&mut self) -> Result<(HashMap<WindowId, Rect>, Vec<Window>)> {
    // Layout computation logic
}

async fn execute_layout_moves(&mut self, layouts: HashMap<WindowId, Rect>, windows: Vec<Window>) -> Result<()> {
    // Move execution logic
}

This would improve maintainability and testability.

🤖 Prompt for AI Agents
In src/window_manager.rs from lines 1074 to 1212, the apply_layout_internal
method is too large and handles multiple concerns like throttling, workspace
detection, layout computation, and window moving. Refactor by extracting the
throttling logic into an async should_apply_layout method returning a bool, the
layout computation into an async compute_workspace_layout method returning the
layouts and windows, and the window moving logic into an async
execute_layout_moves method that takes the layouts and windows as parameters.
Then update apply_layout_internal to call these smaller methods sequentially,
improving readability, maintainability, and testability.

@plyght plyght closed this Jul 24, 2025
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