Как я собрал своё первое macOS-приложение за 2 промпта в Codex
🚀 Скачать приложение: готовый скомпилированный бандл уже доступен в релизах на GitHub.
Я уже давно не использую клавиатуру в 100% случаев. Гораздо чаще я просто надиктовываю текст голосом, ожидая быстрой и точной расшифровки.
В основном это нужно для общения с нейросетями. Моя жена даже шутит, что я теперь больше разговариваю с компьютером, чем с ней. И это, честно говоря, небезосновательные шутки: я искренне считаю, что для качественного выполнения задач — чем больше живого контекста, деталей и нюансов ты скормишь модели, тем лучше будет результат на выходе. Набирать такие объёмы текста руками на клавиатуре просто лень и неэффективно.
Именно поэтому мне понадобилось приложение для диктовки. Не абстрактно «хорошее», а такое, которое работает ровно так, как удобно мне: зажал одну клавишу (push-to-talk), надиктовал поток мыслей, отпустил — и текст сразу появился там, где сейчас мигает курсор.
Что не так с готовыми приложениями
На самом деле на Mac есть прекрасные решения. Я перепробовал разные варианты, и два из них мне действительно понравились.
Wispr Flow работает суперски: приятный интерфейс, мгновенный отклик. Но бесплатный тариф быстро заканчивается жестким лимитом, а дальше сервис настойчиво просит регулярную ежемесячную подписку.
Затем я пошел пробовать MacWhisper:
MacWhisper — отличная штука. Он бесплатно крутит модели прямо на железе мака, что здорово для приватности. Но мне хотелось гонять звук в быструю удалённую облачную модель, к которой у меня уже был готовый доступ по API. А возможность указать свой сервер и API-токен в MacWhisper спрятана за платной Pro-версией.
У всех этих приложений есть свои несомненные плюсы, но есть и один общий минус: они хотят денег.
А в эпоху нейросетей, если приложение за базовую обвязку хочет денег — мы просто садимся и пишем свой vibecoding fork.
Как устроен Simple Flow
Я не написал для этого приложения ни единой строчки кода на Swift. Всё от архитектуры до финальной сборки за меня сделал OpenAI Codex буквально за два промпта.
Верхнеуровнево пайплайн состоит из четырех очевидных шагов:
- Ловим хоткей: приложение висит в Menu Bar и глобально перехватывает нажатие клавиши
Fn(Globe) в режиме push-to-talk. - Пишем аудио: пока клавиша зажата — звук с системного микрофона пишется в буфер.
- Шлём в облако: как только клавишу отпустили — аудио улетает на мой удалённый эндпоинт с авторизацией по токену.
- Вставляем результат: полученный текст через эмуляцию нажатия ⌘V моментально вставляется прямо в активное окно.
Разработка за 2 промпта в Codex
Промпт 1: Логика и архитектура
В первом промпте я просто на словах описал эту цепочку: фоновый демон, клавиша Fn, запись аудио, запрос в сеть и вставка в фокус.
Кодекс сам подтянул нужные фреймворки macOS (AVFoundation, ApplicationServices, CoreGraphics) и собрал под это дело чистую функциональную стейт-машину, слушатель хоткея и инжектор текста:
public enum DictationPhase: Equatable, Sendable {
case idle
case recording
case transcribing
case feedback(FeedbackKind)
}
public struct DictationStateMachine: Sendable {
public private(set) var phase: DictationPhase = .idle
public mutating func handle(_ event: DictationEvent) -> [DictationEffect] {
switch (phase, event) {
case (.idle, .hotkeyPressed):
phase = .recording
return [.captureFocus, .startAudio]
case (.recording, .hotkeyReleased):
phase = .transcribing
return [.stopAndTranscribe]
case (.recording, .escapePressed):
phase = .idle
return [.cancelAudio, .returnToIdle]
case (.transcribing, .transcriptionInserted):
phase = .feedback(.inserted)
return []
case (.transcribing, .failed(let message)):
phase = .feedback(.error(message))
return []
default:
return []
}
}
}import ApplicationServices
import CoreGraphics
import Foundation
public final class HotkeyMonitor: @unchecked Sendable {
private var eventTap: CFMachPort?
private var decoder: HotkeyEventDecoder
public init(hotkey: Hotkey = .fnKey) {
self.decoder = HotkeyEventDecoder(hotkey: hotkey)
}
public func start() {
let mask = (1 << CGEventType.flagsChanged.rawValue)
self.eventTap = CGEvent.tapCreate(
tap: .cghidEventTap,
place: .headInsertEventTap,
options: .listenOnly,
eventsOfInterest: CGEventMask(mask),
callback: { (proxy, type, event, refcon) -> Unmanaged<CGEvent>? in
guard let refcon = refcon else { return Unmanaged.passUnretained(event) }
let monitor = Unmanaged<HotkeyMonitor>.fromOpaque(refcon).takeUnretainedValue()
monitor.handleFlagsChanged(event.flags)
return Unmanaged.passUnretained(event)
},
userInfo: Unmanaged.passUnretained(self).toOpaque()
)
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
CGEvent.tapEnable(tap: eventTap!, enable: true)
}
}import CoreGraphics
import Foundation
public final class SystemEventPoster: EventPosting, @unchecked Sendable {
public func postPasteCommand() -> Bool {
let cmdKeyCode: CGKeyCode = 55 // Command key
let vKeyCode: CGKeyCode = 9 // 'V' key
let source = CGEventSource(stateID: .combinedSessionState)
guard let cmdDown = CGEvent(keyboardEventSource: source, virtualKey: cmdKeyCode, keyDown: true),
let vDown = CGEvent(keyboardEventSource: source, virtualKey: vKeyCode, keyDown: true),
let vUp = CGEvent(keyboardEventSource: source, virtualKey: vKeyCode, keyDown: false),
let cmdUp = CGEvent(keyboardEventSource: source, virtualKey: cmdKeyCode, keyDown: false) else {
return false
}
cmdDown.flags = .maskCommand
vDown.flags = .maskCommand
vUp.flags = .maskCommand
cmdUp.flags = []
// Post Command+V synthetic event tap directly to active focused application
cmdDown.post(tap: .cghidEventTap)
vDown.post(tap: .cghidEventTap)
vUp.post(tap: .cghidEventTap)
cmdUp.post(tap: .cghidEventTap)
return true
}
}Промпт 2: Окно настроек и финальный билд
Во втором промпте я попросил добавить настройки для людей:
- Статус в строке меню (
Ready/Recording). - Нативное окно: выбор микрофона, переназначение хоткея, проверка разрешений macOS (Accessibility, Microphone) и автозапуск при логине.
- Поля для ввода своего URL сервера и API-токена с сохранением в Keychain.
- Скрипт сборки готового
.appфайла.
Кодекс написал весь интерфейс, скомпилировал Swift-код прямо в терминале и отдал мне готовый бандл SimpleFlow.app.
Результат
Я просто перетащил SimpleFlow.app в папку /Applications, вбил в настройках свой токен и сервер — и оно заработало с первого раза именно так, как я хотел.


Я даже снял замеры потребления ресурсов через top прямо во время написания этой статьи:
- CPU:
0.0%в режиме ожидания (процесс находится в состоянииsleepingв RunLoop и просыпается только по прерыванию от глобального хукаCGEventTap). - RAM:
~50 MB(базовый оверхед системных фреймворков AppKit и SwiftUI).
Приложение практически не ест память и процессор, тихо висит в трее и расшифровывает мои голосовые тирады в нейросети без всяких подписок и искусственных ограничений.
Исходный код и релизы на GitHub
Я выложил весь исходный код проекта в открытый доступ:
Готовый архив с приложением (SimpleFlow-v1.0.0.zip) можно скачать напрямую из раздела Releases на GitHub. Для установки достаточно распаковать архив и перетащить SimpleFlow.app в папку /Applications.
Bottom Text
В 2026 году персональный софт — это больше не миф, а реальность для всех, даже для людей без опыта в программировании. Собрать собственную программу под свои задачи сегодня проще, чем искать идеальную альтернативу в интернете.
Не знаю, какой из этого можно сделать вывод — пользуйтесь, чуваки.