Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
90 changes: 90 additions & 0 deletions .github/workflows/check-ios.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: iOS tests and device build

on:
pull_request:
paths:
- 'openless-all/ios/**'
- 'openless-all/app/crates/openless-core/**'
- 'openless-all/app/crates/micyou-*/**'
- 'openless-all/app/contract/**'
- 'openless-all/app/assets/**'
- '.github/workflows/check-ios.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ios-checks-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_PROFILE_DEV_DEBUG: 0
CARGO_PROFILE_TEST_DEBUG: 0

jobs:
test:
runs-on: xcode-27
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.99.0
with:
components: clippy
- name: Record the unchanged shared source baseline
run: |
git diff --exit-code HEAD -- openless-all/app
git rev-parse HEAD:openless-all/app
- name: Prepare the isolated mobile Core
run: python3 openless-all/ios/scripts/prepare-core.py
- name: Test shared keyboard contracts
run: swift test --package-path openless-all/ios/Packages/OpenLessShared
- name: Test mobile Core and iOS Host
run: cargo test --manifest-path openless-all/ios/CoreHost/Cargo.toml --workspace --lib --locked
- name: Check mobile Core
run: cargo clippy --manifest-path openless-all/ios/CoreHost/Cargo.toml -p openless-core --all-targets --locked -- -D warnings
- name: Test interface resources, predictions and native audio
working-directory: openless-all/ios
run: |
python3 scripts/test-interface-resources.py
python3 scripts/build-predictions.py
bash scripts/test-predictions.sh
bash scripts/test-audio.sh
- name: Verify shared sources were not modified
run: git diff --exit-code -- openless-all/app

device-build:
needs: test
runs-on: xcode-27
timeout-minutes: 60
defaults:
run:
working-directory: openless-all/ios
steps:
- uses: actions/checkout@v6
- name: Prepare native build tools and dependencies
run: |
xcodebuild -version
command -v cmake >/dev/null || brew install cmake
bash scripts/bootstrap.sh
bash scripts/bootstrap-local-speech.sh
- name: Build Rime and test composition
run: |
bash scripts/build-rime.sh host
bash scripts/test-rime.sh
bash scripts/build-rime.sh device
- name: Build the mobile Core and refresh attribution
run: |
bash scripts/build-core.sh device
python3 scripts/collect-rust-licenses.py
- name: Build and validate the unsigned iPhone app
env:
OPENLESS_DERIVED_DATA: ${{ runner.temp }}/ios-device
run: |
python3 scripts/validate-project.py
bash scripts/build.sh device Release unsigned
python3 scripts/package-ipa.py \
"$OPENLESS_DERIVED_DATA/Build/Products/Release-iphoneos/OpenLess.app" \
"$RUNNER_TEMP/OpenLess-ci-unsigned.ipa"
- name: Verify shared sources were not modified
run: git diff --exit-code -- ../app
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@

OpenLess is a cross-platform (macOS and Windows) voice-input application — a fully open-source alternative to commercial tools such as [Typeless](https://www.typeless.com/), [Wispr Flow](https://wisprflow.ai), [Lazy](https://heylazy.com), and Superwhisper. Official site: [openless.top](https://openless.top).

The native [iOS app and keyboard](openless-all/ios/README.md) are maintained in `openless-all/ios/`, with Chinese/English typing, voice-focused and hold-space input modes, and speech translation. See the iOS guide for source builds and personal signing.

Place your cursor in any text field — ChatGPT, Claude, Cursor, Notion, an email draft, a chat box — press a single global hotkey, and speak. OpenLess records the audio, transcribes it, polishes the text in the mode you selected, and inserts the result at the cursor. If insertion is blocked, the text is copied to the clipboard instead, so nothing you said is lost.

Unlike dictation tools that simply emit a word-for-word transcript, OpenLess's headline capability is its **AI-prompt mode**: you speak freely, and it adds structure, captures your constraints, and produces a context-rich prompt that you can paste directly into ChatGPT, Claude, or Cursor.
Expand Down
2 changes: 2 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@

OpenLess 是一款跨平台(macOS 与 Windows)语音输入应用,是 [Typeless](https://www.typeless.com/)、[Wispr Flow](https://wisprflow.ai)、[Lazy](https://heylazy.com)、Superwhisper 等商业工具的完全开源替代品。官网:[openless.top](https://openless.top)。

原生 [iOS 应用与输入法](openless-all/ios/README.md) 位于 `openless-all/ios/`,支持中英文打字、语音优先与长按空格输入两种模式,以及语音翻译。源码构建和个人签名方式见 iOS 工程说明。

把光标放在任意文本框中——ChatGPT、Claude、Cursor、Notion、邮件草稿、聊天框——按下一个全局快捷键,然后开口说话。OpenLess 会录音、转写,按你选定的模式润色文本,并将结果插入到光标处。如果插入被阻止,文本会改为复制到剪贴板,你说过的话不会丢失。

与只产出逐字转写稿的听写工具不同,OpenLess 的核心能力是 **AI Prompt 模式**:你自由地说,它来补充结构、整理你的约束条件,生成一段包含上下文的提示词,可直接粘贴进 ChatGPT、Claude 或 Cursor。
Expand Down
2 changes: 2 additions & 0 deletions docs/structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ openless-src/ Git 仓库、分支与发布边界
├── scripts/ 仓库级辅助脚本(版本号同步等)
└── openless-all/
├── README.md 全平台总览
├── ios/ 原生 iPhone App、键盘扩展与录音组件
└── app/ npm 与 Core/Linux Cargo 工作目录
├── src/ React / TypeScript 界面
├── crates/openless-core/ 共享业务 Rust crate
Expand Down Expand Up @@ -48,6 +49,7 @@ openless-src/ Git 仓库、分支与发布边界
| Tauri 组装与系统能力 | `src-tauri/src/coordinator.rs`、`core_adapters.rs`、`tauri_coordinator_host.rs` | 窗口、热键、权限、平台输入与生命周期 |
| Linux 原生接入 | `linux-egui/src/main.rs`、`lib.rs`、`backend.rs` | `audio/credentials/fcitx5/hotkeys/settings` 等 Host 模块;见 [交接](linux-egui-handoff/README.md) |
| Android 集成 | `android/`、`src-tauri/src/android/` | `@android` 别名与 `merge-android-*.mjs` 生成链 |
| iOS 应用与输入法 | `../ios/App/`、`../ios/Keyboard/`、`../ios/RecordingWidget/` | SwiftUI/UIKit 界面、独立 Rust Host、共享版本配置;见 [iOS 工程](../openless-all/ios/README.md) |
| Windows 输入法 | `windows-ime/`、`src-tauri/src/windows_ime_*.rs` | 原生工程、IPC 协议、目标应用和安装检查 |

Core 其余模块按领域列于 [架构模块地图](architecture.md)。平台缺口、事件签名与验收项由专项文档维护,本文件只提供定位。
Expand Down
4 changes: 4 additions & 0 deletions openless-all/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

This directory contains the cross-platform OpenLess application. Start with the repository [documentation index](../docs/index.md), [architecture](../docs/architecture.md), and [source structure](../docs/structure.md).

## Native iOS app and keyboard

[`ios/`](ios/README.md) contains the native iPhone app, Chinese/English keyboard extension, and recording widget. Settings offer a voice-focused layout or a single keyboard with hold-space dictation, translation, and send/cancel gestures. The app and keyboard target iOS 16; the recording widget requires iOS 18. The iOS guide covers the shared version configuration, input engines, source build, signing, and unsigned IPA packaging.

## App Directory

The runnable sources live in `app/`:
Expand Down
4 changes: 4 additions & 0 deletions openless-all/ios/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Preserve upstream license text, including its original whitespace.
Resources/Licenses/**/*.txt -whitespace
# Unified patch context includes significant whitespace prefixes.
CoreHost/Patches/*.patch -whitespace
5 changes: 5 additions & 0 deletions openless-all/ios/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
.vendor/
DerivedData*/
Artifacts/
*.ipa
*.xcarchive/
*.xcresult/
*.log
__pycache__/
Config/Local.xcconfig
*.xcuserstate
Expand All @@ -17,6 +21,7 @@ Resources/SenseVoice/*.download
Resources/Rime/build/
Resources/Rime/user.yaml
Resources/Prediction/*.sqlite3
Resources/Prediction/*.building.sqlite3
!OpenLess.xcodeproj/
!OpenLess.xcodeproj/project.pbxproj
!OpenLess.xcodeproj/xcshareddata/
Expand Down
2 changes: 1 addition & 1 deletion openless-all/ios/App/AppDesign.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ enum AppTheme: String, CaseIterable {
var scheme: ColorScheme? { switch self { case .system: nil; case .light: .light; case .dark: .dark } }
}

/// One palette for the product screens; no gray-on-gray diagnostic panels.
/// Shared colors for product screens and controls.
enum AppDesign {
static let canvas = Color(uiColor: UIColor { traits in
traits.userInterfaceStyle == .dark
Expand Down
6 changes: 2 additions & 4 deletions openless-all/ios/App/ExperimentalPiPKeepAlive.swift
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,7 @@ final class ExperimentalPiPKeepAlive: NSObject {
onChange?()
}

/// A low-frequency content update retains the reference's content-only path
/// without its 60 Hz animation. Device testing must establish long-run retention.
/// Refresh content once per second while PiP is active and unsuspended.
private func updateContentRefresh() {
let eligible = enabled && isActive && controller?.isPictureInPictureSuspended == false
&& !protectedDataUnavailable
Expand All @@ -283,8 +282,7 @@ final class ExperimentalPiPKeepAlive: NSObject {
Task { @MainActor [weak self] in
guard let self, self.isReady else { return }
self.contentPhase.toggle()
// A nearly transparent layer mutation generates content work, without
// a visible video surface or an audio session. Never request 120 Hz.
// A transparent layer update avoids starting an audio or video engine.
self.contentController?.view.layer.backgroundColor = UIColor(white: 0,
alpha: self.contentPhase ? 0.001 : 0).cgColor
self.contentController?.view.setNeedsDisplay()
Expand Down
10 changes: 2 additions & 8 deletions openless-all/ios/App/HostApplicationReturn.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,8 @@
#import <objc/message.h>
#import <objc/runtime.h>

// iOS 27.0 (24A434), arm64 Simulator runtime probe, 2026-10-18:
// UIApplication._systemNavigationAction: @16@0:8
// UISystemNavigationAction.destinations: @16@0:8
// bundleIdForDestination: @24@0:8Q16; sendResponseForDestination: B24@0:8Q16
// LSApplicationWorkspace.defaultWorkspace: @16@0:8
// openApplicationWithBundleID: B24@0:8@16
// Check the complete ABI again before every call. No installation inventory,
// URL-scheme guessing, process suspension, or private ivar offsets are used.
// Validate each runtime method's complete ABI before invoking it. A return
// destination belongs only to the current activation; never enumerate apps.
static BOOL OLMethodMatches(id object, SEL selector, const char *result,
const char *argument) {
if (!object) return NO;
Expand Down
3 changes: 1 addition & 2 deletions openless-all/ios/App/PiPCoexistenceAdapter.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// Adapted from Yoroin/GlobalRefresh-PiP, commit 1b62341c1d62308962fb3656192d6783631aa00c.
// Original PiP demo: CaiWanFeng/PiP. See Resources/Licenses/GlobalRefresh-PiP-NOTICE.txt.
// Third-party source attribution is bundled in Resources/Licenses.
import Foundation
import ObjectiveC
#if os(iOS)
Expand Down
9 changes: 4 additions & 5 deletions openless-all/ios/AppTests/SpeakButtonVisualTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,20 @@ nonisolated final class SpeakButtonVisualTests: XCTestCase {
controller.view.layoutIfNeeded()
button.configure(phase: .recording, level: 0, available: true)
try await Task.sleep(for: .milliseconds(300))
capture(controller.view, name: "build11-recording-silence")
// Synthetic RMS inputs exercise only the presentation. No microphone is
// activated; screenshots/video must not be described as capture evidence.
capture(controller.view, name: "recording-silence")
// Synthetic RMS values exercise presentation without opening the microphone.
for _ in 0..<3 {
for level: Float in [0, 0.004, 0.01, 0.025, 0.06, 0.1, 0.025, 0.01] {
button.configure(phase: .recording, level: level, available: true)
try await Task.sleep(for: .milliseconds(160))
}
}
capture(controller.view, name: "build11-recording-speech")
capture(controller.view, name: "recording-speech")
XCTAssertEqual(button.accessibilityLabel, "结束录音")
button.configure(phase: .processing, level: 0, available: false)
try await Task.sleep(for: .milliseconds(350))
XCTAssertEqual(button.accessibilityLabel, "thinking")
capture(controller.view, name: "build11-thinking")
capture(controller.view, name: "thinking")
}

@MainActor private func capture(_ view: UIView, name: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import XCTest
import UIKit
import OpenLessShared

/// Call in an iOS test host on MainActor. Uses the actual patched UIView and UILabel sizes.
/// Measures the rendered UIView and UILabel geometry on the main actor.
/// Geometry uses the production view; only the irrelevant canvas color is supplied here.
@MainActor
enum TranslationArcGeometryChecks {
Expand Down
4 changes: 2 additions & 2 deletions openless-all/ios/Config/App-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
Expand All @@ -30,7 +30,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>15</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSMicrophoneUsageDescription</key>
<string>仅在你点按或按住录音时采集声音,结束后立即释放麦克风。</string>
<key>NSSpeechRecognitionUsageDescription</key>
Expand Down
3 changes: 2 additions & 1 deletion openless-all/ios/Config/Base.xcconfig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Local signing is deliberately not committed.
#include "Version.xcconfig"
// Signing overrides stay local to each developer.
OL_BUNDLE_ID = app.openless.ios
OL_APP_GROUP = group.app.openless.ios
#include? "Local.xcconfig"
4 changes: 2 additions & 2 deletions openless-all/ios/Config/Keyboard-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>15</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
Expand Down
4 changes: 2 additions & 2 deletions openless-all/ios/Config/RecordingWidget-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>15</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
Expand Down
3 changes: 3 additions & 0 deletions openless-all/ios/Config/Version.xcconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Shared by the app, keyboard and recording widget.
MARKETING_VERSION = 0.1.0
CURRENT_PROJECT_VERSION = 16
Loading
Loading