A native macOS IDE for Haskell, built in SwiftUI with a Liquid Glass UI. Curry
drives user-installed GHC via GHCup, Stack, or Cabal; talks to
haskell-language-server over LSP for autocomplete and diagnostics; ships a
built-in GHCi REPL for quick snippets; and layers on Apple Intelligence
suggestions (on supported Macs) via Apple's on-device Foundation Models.
Curry is distributed via Developer ID + notarization. This isn't an oversight —
it's a hard constraint of the product. The Mac App Store requires App Sandbox
on every Mach-O in the bundle, and the kernel refuses any child process that
doesn't carry exactly com.apple.security.app-sandbox + com.apple.security.inherit
and nothing else. The GHC, HLS, Stack, Cabal, and GHCup binaries are signed
upstream with Developer ID and don't — can't — satisfy that rule, so a
sandboxed Curry simply cannot exec them. Every serious commercial IDE on
macOS (Nova, VS Code, JetBrains, Fleet, Zed, Sublime Text) reached the same
conclusion.
See Sources/CurryIDE/Resources/CurryIDE.entitlements for the exact Hardened
Runtime relaxations we request (JIT, library validation disabled, DYLD env
vars), and the research document bundled in the original spec for citations.
| Subsystem | Path |
|---|---|
| App entry + chrome | Sources/CurryIDE/App/ |
| Liquid Glass styles | Sources/CurryIDE/Views/LiquidGlass.swift |
| Project sidebar | Sources/CurryIDE/Views/Sidebar/ |
| Tabbed / split editor | Sources/CurryIDE/Views/Editor/ |
| Haskell highlighter | Sources/CurryIDE/Views/Editor/HaskellSyntaxHighlighter.swift |
| GHCi console | Sources/CurryIDE/Views/Console/ |
| GHCup / Stack / Cabal | Sources/CurryIDE/Services/ |
| HLS LSP client | Sources/CurryIDE/LSP/ |
| Apple Intelligence | Sources/CurryIDE/AppleIntelligence/ |
| Workspace model | Sources/CurryIDE/Models/Workspace.swift |
┌──────────────────────── Curry (SwiftUI) ────────────────────────┐
│ RootView │
│ ├── ProjectSidebar (dismissable, ⌘0) │
│ ├── EditorSurface │
│ │ └── recursive HSplit/VSplit of EditorPaneView │
│ │ └── NSTextView-backed CodeEditorView │
│ │ ├── HaskellSyntaxHighlighter │
│ │ └── completions (HLS + keywords + AI hint) │
│ └── ConsoleSurface │
│ ├── GHCi → GHCiSession (spawned `ghci`) │
│ ├── Build → BuildRunner → stack/cabal │
│ └── Problems → parsed GHC diagnostics │
│ │
│ Services │
│ ├── ToolchainManager → GHCupService (`ghcup list/install`) │
│ ├── HLSClient → LSPTransport (stdio JSON-RPC) │
│ └── IntelligenceCoord. → FoundationModels.LanguageModelSession │
└─────────────────────────────────────────────────────────────────┘
The editor talks to compilers only through a service layer. This is the "build server" split the research recommends: if a sandboxed learning variant is ever needed, swap the services out without touching the UI.
- macOS 15.1 (Sequoia) or newer for Apple Intelligence; the rest of the IDE runs on macOS 13+.
- Apple silicon for on-device Foundation Models. On Intel Macs the AI panel reports "unavailable" and the editor falls back to HLS + keyword completion.
- GHCup installed at
~/.ghcup/bin/ghcup. Curry guides the user through install on first launch if it's missing.
Curry is an Xcode project, generated from project.yml by
XcodeGen. The .xcodeproj itself is
gitignored — project.yml is the source of truth.
# One-time:
brew install xcodegen
# Every time you pull or edit project.yml:
xcodegen generate
# Then either:
open CurryIDE.xcodeproj # develop in Xcode 16+
# or, for a command-line build:
xcodebuild -project CurryIDE.xcodeproj -scheme CurryIDE buildRunning from Xcode uses "Sign to Run Locally" by default — it'll launch on your
machine without a paid Developer account. For a shippable .app:
- Set
DEVELOPMENT_TEAMinproject.ymlto your Developer ID team, regenerate. - Archive in Xcode (Product ▸ Archive) or with
xcodebuild archive. - Run the archive through
notarytool(xcrun notarytool submit …) and staple (xcrun stapler staple).
The hand-authored Info.plist and CurryIDE.entitlements at
Sources/CurryIDE/Resources/ are wired in via INFOPLIST_FILE and
CODE_SIGN_ENTITLEMENTS build settings, so they remain the authoritative
source — XcodeGen does not regenerate them.
- Tabs per pane; drag-and-drop reordering is a TODO
- Split views via the pane menu: Split Right / Split Down. The same file
can be open in multiple splits (shared
SourceDocumentbuffer). - Dismissable sidebar via ⌘0 or the toolbar icon.
- Line numbers rendered by a custom
NSRulerView. - Find via the built-in
NSTextViewfind bar.
Regex-driven in HaskellSyntaxHighlighter.swift. Handles block/line comments,
pragmas, strings, chars, numbers, reserved keywords, constructors, and
operators (->, =>, <-, ::, |). The highlighter only reprocesses the
line range around the edit, keeping edits cheap on multi-thousand-line files.
For higher fidelity later: either adopt tree-sitter-haskell via
TreeSitter.xcframework, or consume HLS's semantic tokens response.
Three sources, merged:
- HLS
textDocument/completion(authoritative types, in-scope symbols). HaskellKeywords.all— reserved words + common Prelude.- Apple Intelligence ghost-text suggestions from
IntelligenceCoordinator.suggest(...).
HLS completions are cached per-document so the synchronous NSTextView
completions callback can serve immediately while a background request refreshes
the cache.
GHCupService wraps the ghcup CLI for listing, installing, and switching
toolchains. The Haskell ▸ Manage Toolchains… sheet walks users through
installing GHCup itself (we don't pipe get-ghcup.haskell.org automatically —
it deserves a deliberate click) and then through picking a GHC + HLS.
ProjectScaffolder runs stack new or cabal init --non-interactive. The
⌘⇧N sheet picks a name, location, and build system.
GHCiSession spawns ghci, pipes its stdout/stderr into a SwiftUI transcript
view, and feeds lines back through stdin. Use ⌘⇧G to load the current
file; the Haskell ▸ menu also exposes a scratch session.
IntelligenceCoordinator keeps one LanguageModelSession warm (on macOS 15.1+,
Apple silicon, AI enabled). Every edit schedules a debounced 450 ms request
that sends the ±1600 characters around the caret plus a compact symbol table
produced by HaskellProjectIndexer. The returned snippet becomes
document.inlineHint, which the editor can render as ghost text — wire that
surface in when you're ready.
Falls back silently to HLS + keyword completion on unsupported hardware.
| Shortcut | Action |
|---|---|
| ⌘⇧N | New Haskell Project… |
| ⌘O | Open folder… |
| ⌘0 | Toggle sidebar |
| ⌘B | Build |
| ⌘R | Run |
| ⌘⇧G | Open GHCi for current file |
- A packaged
.xcodeproj(SPM + Xcode 16 handles everything the app needs). - Ghost-text rendering — the
SourceDocument.inlineHintvalue is populated but the editor doesn't paint it yet.NSTextView'sdrawInsertionPoint(in:color:turnedOn:)override is the usual seam. publishDiagnostics→Workspace.diagnosticswiring.- Drag-and-drop reorder of editor tabs.
- Debugger integration (would be GHCi breakpoints today).
Each is a small, localized addition on top of the interfaces already defined.
