Skip to content
Lucent
SEARCH LUCENT

Search guides, APIs, and examples.

GitHub

Very early and experimental. The language, the generated native code and every package API change without notice. Do not use Lucent in production.

Coming from Swift or Kotlin

Swift and Kotlin concepts map to Lucent one to one. A module written in Swift, Kotlin and a JS binding becomes one Lucent module.

SwiftKotlinLucent
structdata classan object type, type Point = { x: number; y: number }: copied at the boundary
classclassclass: reference-counted, as in Swift
protocolinterfaceinterface, used with implements
a delegatea listenera class that implements the SDK's protocol or interface
asyncsuspendasync: runs on the Lucent thread
@MainActorDispatchers.Mainmain(() => …) from lucent:thread
T?T?T | null
throwsexceptionsthrow error(code, message); SDK errors arrive as errors with a code
if #available(iOS 17, *)Build.VERSION.SDK_INT >= 34available("ios", 17), available("android", 34)
enumenum classenum

SDK names stay as you know them: Swift's names on iOS (UIDevice.current), Java's on Android (Build.MODEL). Nested types join with _, as in UIImpactFeedbackGenerator_FeedbackStyle and Build_VERSION.

An Expo module of the clipboard needs Swift, Kotlin and a JavaScript binding. This is a short version of each, in the Expo Modules API:

ClipboardModule.swift
import ExpoModulesCore
public class ClipboardModule: Module {
public func definition() -> ModuleDefinition {
Name("Clipboard")
AsyncFunction("getStringAsync") { () -> String in
UIPasteboard.general.string ?? ""
}.runOnQueue(.main)
AsyncFunction("setStringAsync") { (text: String) in
UIPasteboard.general.string = text
}.runOnQueue(.main)
}
}

The Lucent module does the same in one file, and JavaScript imports it directly:

clipboard.lucent.ts
import { PLATFORM } from "lucent:platform";
import { UIPasteboard } from "lucent:ios/UIKit";
import { ClipboardManager, ClipData } from "lucent:android/android.content";
import { appContext } from "lucent:android";
import { main } from "lucent:thread";
export async function getStringAsync(): Promise<string> {
if (PLATFORM === "ios") {
return main(() => UIPasteboard.general.string ?? "");
} else {
return main(() => {
const clip = appContext().getSystemService(ClipboardManager)?.getPrimaryClip();
return clip?.getItemAt(0)?.getText() ?? "";
});
}
}
export async function setStringAsync(text: string): Promise<void> {
if (PLATFORM === "ios") {
await main(() => {
UIPasteboard.general.string = text;
});
} else {
await main(() => {
const clip = ClipData.newPlainText(null, text);
if (clip) appContext().getSystemService(ClipboardManager)?.setPrimaryClip(clip);
});
}
}
See the C++
m_clipboard.mm
// Generated by Lucent from clipboard.lucent.ts. Do not edit.
#import <UIKit/UIKit.h>
#include <lucent/platform/ios.h>
#include "m_clipboard.h"
#pragma push_macro("getStringAsync")
#undef getStringAsync
#pragma push_macro("setStringAsync")
#undef setStringAsync
#pragma push_macro("text")
#undef text
namespace lucent_app {
lucent::Promise<lucent::String> m_clipboard::getStringAsync() {
#line 8 "clipboard.lucent.ts"
{
#line 9 "clipboard.lucent.ts"
lucent::Fn<lucent::String()> v0_ = lucent::Fn<lucent::String()>([]() mutable -> lucent::String {
#line 9 "clipboard.lucent.ts"
lucent::NativeRef v0_ = lucent::objc::wrap([UIPasteboard generalPasteboard], "UIPasteboard.general");
lucent::Opt<lucent::String> v1_ = lucent::objc::fromNSStringOpt([((UIPasteboard*)lucent::objc::unwrap(v0_)) string]);
bool v3_ = !v1_.has();
lucent::String v6_{};
if (v3_) {
v6_ = lucent::String();
} else {
v6_ = v1_.value();
}
#line 9 "clipboard.lucent.ts"
return v6_;
});
#line 9 "clipboard.lucent.ts"
lucent::Promise<lucent::String> v1_ = lucent::runOnMain(v0_);
lucent::String v2_ = co_await v1_;
co_return v2_;
}
}
lucent::Promise<void> m_clipboard::setStringAsync(lucent::String p0_) {
#line 19 "clipboard.lucent.ts"
{
#line 20 "clipboard.lucent.ts"
lucent::Fn<void()> v1_ = lucent::Fn<void()>([text = p0_]() mutable -> void {
#line 21 "clipboard.lucent.ts"
lucent::NativeRef v0_ = lucent::objc::wrap([UIPasteboard generalPasteboard], "UIPasteboard.general");
lucent::Opt<lucent::String> v2_ = lucent::Opt<lucent::String>(text);
(void)({
auto v_ = v2_;
[((UIPasteboard*)lucent::objc::unwrap(v0_)) setString:lucent::objc::ifPresent(v_, [&](const auto& x_) {
return lucent::objc::toNSString(x_);
})];
v_;
});
});
#line 20 "clipboard.lucent.ts"
lucent::Promise<void> v2_ = lucent::runOnMain(v1_);
co_await v2_;
}
co_return;
}
void m_clipboard::init() {
}
} // namespace lucent_app
#pragma pop_macro("text")
#pragma pop_macro("setStringAsync")
#pragma pop_macro("getStringAsync")
  • The JS binding is gone: the module's exports are the API, typed from the source.
  • main() replaces runOnQueue. APIs that Swift marks @MainActor compile only inside it.
  • Kotlin's !! and ?. become Lucent's ?. and ??, since unannotated Java references are nullable.

The full port of expo-clipboard is in the examples.