Скругляем нижние углы экрана MacBook: маленькая утилита на Swift

—

от автора

У 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 = falsew.backgroundColor = .clearw.hasShadow = falsew.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-классы — считаются. Самый чистый выход — явная точка входа:

@mainstruct 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’ам.

ссылка на оригинал статьи https://habr.com/ru/articles/1106452/