From 51b7de23fae9252d0c4d9b12bff1afd803c947d7 Mon Sep 17 00:00:00 2001 From: ksmith Date: Fri, 16 Jan 2026 01:34:29 +0000 Subject: [PATCH] Initial commit - GridSnap Cinnamon Extension --- .gitignore | 14 +++ README.md | 132 +++++++++++++++++++++ extension.js | 308 +++++++++++++++++++++++++++++++++++++++++++++++++ icon.svg | 11 ++ install.sh | 34 ++++++ metadata.json | 8 ++ stylesheet.css | 22 ++++ 7 files changed, 529 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 extension.js create mode 100644 icon.svg create mode 100755 install.sh create mode 100644 metadata.json create mode 100644 stylesheet.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d7b7a9c --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Editor files +*~ +*.swp +*.swo +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db + +# Temporary files +*.tmp +*.bak diff --git a/README.md b/README.md new file mode 100644 index 0000000..49b1604 --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# GridSnap for Cinnamon Desktop + +A window tiling manager extension for Linux Mint's Cinnamon Desktop, inspired by Microsoft PowerToys FancyZones. + +## Features + +- **Multiple Zone Layouts**: Pre-configured layouts including 2x2 grid, 3 columns, and focus layouts +- **Visual Overlay**: See your zones while dragging windows +- **Keyboard Shortcuts**: Quick snap to zones using hotkeys +- **Layout Cycling**: Switch between different zone layouts on the fly + +## Installation + +1. Copy the extension folder to your Cinnamon extensions directory: + ```bash + cp -r gridsnap-cinnamon ~/.local/share/cinnamon/extensions/gridsnap@cinnamon-extension + ``` + +2. Restart Cinnamon (Alt+F2, type 'r', press Enter) or log out and back in + +3. Enable the extension: + - Open System Settings → Extensions + - Find "GridSnap" + - Toggle it on + +## Usage + +### Keyboard Shortcuts + +- **Super + Z**: Toggle zone overlay (show/hide zones) +- **Super + Shift + Z**: Cycle through different layouts +- **Super + Numpad 1-9**: Snap focused window to zone 1-9 + +### Mouse Usage + +1. Hold **Shift** while dragging a window to see the zone overlay +2. Drop the window over a zone to snap it there +3. Release Shift or move outside zones to cancel + +## Available Layouts + +1. **2x2 Grid**: Four equal quadrants +2. **3 Columns**: Three equal vertical columns +3. **Focus Left**: Large left area with two smaller right zones + +## Customization + +To add your own layouts, edit the `DEFAULT_LAYOUTS` object in `extension.js`: + +```javascript +const DEFAULT_LAYOUTS = { + 'custom-layout': { + name: 'My Custom Layout', + zones: [ + { x: 0, y: 0, width: 0.5, height: 1 }, // Left half + { x: 0.5, y: 0, width: 0.5, height: 1 } // Right half + ] + } +}; +``` + +Zone coordinates are relative (0.0 to 1.0): +- `x`: Horizontal position (0 = left edge, 1 = right edge) +- `y`: Vertical position (0 = top edge, 1 = bottom edge) +- `width`: Zone width as fraction of screen width +- `height`: Zone height as fraction of screen height + +## Troubleshooting + +### Extension won't load +- Check the Looking Glass console (Alt+F2, type 'lg', press Enter) for errors +- Ensure the UUID in metadata.json matches the folder name + +### Overlay doesn't show +- Make sure no other extensions conflict with window management +- Try disabling and re-enabling the extension + +### Keybindings don't work +- Check System Settings → Keyboard → Shortcuts for conflicts +- Ensure the extension is enabled + +## Development + +### File Structure +``` +gridsnap@cinnamon-extension/ +├── metadata.json # Extension metadata +├── extension.js # Main extension code +├── stylesheet.css # Visual styling +└── README.md # This file +``` + +### Testing Changes +After editing: +1. Reload Cinnamon: Alt+F2 → 'r' → Enter +2. Or use Looking Glass: Alt+F2 → 'lg' → Enter + +### Debug Logging +Add logging in extension.js: +```javascript +global.log('GridSnap: Your message here'); +``` +View logs with: +```bash +tail -f ~/.cinnamon/glass.log +``` + +## Future Enhancements + +- [ ] Custom layout editor GUI +- [ ] Per-monitor zone configurations +- [ ] Save window positions and restore on login +- [ ] Zone layout import/export +- [ ] More pre-configured layouts +- [ ] Animation effects +- [ ] Settings panel for configuration + +## Contributing + +Feel free to fork and submit pull requests! Areas that could use help: +- Additional layout presets +- UI improvements for the overlay +- Settings GUI implementation +- Multi-monitor support improvements + +## License + +MIT License - Feel free to use and modify as needed. + +## Credits + +Inspired by Microsoft PowerToys FancyZones for Windows. diff --git a/extension.js b/extension.js new file mode 100644 index 0000000..d400919 --- /dev/null +++ b/extension.js @@ -0,0 +1,308 @@ +const St = imports.gi.St; +const Main = imports.ui.main; +const Meta = imports.gi.Meta; +const Clutter = imports.gi.Clutter; +const Lang = imports.lang; +const Mainloop = imports.mainloop; +const Settings = imports.ui.settings; + +let zoneManager; + +// Default zone layouts +const DEFAULT_LAYOUTS = { + 'grid-2x2': { + name: '2x2 Grid', + zones: [ + { x: 0, y: 0, width: 0.5, height: 0.5 }, // Top-left + { x: 0.5, y: 0, width: 0.5, height: 0.5 }, // Top-right + { x: 0, y: 0.5, width: 0.5, height: 0.5 }, // Bottom-left + { x: 0.5, y: 0.5, width: 0.5, height: 0.5 } // Bottom-right + ] + }, + 'columns-3': { + name: '3 Columns', + zones: [ + { x: 0, y: 0, width: 0.33, height: 1 }, + { x: 0.33, y: 0, width: 0.34, height: 1 }, + { x: 0.67, y: 0, width: 0.33, height: 1 } + ] + }, + 'focus-left': { + name: 'Focus Left', + zones: [ + { x: 0, y: 0, width: 0.7, height: 1 }, // Main left + { x: 0.7, y: 0, width: 0.3, height: 0.5 }, // Top-right + { x: 0.7, y: 0.5, width: 0.3, height: 0.5 } // Bottom-right + ] + } +}; + +function ZoneManager() { + this._init(); +} + +ZoneManager.prototype = { + _init: function() { + this.overlay = null; + this.currentLayout = 'grid-2x2'; + this.layouts = DEFAULT_LAYOUTS; + this.isShowing = false; + this.dragInProgress = false; + + // Connect to window grab events + this._connectSignals(); + this._setupKeybindings(); + }, + + _connectSignals: function() { + // Monitor for window grab begin/end + this.grabOpBeginId = global.display.connect('grab-op-begin', + Lang.bind(this, this._onGrabBegin)); + this.grabOpEndId = global.display.connect('grab-op-end', + Lang.bind(this, this._onGrabEnd)); + }, + + _setupKeybindings: function() { + // Add keybinding to show zones overlay (Super+Z) + Main.keybindingManager.addHotKey( + 'show-zones', + 'z', + Lang.bind(this, this._toggleZonesOverlay) + ); + + // Add keybindings for quick snap to zones (Super+Numpad) + for (let i = 1; i <= 9; i++) { + Main.keybindingManager.addHotKey( + 'snap-to-zone-' + i, + 'KP_' + i, + Lang.bind(this, function() { this._snapToZone(i - 1); }) + ); + } + }, + + _onGrabBegin: function(display, window, op) { + // Check if this is a window move operation + if (op === Meta.GrabOp.MOVING) { + this.dragInProgress = true; + + // Check if Shift is held - show overlay + let mods = global.get_pointer()[2]; + if (mods & Clutter.ModifierType.SHIFT_MASK) { + this._showZonesOverlay(); + } + } + }, + + _onGrabEnd: function(display, window, op) { + if (op === Meta.GrabOp.MOVING && this.dragInProgress) { + this.dragInProgress = false; + + if (this.isShowing) { + // Snap to zone if cursor is over one + this._snapWindowToZone(window); + this._hideZonesOverlay(); + } + } + }, + + _toggleZonesOverlay: function() { + if (this.isShowing) { + this._hideZonesOverlay(); + } else { + this._showZonesOverlay(); + } + }, + + _showZonesOverlay: function() { + if (this.isShowing) return; + + let monitor = Main.layoutManager.primaryMonitor; + + // Create overlay container + this.overlay = new St.Widget({ + style_class: 'gridsnap-overlay', + reactive: false, + x: monitor.x, + y: monitor.y, + width: monitor.width, + height: monitor.height + }); + + // Add semi-transparent background + this.overlay.set_style( + 'background-color: rgba(0, 0, 0, 0.3);' + ); + + // Draw zones + let layout = this.layouts[this.currentLayout]; + layout.zones.forEach((zone, index) => { + let zoneActor = this._createZoneActor(zone, monitor, index); + this.overlay.add_child(zoneActor); + }); + + Main.layoutManager.addChrome(this.overlay); + this.isShowing = true; + }, + + _createZoneActor: function(zone, monitor, index) { + let x = monitor.width * zone.x; + let y = monitor.height * zone.y; + let width = monitor.width * zone.width; + let height = monitor.height * zone.height; + + let actor = new St.Widget({ + style_class: 'gridsnap-zone', + x: x, + y: y, + width: width, + height: height + }); + + actor.set_style( + 'border: 2px solid rgba(100, 149, 237, 0.8);' + + 'background-color: rgba(100, 149, 237, 0.2);' + + 'border-radius: 4px;' + ); + + // Add zone number label + let label = new St.Label({ + text: String(index + 1), + style_class: 'gridsnap-zone-label', + x: 10, + y: 10 + }); + label.set_style( + 'color: white;' + + 'font-size: 24px;' + + 'font-weight: bold;' + + 'text-shadow: 2px 2px 4px rgba(0,0,0,0.8);' + ); + actor.add_child(label); + + return actor; + }, + + _hideZonesOverlay: function() { + if (!this.isShowing) return; + + if (this.overlay) { + Main.layoutManager.removeChrome(this.overlay); + this.overlay.destroy(); + this.overlay = null; + } + + this.isShowing = false; + }, + + _snapWindowToZone: function(window) { + if (!window) return; + + let [x, y] = global.get_pointer(); + let monitor = Main.layoutManager.primaryMonitor; + + // Convert to relative coordinates + let relX = (x - monitor.x) / monitor.width; + let relY = (y - monitor.y) / monitor.height; + + // Find which zone the cursor is in + let layout = this.layouts[this.currentLayout]; + let targetZone = null; + + for (let zone of layout.zones) { + if (relX >= zone.x && relX < zone.x + zone.width && + relY >= zone.y && relY < zone.y + zone.height) { + targetZone = zone; + break; + } + } + + if (targetZone) { + this._moveWindowToZone(window, targetZone, monitor); + } + }, + + _snapToZone: function(zoneIndex) { + // Snap the focused window to a specific zone + let window = global.display.focus_window; + if (!window) return; + + let layout = this.layouts[this.currentLayout]; + if (zoneIndex >= layout.zones.length) return; + + let monitor = Main.layoutManager.primaryMonitor; + this._moveWindowToZone(window, layout.zones[zoneIndex], monitor); + }, + + _moveWindowToZone: function(window, zone, monitor) { + // Unmaximize if maximized + if (window.get_maximized()) { + window.unmaximize(Meta.MaximizeFlags.BOTH); + } + + // Calculate absolute coordinates + let x = monitor.x + Math.round(monitor.width * zone.x); + let y = monitor.y + Math.round(monitor.height * zone.y); + let width = Math.round(monitor.width * zone.width); + let height = Math.round(monitor.height * zone.height); + + // Move and resize window + window.move_resize_frame(true, x, y, width, height); + }, + + cycleLayout: function() { + let layouts = Object.keys(this.layouts); + let currentIndex = layouts.indexOf(this.currentLayout); + let nextIndex = (currentIndex + 1) % layouts.length; + this.currentLayout = layouts[nextIndex]; + + Main.notify('GridSnap', 'Layout: ' + this.layouts[this.currentLayout].name); + + // Refresh overlay if showing + if (this.isShowing) { + this._hideZonesOverlay(); + this._showZonesOverlay(); + } + }, + + destroy: function() { + // Disconnect signals + if (this.grabOpBeginId) { + global.display.disconnect(this.grabOpBeginId); + } + if (this.grabOpEndId) { + global.display.disconnect(this.grabOpEndId); + } + + // Remove keybindings + Main.keybindingManager.removeHotKey('show-zones'); + for (let i = 1; i <= 9; i++) { + Main.keybindingManager.removeHotKey('snap-to-zone-' + i); + } + + // Clean up overlay + this._hideZonesOverlay(); + } +}; + +function init(metadata) { + // Extension initialization +} + +function enable() { + zoneManager = new ZoneManager(); + + // Add keybinding to cycle layouts + Main.keybindingManager.addHotKey( + 'cycle-layout', + 'z', + Lang.bind(zoneManager, zoneManager.cycleLayout) + ); +} + +function disable() { + if (zoneManager) { + Main.keybindingManager.removeHotKey('cycle-layout'); + zoneManager.destroy(); + zoneManager = null; + } +} diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..7d69cd9 --- /dev/null +++ b/icon.svg @@ -0,0 +1,11 @@ + + + + + + + 1 + 2 + 3 + 4 + diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..154c322 --- /dev/null +++ b/install.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# GridSnap for Cinnamon - Installation Script + +EXTENSION_UUID="gridsnap@cinnamon-extension" +INSTALL_DIR="$HOME/.local/share/cinnamon/extensions/$EXTENSION_UUID" + +echo "Installing GridSnap for Cinnamon..." + +# Create extensions directory if it doesn't exist +mkdir -p "$HOME/.local/share/cinnamon/extensions" + +# Copy extension files +echo "Copying extension files..." +cp -r . "$INSTALL_DIR" + +# Remove the install script from the installed version +rm -f "$INSTALL_DIR/install.sh" + +echo "" +echo "Installation complete!" +echo "" +echo "To enable the extension:" +echo "1. Restart Cinnamon (Alt+F2, type 'r', press Enter)" +echo " OR log out and back in" +echo "2. Open System Settings → Extensions" +echo "3. Find 'GridSnap' and toggle it on" +echo "" +echo "Keyboard shortcuts:" +echo " Super+Z : Show/hide zones" +echo " Super+Shift+Z : Cycle layouts" +echo " Super+Numpad1-9 : Snap to zone" +echo "" +echo "Enjoy your new window manager!" diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..975da68 --- /dev/null +++ b/metadata.json @@ -0,0 +1,8 @@ +{ + "uuid": "gridsnap@cinnamon-extension", + "name": "GridSnap", + "description": "Advanced window tiling manager with customizable zone layouts", + "version": "0.1.0", + "cinnamon-version": ["5.0", "5.2", "5.4", "5.6", "6.0"], + "author": "Your Name" +} diff --git a/stylesheet.css b/stylesheet.css new file mode 100644 index 0000000..ebcf63d --- /dev/null +++ b/stylesheet.css @@ -0,0 +1,22 @@ +.gridsnap-overlay { + background-color: rgba(0, 0, 0, 0.3); +} + +.gridsnap-zone { + border: 2px solid rgba(100, 149, 237, 0.8); + background-color: rgba(100, 149, 237, 0.2); + border-radius: 4px; + transition: all 0.2s ease; +} + +.gridsnap-zone:hover { + background-color: rgba(100, 149, 237, 0.4); + border-color: rgba(100, 149, 237, 1.0); +} + +.gridsnap-zone-label { + color: white; + font-size: 24px; + font-weight: bold; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); +}