@@ -6,9 +6,12 @@ import TilerCore
66public enum TilingCommand : Equatable , Sendable {
77 case leftHalf( nextDisplay: Bool )
88 case rightHalf( nextDisplay: Bool )
9+ case leftThird( nextDisplay: Bool )
10+ case rightThird( nextDisplay: Bool )
911 case maximize
1012 case centerThird
1113 case restore
14+ case lockScreen
1215}
1316
1417/// AX window manipulation layer (specs/window-actions/spec.md).
@@ -34,6 +37,10 @@ public final class WindowActions {
3437 /// can feed PermissionMonitor.noteActionFailed().
3538 @discardableResult
3639 public func perform( _ command: TilingCommand ) -> Bool {
40+ if command == . lockScreen {
41+ lockScreen ( )
42+ return true
43+ }
3744 guard let ( app, window) = frontmostFocusedWindow ( ) else {
3845 NSLog ( " Tiler: no focused window for \( command) — ignoring " )
3946 return false
@@ -45,6 +52,10 @@ public final class WindowActions {
4552 /// the window by pid instead of relying on frontmost focus).
4653 @discardableResult
4754 public func perform( _ command: TilingCommand , app: AXUIElement , window: AXUIElement ) -> Bool {
55+ if command == . lockScreen {
56+ lockScreen ( )
57+ return true
58+ }
4859 trimStoresIfNeeded ( )
4960 guard let currentAX = frame ( of: window) else {
5061 NSLog ( " Tiler: cannot read window frame — ignoring \( command) " )
@@ -93,6 +104,12 @@ public final class WindowActions {
93104 width: vf. width / 2 , height: vf. height)
94105 }
95106
107+ func third( of screen: NSScreen , left: Bool ) -> CGRect {
108+ let vf = screen. visibleFrame
109+ return CGRect ( x: left ? vf. minX : vf. minX + vf. width * 2 / 3 , y: vf. minY,
110+ width: vf. width / 3 , height: vf. height)
111+ }
112+
96113 switch command {
97114 case . maximize:
98115 return current. visibleFrame
@@ -103,7 +120,11 @@ public final class WindowActions {
103120 return half ( of: nextDisplay ? next ( after: current, in: screens) : current, left: true )
104121 case . rightHalf( let nextDisplay) :
105122 return half ( of: nextDisplay ? next ( after: current, in: screens) : current, left: false )
106- case . restore:
123+ case . leftThird( let nextDisplay) :
124+ return third ( of: nextDisplay ? next ( after: current, in: screens) : current, left: true )
125+ case . rightThird( let nextDisplay) :
126+ return third ( of: nextDisplay ? next ( after: current, in: screens) : current, left: false )
127+ case . restore, . lockScreen:
107128 return nil
108129 }
109130 }
@@ -234,6 +255,28 @@ public final class WindowActions {
234255 AXUIElementSetAttributeValue ( element, attribute as CFString , value ? kCFBooleanTrue : kCFBooleanFalse)
235256 }
236257
258+ /// System screen lock (⌃A hotkey): private login framework, same effect as
259+ /// the system Lock Screen; CGSession suspend as fallback. No AX involved.
260+ private func lockScreen( ) {
261+ typealias LockFn = @convention ( c) ( ) -> Int32
262+ if let handle = dlopen ( " /System/Library/PrivateFrameworks/login.framework/login " , RTLD_NOW) ,
263+ let sym = dlsym ( handle, " SACLockScreenImmediate " ) {
264+ _ = unsafeBitCast ( sym, to: LockFn . self) ( )
265+ NSLog ( " Tiler: screen locked (SACLockScreenImmediate) " )
266+ return
267+ }
268+ let task = Process ( )
269+ task. executableURL = URL ( fileURLWithPath:
270+ " /System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession " )
271+ task. arguments = [ " -suspend " ]
272+ do {
273+ try task. run ( )
274+ NSLog ( " Tiler: screen locked (CGSession fallback) " )
275+ } catch {
276+ NSLog ( " Tiler: lock screen failed: \( error) " )
277+ }
278+ }
279+
237280 /// Drop entries for windows that no longer answer AX queries (closed windows).
238281 private func trimStoresIfNeeded( ) {
239282 guard originalFrames. count > 64 else { return }
0 commit comments