Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
df10a1b
chore: configure fork signing, bundle ID and update feed
barto95100 Oct 25, 2026
97f870f
ci: add daily upstream sync workflow
barto95100 Oct 25, 2026
f660a73
chore: pin SwiftFormat rules to upstream style and apply update feed …
barto95100 Oct 25, 2026
ee0816e
ci: pin SwiftFormat version to 0.64.1
barto95100 Oct 25, 2026
0ec8663
fix: stop link preview flicker on hover within nested anchors
Jul 17, 2026
0a6b7c2
fix: only publish link hover updates when the hovered link changes
barto95100 Oct 25, 2026
532c05e
Merge the-ora/browser#246: stop link preview flicker on hover
barto95100 Oct 25, 2026
41b81b6
fix(tabs): keep urlString in sync with the tab URL on navigation
barto95100 Oct 25, 2026
f30a8a4
fix(launcher): make Enter open a typed or pasted URL
barto95100 Oct 25, 2026
1acba7d
Merge fix/urlbar-enter-submit: Enter in the URL bar opens the typed URL
barto95100 Oct 25, 2026
8f031e0
fix(urlbar): end editing on any click outside the field and let click…
barto95100 Oct 25, 2026
41e0443
Merge fix/urlbar-dismiss-on-click-outside: dismiss URL bar editing on…
barto95100 Oct 25, 2026
ff4c235
fix(permissions): wire up the media capture delegate and stop grantin…
barto95100 Oct 25, 2026
76ca4d7
build: make the unit test target signable
barto95100 Oct 25, 2026
4d7607c
feat(permissions): add per-space site permission model and store
barto95100 Oct 25, 2026
e983e0d
feat(permissions): ask per site before granting camera and microphone
barto95100 Oct 25, 2026
2a2f4b7
test: avoid force unwraps in site permission tests
barto95100 Oct 25, 2026
254b69d
feat(permissions): review and revoke site permissions per space in se…
barto95100 Oct 25, 2026
9676342
Merge feat/site-permissions: ask per site before granting camera and …
barto95100 Oct 25, 2026
4da3f77
ci: build the app and run unit tests on every push and pull request
barto95100 Oct 25, 2026
810e0c7
Merge pull request #1 from barto95100/ci/build-and-test
barto95100 Oct 25, 2026
6618f35
fix: keep full screen when Escape is pressed in a web page
barto95100 Oct 25, 2026
8f2e5ea
Merge pull request #2 from barto95100/fix/179-esc-exits-fullscreen
barto95100 Oct 25, 2026
a9cd8e9
fix: make the Fullscreen keyboard shortcut work
barto95100 Oct 25, 2026
0f4cb87
Merge pull request #3 from barto95100/fix/fullscreen-shortcut
barto95100 Oct 25, 2026
619cbf6
fix: open links from other apps reliably, including on a cold launch
barto95100 Oct 25, 2026
06047ad
fix: open local HTML files
barto95100 Oct 25, 2026
3b63de4
fix: show the host or file name for pages without a title
barto95100 Oct 25, 2026
0a2196e
test: run tab title tests on the main actor
barto95100 Oct 25, 2026
b5e5dca
Merge pull request #4 from barto95100/fix/178-open-links-cold-launch
barto95100 Oct 25, 2026
54ae5b7
fix(media): don't show the mini player for silent or muted media
barto95100 Oct 25, 2026
95e115b
Merge pull request #5 from barto95100/fix/187-mini-player-silent-media
barto95100 Oct 25, 2026
64c040f
fix(media): make Picture in Picture reliable and stop flooding the ap…
barto95100 Oct 25, 2026
652cd1d
Merge pull request #6 from barto95100/fix/media-player-pip
barto95100 Oct 25, 2026
6b9e0a4
fix(privacy): stop overriding navigator and screen properties
barto95100 Oct 25, 2026
bd8836a
fix: send Safari's exact user agent instead of a duplicated one
barto95100 Oct 25, 2026
5faa6c6
fix(privacy): vary fingerprinting noise per site and per session
barto95100 Oct 25, 2026
781c343
Merge pull request #7 from barto95100/fix/fingerprinting-youtube
barto95100 Oct 25, 2026
a8c106f
feat(tabs): recover from web content process crashes like Safari
barto95100 Oct 25, 2026
ccc42f3
chore(debug): add a command to simulate a page crash
barto95100 Oct 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 99 additions & 55 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
@@ -1,66 +1,110 @@
name: Lint and Formatting
# Lint, format check, build and unit tests on every push and pull request.
# Also runnable on demand (used by Upstream Sync, whose PRs cannot trigger
# workflows by themselves).
name: Build and Test

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch:

concurrency:
group: build-and-test-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
# Same versions as the ones used locally, so lint results match.
SWIFTLINT_VERSION: "0.65.1"
SWIFTFORMAT_VERSION: "0.64.1"

jobs:
build-and-test:
runs-on: macos-latest

timeout-minutes: 45

steps:
- name: Check architecture
run: uname -m

- name: Checkout code
uses: actions/checkout@v4

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Show Xcode version
run: xcodebuild -version

- name: Show Swift version
run: swift --version

- name: Install Xcodegen
run: brew install xcodegen

- name: Generate Xcode project
run: xcodegen

- name: Install SwiftLint
run: brew install swiftlint

- name: Run SwiftLint
run: swiftlint lint

- name: Install SwiftFormat
run: brew install swiftformat

- name: Check code formatting
run: swiftformat --lint .

- name: Install Xcbeautify
run: brew install xcbeautify

- name: Clean build folder
run: xcodebuild clean -scheme ora -project Ora.xcodeproj

- name: Cache SPM dependencies
uses: actions/cache@v3
with:
path: |
~/Library/Caches/org.swift.swiftpm/
~/.swiftpm/
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.swift') }}
restore-keys: |
${{ runner.os }}-spm-

- name: Resolve dependencies
run: xcodebuild -resolvePackageDependencies
- name: Checkout
uses: actions/checkout@v5

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Show toolchain
run: |
uname -m
xcodebuild -version
swift --version

- name: Install pinned SwiftLint and SwiftFormat
run: |
bin="$RUNNER_TEMP/tools-bin"
mkdir -p "$bin"
# Download a release archive and install the named binary from it,
# wherever it sits inside the archive.
install_tool() {
local name="$1" url="$2" dir="$RUNNER_TEMP/$1"
curl -fsSL -o "$dir.zip" "$url"
unzip -o -q "$dir.zip" -d "$dir"
install -m 755 "$(find "$dir" -type f -name "$name" | head -n 1)" "$bin/$name"
}
install_tool swiftlint \
"https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/portable_swiftlint.zip"
install_tool swiftformat \
"https://github.com/nicklockwood/SwiftFormat/releases/download/${SWIFTFORMAT_VERSION}/swiftformat.zip"
echo "$bin" >> "$GITHUB_PATH"

- name: Install XcodeGen and xcbeautify
run: brew install xcodegen xcbeautify

- name: Lint
run: |
swiftlint version
swiftlint lint --quiet

- name: Check formatting
run: |
swiftformat --version
swiftformat --lint .

- name: Generate Xcode project
run: xcodegen

- name: Cache Swift packages
uses: actions/cache@v4
with:
path: .spm
key: ${{ runner.os }}-spm-${{ hashFiles('project.yml') }}
restore-keys: |
${{ runner.os }}-spm-

# The CI has no signing identity: sign ad hoc ("-") instead, which is
# enough to build, run the host app and load the test bundle.
- name: Build and run unit tests
run: |
set -o pipefail
xcodebuild test \
-project Ora.xcodeproj \
-scheme ora \
-destination "platform=macOS,arch=arm64" \
-clonedSourcePackagesDirPath .spm \
-resultBundlePath "$RUNNER_TEMP/TestResults.xcresult" \
CODE_SIGN_IDENTITY="-" \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM="" \
PROVISIONING_PROFILE_SPECIFIER="" \
| xcbeautify --renderer github-actions

- name: Upload test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: TestResults
path: ${{ runner.temp }}/TestResults.xcresult
retention-days: 7
74 changes: 74 additions & 0 deletions .github/workflows/upstream-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Vérifie chaque jour s'il y a du nouveau sur the-ora/browser et ouvre une PR
# de synchronisation dans ce fork (branche `upstream-sync` -> `main`).
name: Upstream Sync

on:
schedule:
- cron: "17 5 * * *" # tous les jours vers 7h (heure de Paris)
workflow_dispatch:

permissions:
contents: write
pull-requests: write
actions: write

concurrency:
group: upstream-sync
cancel-in-progress: false

jobs:
sync:
runs-on: ubuntu-latest
env:
UPSTREAM_REPO: the-ora/browser
UPSTREAM_BRANCH: main
SYNC_BRANCH: upstream-sync
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Fetch upstream
run: |
git remote add upstream "https://github.com/${UPSTREAM_REPO}.git"
git fetch upstream "${UPSTREAM_BRANCH}" --tags

- name: Check for new upstream commits
id: check
run: |
count=$(git rev-list --count "HEAD..upstream/${UPSTREAM_BRANCH}")
echo "count=${count}" >> "$GITHUB_OUTPUT"
echo "Nouveaux commits upstream : ${count}"

- name: Push sync branch
if: steps.check.outputs.count != '0'
run: |
git push --force origin "upstream/${UPSTREAM_BRANCH}:refs/heads/${SYNC_BRANCH}"

- name: Open or update pull request
if: steps.check.outputs.count != '0'
run: |
log=$(git log --oneline --no-merges "HEAD..upstream/${UPSTREAM_BRANCH}" | head -50)
body=$(printf '%s\n\n%s\n\n```\n%s\n```\n' \
"Synchronisation automatique depuis [${UPSTREAM_REPO}](https://github.com/${UPSTREAM_REPO})." \
"${{ steps.check.outputs.count }} nouveau(x) commit(s) upstream :" \
"$log")
existing=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$SYNC_BRANCH" --state open --json number --jq '.[0].number')
if [ -n "$existing" ]; then
gh pr edit "$existing" --repo "$GITHUB_REPOSITORY" --body "$body"
echo "PR #$existing mise à jour"
else
gh pr create --repo "$GITHUB_REPOSITORY" \
--base main --head "$SYNC_BRANCH" \
--title "Sync upstream (${UPSTREAM_REPO})" \
--body "$body"
fi

# Pull requests opened with GITHUB_TOKEN do not trigger other workflows,
# but workflow_dispatch does: run the CI on the sync branch explicitly so
# the pull request gets its checks.
- name: Run CI on the sync branch
if: steps.check.outputs.count != '0'
run: gh workflow run build-and-test.yml --repo "$GITHUB_REPOSITORY" --ref "$SYNC_BRANCH"
8 changes: 8 additions & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@
--disable sortedImports
--disable consecutiveSpaces
--disable redundantReturn
# Règles apparues dans les versions récentes de SwiftFormat : désactivées pour
# garder le formatage d'origine (et éviter les conflits avec the-ora/browser)
--disable wrapIfStatementBodies
--disable nestedIf
--disable repeatedGuard
--disable redundantSwiftUIGroup
--disable wrapIfExpressionBodies
--disable redundantEmptyView
--exclude Pods
--exclude Carthage
--exclude .build
Expand Down
31 changes: 7 additions & 24 deletions ora/App/OraApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
return .terminateLater
}

func application(_ application: NSApplication, open urls: [URL]) {
handleIncomingURLs(urls)
}

func getWindow() -> NSWindow? {
if let key = NSApp.keyWindow { return key }
if let visible = NSApp.windows.first(where: { $0.isVisible }) { return visible }
if let any = NSApp.windows.first {
any.makeKeyAndOrderFront(nil)
return any
}
return WindowFactory.makeMainWindow(rootView: OraRoot())
}

func handleIncomingURLs(_ urls: [URL]) {
let window = getWindow()!
for url in urls {
let userInfo: [AnyHashable: Any] = ["url": url]
DispatchQueue.main.async {
NotificationCenter.default.post(name: .openURL, object: window, userInfo: userInfo)
}
}
}
// Links opened from other apps are routed by SwiftUI to a browser window
// (see `handlesExternalEvents` on the scenes and in `OraRoot`), so the app
// delegate deliberately does not implement `application(_:open:)`.
}

func deleteSwiftDataStore(_ loc: String) {
Expand Down Expand Up @@ -82,7 +62,9 @@ struct OraApp: App {
.defaultSize(width: 1440, height: 900)
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentMinSize)
.handlesExternalEvents(matching: [])
// Links opened from other apps (Ora as default browser) go to a regular
// window; SwiftUI creates one if none is open, e.g. on a cold launch.
.handlesExternalEvents(matching: ["*"])

WindowGroup("Private", id: "private") {
OraRoot(isPrivate: true)
Expand Down Expand Up @@ -114,6 +96,7 @@ struct OraApp: App {
.windowToolbarStyle(UnifiedCompactWindowToolbarStyle())
.windowResizability(.contentSize)
.defaultSize(width: 980, height: 640)
.handlesExternalEvents(matching: [])
.commands { OraCommands() }
}
}
8 changes: 8 additions & 0 deletions ora/App/OraCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ struct OraCommands: Commands {
NotificationCenter.default.post(name: .goForward, object: NSApp.keyWindow)
}
.keyboardShortcut(KeyboardShortcuts.Navigation.forward.keyboardShortcut)

#if DEBUG
Divider()

Button("Copy Page Crash Command (All Tabs in Space)") {
NotificationCenter.default.post(name: .simulatePageCrash, object: NSApp.keyWindow)
}
#endif
}

CommandMenu("Tabs") {
Expand Down
52 changes: 52 additions & 0 deletions ora/App/OraRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,23 @@ struct OraRoot: View {
.modelContext(downloadContext)
.withTheme()
.enableInjection()
// External links: reuse this window instead of opening a new one
// per link. `onOpenURL` only runs once the view is ready, so links
// that launch Ora are not lost. Private windows never take them.
.handlesExternalEvents(
preferring: privacyMode.isPrivate ? [] : ["*"],
allowing: privacyMode.isPrivate ? [] : ["*"]
)
.onOpenURL { url in
guard !privacyMode.isPrivate else { return }
tabManager.openTab(
url: url,
historyManager: historyManager,
downloadManager: downloadManager,
focusAfterOpening: true,
isPrivate: false
)
}
.onAppear {
downloadManager.toastManager = toastManager
Task {
Expand Down Expand Up @@ -148,6 +165,17 @@ struct OraRoot: View {
return false
}

// Full screen: macOS only offers Globe+F in the View menu, which
// keyboards without a Globe key cannot type. Honour Ora's own,
// customizable shortcut (⌃⌘F by default) as well.
keyModifierListener.registerKeyDownHandler { event in
guard KeyChord(fromEvent: event) == KeyboardShortcuts.Window.fullscreen.currentChord,
let targetWindow = event.window ?? NSApp.keyWindow
else { return false }
targetWindow.toggleFullScreen(nil)
return true
}

// Cmd+Q quit confirmation
NotificationCenter.default.addObserver(forName: .quitRequested, object: nil, queue: .main) { note in
guard note.object as? NSWindow === window ?? NSApp.keyWindow else { return }
Expand Down Expand Up @@ -213,6 +241,30 @@ struct OraRoot: View {
tabManager.activeTab?.reload()
}
}
#if DEBUG
NotificationCenter.default.addObserver(
forName: .simulatePageCrash,
object: nil,
queue: .main
) { note in
Task { @MainActor in
guard note.object as? NSWindow === window ?? NSApp.keyWindow else { return }
// The sandbox forbids Ora to kill a web content process
// itself: copy the command that crashes them, to run in
// Terminal (SIGKILL ends a process exactly like a crash).
let tabs = tabManager.activeContainer?.tabs ?? []
let pids = Set(tabs.compactMap { $0.browserPage?.webContentProcessIdentifier })
guard !pids.isEmpty else {
print("[Simulate crash] No web content process found")
return
}
let command = "kill -9 " + pids.sorted().map(String.init).joined(separator: " ")
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(command, forType: .string)
print("[Simulate crash] Copied to the clipboard, run it in Terminal: \(command)")
}
}
#endif
NotificationCenter.default.addObserver(forName: .goBack, object: nil, queue: .main) { note in
Task { @MainActor in
guard note.object as? NSWindow === window ?? NSApp.keyWindow else { return }
Expand Down
Loading