Как выбор двух мониторов превратился в собственный RDP/SSH‑клиент для macOS
Когда я перешёл на Mac, мне казалось, что тема удалённого доступа давно закрыта. RDP существует много лет, SSH встроен в систему, SFTP‑клиентов хватает. Оставалось выбрать удобную программу и работать.
Проблема обнаружилась после подключения внешних мониторов.
Мне хотелось не просто включить «использовать все дисплеи», а самому указать, на каких физических экранах Mac должна открываться конкретная Windows‑сессия. Например, отдать Windows два внешних монитора, а экран MacBook оставить под macOS. Или наоборот.
Нормального способа сделать именно это я не нашёл.
Так появился первый прототип Selective Remote.


С этого всё начиналось: профиль подключения и настройки RDP.
Сначала была одна задача: RDP на выбранных мониторах
Первый вариант приложения умел почти ничего: обнаружить дисплеи macOS, выбрать нужные и сформировать топологию для FreeRDP.
На бумаге задача выглядит просто:
получить список экранов;
выбрать нужные;
передать координаты и размеры RDP‑клиенту;
открыть Windows на выбранных дисплеях.
Реальность быстро добавила Retina, разные scale factor, отрицательные координаты, основной монитор, разную высоту экранов и изменение топологии после отключения кабеля.
Особенно хорошо ошибки проявлялись в сочетании обычного внешнего монитора и встроенного Retina‑дисплея. На одном экране всё выглядело идеально, а на втором удалённый рабочий стол внезапно занимал часть пространства или получал чёрные области.

Приложение видит физические дисплеи macOS и строит из выбранных экранов виртуальную схему Windows.
Внутри RDP‑части используется SDL‑FreeRDP. Интерфейс и модель профилей написаны на Swift/SwiftUI, а для некоторых низкоуровневых вещей понадобились нативные компоненты.
Автоматического режима оказалось мало: иногда нужно вручную определить расположение виртуальных экранов — в том числе когда физическая схема на столе не должна один в один повторяться внутри Windows.

В ручном режиме виртуальные дисплеи можно переставлять независимо от физической раскладки macOS.
Именно здесь проект получил свою первую настоящую особенность: приложение строит непрерывную виртуальную раскладку Windows, но сохраняет связь между ней и выбранными физическими мониторами macOS.
Рабочее название тогда было SelectiveRDP.
RDP — это не только картинка на экране
Постепенно рядом с мониторами появились более привычные RDP‑настройки: звук, clipboard, микрофон, принтеры, камера, локальные папки, RD Gateway и правила клавиатуры.

Разрешения macOS проверяются только для реально включённых перенаправлений: отказ в доступе к камере не должен блокировать обычную RDP‑сессию.
Отдельная серия проблем была связана с клавиатурой Mac.
Пользователь ожидает, что Command+C и Command+V будут вести себя привычно, но удалённой Windows одновременно нужна настоящая клавиша Win. Пришлось делать отдельные правила для Command/Option и внимательно следить не только за key‑down, но и за key‑up. Один потерянный modifier‑up — и Windows начинает считать клавишу постоянно зажатой.
Почему RDP‑клиент вдруг начал расти
После первых рабочих сессий стало очевидно: администратор редко живёт только в RDP.
Обычно сценарий выглядит примерно так:
открыть RDP;
проверить Linux‑сервер по SSH;
забрать или положить файл;
поднять туннель;
вернуться в Windows;
снова открыть терминал.
Можно оставить для каждого шага отдельное приложение. Я так и делал. Но постепенно стало раздражать, что одни и те же серверы, адреса и способы аутентификации существуют в нескольких местах.
Поэтому рядом с RDP появился SSH. Потом SFTP. Затем port forwarding, Keychain, диагностика, Jump Host и управление ключами.
В какой‑то момент название SelectiveRDP стало просто неверным. Так проект превратился в Selective Remote.
Терминал: самое простое оказалось не таким простым
Терминал запускает системный /usr/bin/ssh через псевдотерминал. Для отображения используется xterm.js, а между ним и OpenSSH работает небольшой PTY‑мост.
Я специально не хотел писать собственную реализацию SSH‑протокола. Чем больше приложение может оставить штатному OpenSSH, тем меньше магии приходится поддерживать самостоятельно.
Но просто показать чёрное окно оказалось мало.
Со временем появились независимые вкладки, раздельные панели, сетка до четырёх терминалов, отдельное подключение на каждую вкладку, восстановление workspace, локальная история и подсказки.

Каждая панель может использовать своё SSH‑подключение, а workspace восстанавливается между открытиями.
Отдельной головной болью оказалось состояние UI.
Если переключиться на другой раздел и вернуться, пользователь ожидает увидеть тот же терминал и тот же экран, а не заново созданное SwiftUI‑представление. Если терминал отключён — пустая чёрная панель выглядит как баг, поэтому появились явные состояния «подключён / не подключён / ошибка», кнопка повторного подключения и контекстные действия.
История команд хранится локально на Mac. Строки с распространёнными признаками токенов, паролей и других секретов в неё не записываются.
Позже к истории добавились подсказки по типовым командам и уже использованным строкам.

Подсказки объединяют локальную историю и встроенный набор типовых команд.
Из таких мелочей постепенно и складывается ощущение нормального рабочего инструмента.
SFTP: файловый менеджер, а не кнопка «Upload»
SFTP сначала был вкладкой внутри SSH‑профиля. Но вскоре оказалось, что файловый менеджер удобнее воспринимать как самостоятельное рабочее пространство.
Левая сторона всегда может показывать Mac, а правую можно подключить к сохранённому профилю или временному серверу.

Локальная и удалённая панели работают независимо от того, какой профиль открыт в основном разделе.
Сейчас там есть фильтрация и сортировка, дата изменения, владелец, права, размер, создание и переименование файлов, удаление, редактирование небольших UTF-8-файлов, очередь передач, рекурсивное копирование и drag‑and‑drop.
Причём большая часть багов оказалась не в SFTP как протоколе.
Проблемы были гораздо прозаичнее:
выделение срабатывало через раз;
двойной клик по каталогу конфликтовал с selection;
большой файл показывал неверный progress;
удалённая папка не обновлялась вовремя;
переключение между разделами рвало соединение;
drag‑and‑drop в Finder на macOS оказался отдельным приключением.
То есть именно те вещи, которые редко выглядят интересными в списке фич, оказываются самыми заметными при ежедневной работе.
Keychain: когда «просто сохранить пароль» перестаёт быть просто
Когда Terminal, SFTP и Forwarding начали использовать одни SSH‑профили, стало понятно, что реквизиты тоже должны быть едиными.
Пароли и passphrase сохраняются в macOS Keychain. Приватные SSH‑ключи при этом остаются обычными файлами в ~/.ssh или другом выбранном пользователем месте — приложение не копирует private key в своё хранилище.
Со временем управление реквизитами выросло в отдельный раздел.

SSH ID, Touch ID, сертификаты, сохранённые реквизиты, SSH CA и Known Hosts собраны в одном месте.
В Keychain сейчас можно работать с:
обычными SSH ID;
отдельными Touch ID Key;
сохранёнными SSH password entries;
ssh-agent;OpenSSH certificates;
Known Hosts;
SSH Certificate Authorities.
Для ключа можно посмотреть fingerprint и public key, загрузить его в ssh-agent, установить публичную часть на сервер и увидеть, какие профили его используют.
Отдельно появился Touch ID Key. Здесь я сознательно разделил обычный SSH ID и «биометрический» ключ: если в профиле выбран режим Touch ID Key, используются только соответствующие ECDSA‑ключи, а не любой произвольный Ed25519/RSA identity.
Идея простая: Touch ID — не новый сетевой протокол. Это локальный контроль доступа перед использованием защищённого секрета. Само SSH‑соединение по‑прежнему строится штатным OpenSSH.
Known Hosts, сертификаты и Jump Host
Одна из вещей, которую легко испортить красивым интерфейсом, — проверка host key.
Я не хотел делать кнопку в стиле «ключ сервера изменился — принять новый», потому что именно в этот момент пользователь должен остановиться и понять, почему он изменился.
Поэтому Known Hosts вынесены в интерфейс отдельно: можно посмотреть fingerprint, проверить текущий ключ сервера и удалить старую запись. При удалении known_hosts сначала создаётся резервная копия.
Появилась и работа с OpenSSH certificates: приложение умеет находить *-cert.pub, показывать срок действия и использовать certificate вместе с соответствующим SSH ID. Следующим шагом стало управление SSH CA.
Для корпоративных сетей пригодился и ProxyJump: конечный SSH‑сервер может быть доступен только через bastion.

Способ входа, Touch ID, Jump Host и OpenSSH‑параметры находятся в одном SSH‑профиле.
Интересный баг появился именно здесь: первый вариант Jump Host просил пароль bastion‑сервера даже тогда, когда он уже лежал в Keychain.
Причина оказалась архитектурной. AskPass умел получать секрет только для конечного сервера. Для двух SSH‑hop'ов пришлось разделить target credential и jump credential, чтобы приложение не перепутало два разных пароля.
Это хороший пример того, почему «добавить -J» и «добавить полноценный Jump Host в приложение» — немного разные задачи.
Туннели: процесс должен жить независимо от экрана
Port forwarding тоже начинался как несколько полей внутри SSH‑профиля.
Но туннель по смыслу живёт своей жизнью: ему нужны режим, локальный порт, SSH‑сервер, destination и состояние процесса.
Поэтому появился отдельный глобальный Forwarding с Local, Remote и Dynamic/SOCKS.

Активный туннель подсвечивается, а схема показывает направление трафика.
Мне хотелось, чтобы Local/Remote/Dynamic различались не только названием вкладки. Поэтому интерфейс показывает схему соединения: Mac → SSH Server → Destination или SOCKS‑маршрут.
Работающий туннель подсвечивается зелёным и слегка анимируется. Это декоративная деталь, но здесь она полезна: сразу видно, что перед вами не просто сохранённая конфигурация, а живой процесс.
Позже пришлось разделить два понятия:
профильный туннель — относится к конкретному SSH‑хосту;
независимый туннель — живёт в глобальном Forwarding.
Профильные туннели получили тот же визуальный язык, но остаются частью конкретного хоста.

Профильный туннель виден внутри карточки хоста и не считается независимым туннелем глобального Forwarding.
До этого запуск туннеля внутри карточки хоста неожиданно увеличивал счётчик глобального Forwarding. Технически процесс действительно работал, но с точки зрения интерфейса это были разные сущности.
Диагностика появилась потому, что «Connection failed» бесполезно
Ошибка SSH может находиться где угодно:
DNS;
TCP;
proxy;
Jump Host;
host key;
SSH key;
пароль;
ssh-agent;сертификат.
Поэтому в приложении постепенно появился отдельный SSH Diagnostics. Он показывает не бесконечную командную строку аргументов, а сначала человекочитаемую конфигурацию, а полные параметры можно раскрыть отдельно.
Это кажется мелочью, пока однажды не приходится разбираться, почему один профиль подключается через Terminal, а SFTP — нет.
Самая полезная часть разработки — не фичи, а реальные поломки
Большинство изменений в Selective Remote появилось не из roadmap.
Они появлялись примерно так:
Подключил второй монитор — нашёл ошибку Retina.
Перешёл из SFTP в Terminal — соединение пропало.
Перетащил файл в Finder — обнаружился отдельный macOS drag‑and‑drop сценарий.
Запустил tunnel — процесс сразу завершился.
Выбрал Touch ID Key — приложение предложило вообще другой тип SSH‑ключа.
Добавил Jump Host — OpenSSH снова попросил пароль, который уже лежит в Keychain.
Мне нравится превращать такие случаи в воспроизводимые сценарии и, где возможно, в тесты.
Сейчас CI гоняет Swift‑тесты, дополнительные проверки нативных helper'ов, production build, а release workflow по тегу собирает DMG. Версия приложения, build number, update feed и release notes проверяются отдельно — именно потому, что однажды всё это уже умело рассинхронизироваться.
Что получилось к версии 0.20.15
На момент публикации в Selective Remote есть:
RDP через SDL‑FreeRDP;
выбор физических мониторов и собственная виртуальная схема Windows;
RD Gateway;
перенаправление устройств и папок;
многовкладочный и многопанельный SSH Terminal Workspace;
локальная история и контекстные подсказки;
двухпанельный SFTP;
drag‑and‑drop и очередь передач;
глобальный и профильный SSH Forwarding;
Local, Remote и Dynamic/SOCKS tunnels;
Password, SSH ID,
ssh-agentи Touch ID Key;macOS Keychain;
Known Hosts;
OpenSSH certificates и SSH CA;
HTTP CONNECT и SOCKS5 proxy;
ProxyJump / Jump Host;
SSH Diagnostics;
импорт хостов из
~/.ssh/config;Quick Connect;
русский и английский интерфейс;
обновления через GitHub Releases.
Проект распространяется по MIT и собирается для Apple Silicon. Пользовательский вариант — обычный DMG: открыть и перенести приложение в Applications.
Исходный код и релизы:
https://github.com/PastFly/Selective‑Remote
Зачем вообще писать ещё один remote‑клиент
Selective Remote не начинался как попытка сделать «конкурента всем».
Он начался с очень узкого вопроса:
Почему я не могу сам выбрать, на каких мониторах Mac должна находиться моя Windows‑сессия?
А дальше оказалось, что вокруг этой задачи есть десятки маленьких неудобств, которые лично мне хотелось решить иначе.
Наверное, поэтому проект мне до сих пор интересен. Я не пытаюсь заранее угадать идеальный набор функций. Я использую приложение, нахожу вещь, которая раздражает, и пытаюсь сделать её немного лучше.
Иногда из одного такого раздражения получается ещё один пункт настроек.
А иногда — целый SSH‑клиент.

