У MacBook на процессорах M экран скруглён сверху, а снизу углы прямые. При этом macOS рисует окна, Dock и меню со скруглениями со всех сторон. В итоге в нижних углах экрана всё время видны маленькие треугольники обоев, особенно на тёмной теме и в развёрнутых окнах.

Видимо, я перфекционист, потому что, это бесит каждый раз, когда есть контраст между содержанием открытого окна и обоев. Поэтому я сделал (ну как я? - я и клод) Screen Corners: она рисует в нижних углах встроенного экрана две крошечные чёрные маски, и экран выглядит одинаково скруглённым со всех сторон.

ПОСЛЕ

ПОСЛЕ

Что умеет

  • Углы видны поверх всех окон, Dock и полноэкранных приложений, на всех рабочих столах.

  • Клики проходят сквозь маски.

  • Иконка в строке меню: слайдер радиуса, скругление верхних углов и внешних мониторов, автозапуск.

  • Почти не тратит CPU: четыре статичных окна, которые перерисовываются только при смене конфигурации экранов.

  • Бесплатно, MIT, без телеметрии и зависимостей.

Дальше — несколько деталей реализации, которые могут пригодиться, если будете делать что-то похожее.

Как это сделано

Окно-уголок

Каждый угол — это отдельное маленькое окно без рамки размером r × r. Внутри нарисована фигура «квадрат минус круг». Удобнее всего сделать её через even-odd заливку:

override func draw(_ dirtyRect: NSRect) {    let r = bounds.width    let center = NSPoint(x: r, y: r)          // для левого нижнего угла    let path = NSBezierPath(rect: bounds)    path.appendOval(in: NSRect(x: center.x - r, y: center.y - r,                               width: 2 * r, height: 2 * r))    path.windingRule = .evenOdd               // квадрат минус круг    NSColor.black.setFill()    path.fill()
}

Чтобы окно не мешало и было видно везде, ему нужен такой набор настроек:

w.isOpaque = false
w.backgroundColor = .clear
w.hasShadow = false
w.ignoresMouseEvents = true                    // клики проходят насквозь
w.level = .screenSaver                         // выше Dock и меню
w.collectionBehavior = [.canJoinAllSpaces,     // на всех рабочих столах                        .stationary,           // не уезжает в Mission Control                        .ignoresCycle,         // не попадает в Cmd+`                        .fullScreenAuxiliary]  // видно над полноэкранными приложениями

Встроенный экран определяется через CGDisplayIsBuiltin, а при подключении монитора, смене разрешения и выходе из сна окна пересоздаются по NSApplication.didChangeScreenParametersNotification и NSWorkspace.didWakeNotification.

Подвох со скриншотами

Чтобы чёрные уголки не портили скриншоты, я выставил окнам sharingType = .none. Это сработало даже слишком хорошо: когда я делал картинку «до/после» для README, оба скриншота получились байт в байт одинаковыми. Маски на экране есть, а на снимке их нет.

В итоге в приложении появилась скрытая настройка, которая разрешает углам попадать на скриншоты:

defaults write com.screencorners.app allowCapture -bool true

Swift 6 и MainActor

Первая версия была одним файлом с top-level кодом и упала на новом компиляторе:

error: call to main actor-isolated initializer 'init(cfg:)'
in a synchronous nonisolated context

Top-level код в main.swift не считается изолированным на главном акторе, а AppKit-классы — считаются. Самый чистый выход — явная точка входа:

@main
struct ScreenCornersApp {    @MainActor    static func main() {        let app = NSApplication.shared        let delegate = AppDelegate()        app.delegate = delegate        app.setActivationPolicy(.accessory)   // без иконки в Dock        withExtendedLifetime(delegate) { app.run() }    }
}

Собирать такой файл нужно с флагом -parse-as-library, иначе компилятор снова решит, что это скрипт.

Автозапуск через LaunchAgent

Вместо SMAppService я выбрал обычный пользовательский LaunchAgent в ~/Library/LaunchAgents: его видно, его легко удалить руками, и он не зависит от подписи приложения. Полезный трюк — KeepAlive с условием:

<key>KeepAlive</key>
<dict>    <key>SuccessfulExit</key>    <false/>
</dict>

Если приложение упало, launchd его поднимет. Если пользователь нажал «Выйти» (код возврата 0), перезапуска не будет.

Установка без Gatekeeper

Без платного аккаунта разработчика Apple готовое .app при первом запуске упрётся в предупреждение Gatekeeper. Поэтому установщик скачивает исходники и собирает приложение прямо на вашем Mac через Xcode Command Line Tools. Локально собранное приложение предупреждений не вызывает, а код можно прочитать перед установкой: там несколько сотен строк Swift.

Попробовать

Инструкция по установке, настройки и удаление описаны в README:

https://github.com/PankovDefi/screen-corners

Нужны macOS 13 или новее и Xcode Command Line Tools. Если у вас другая модель MacBook, напишите в комментариях, какой радиус подошёл: по умолчанию на моем macBook pro M1pro стоит 15 pt, но у разных моделей значение может немного отличаться.

Буду рад звёздам, issue и pull request’ам.