diff --git a/apps/mobile/README.md b/apps/mobile/README.md index ad0242c2714..552216bf353 100644 --- a/apps/mobile/README.md +++ b/apps/mobile/README.md @@ -34,6 +34,30 @@ Build and run the local iOS dev client: vp run ios:dev ``` +If your Xcode account only has a Personal Team, use a bundle identifier you control and opt into the +reduced-capability local build. Personal Team builds omit the widget extension, push entitlement, and +native Sign in with Apple entitlement; builds without this opt-in are unchanged. + +```bash +T3CODE_IOS_PERSONAL_TEAM=1 \ +T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID=com.example.t3code.dev \ +vp run ios:dev +``` + +Build and install a self-contained Release app that does not need Metro: + +```bash +vp run ios:release +``` + +The Personal Team equivalent also needs a unique bundle identifier: + +```bash +T3CODE_IOS_PERSONAL_TEAM=1 \ +T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID=com.example.t3code \ +vp run ios:release +``` + Build and run the local iOS preview app: ```bash diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 1dd884aeb8d..0cb550059e3 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -8,6 +8,15 @@ const repoEnv = loadRepoEnv(); Object.assign(process.env, repoEnv); const APP_VARIANT = resolveAppVariant(repoEnv.APP_VARIANT); +const isIosPersonalTeamBuild = repoEnv.T3CODE_IOS_PERSONAL_TEAM === "1"; + +const personalTeamBundleIdentifier = repoEnv.T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID?.trim(); + +if (isIosPersonalTeamBuild && !personalTeamBundleIdentifier) { + throw new Error( + "T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID is required when T3CODE_IOS_PERSONAL_TEAM=1.", + ); +} const VARIANT_CONFIG: Record< AppVariant, @@ -17,7 +26,6 @@ const VARIANT_CONFIG: Record< readonly iosIcon: string; readonly iosBundleIdentifier: string; readonly androidPackage: string; - readonly relyingParty?: string; } > = { development: { @@ -26,7 +34,6 @@ const VARIANT_CONFIG: Record< iosIcon: "./assets/icon-composer-dev.icon", iosBundleIdentifier: "com.t3tools.t3code.dev", androidPackage: "com.t3tools.t3code.dev", - relyingParty: "clerk.t3.codes", }, preview: { appName: "T3 Code Preview", @@ -34,7 +41,6 @@ const VARIANT_CONFIG: Record< iosIcon: "./assets/icon-composer-prod.icon", iosBundleIdentifier: "com.t3tools.t3code.preview", androidPackage: "com.t3tools.t3code.preview", - relyingParty: "clerk.t3.codes", }, production: { appName: "T3 Code", @@ -42,7 +48,6 @@ const VARIANT_CONFIG: Record< iosIcon: "./assets/icon-composer-prod.icon", iosBundleIdentifier: "com.t3tools.t3code", androidPackage: "com.t3tools.t3code", - relyingParty: "clerk.t3.codes", }, }; @@ -58,6 +63,27 @@ function resolveAppVariant(value: string | undefined): AppVariant { } const variant = VARIANT_CONFIG[APP_VARIANT]; +const iosBundleIdentifier = + isIosPersonalTeamBuild && personalTeamBundleIdentifier + ? personalTeamBundleIdentifier + : variant.iosBundleIdentifier; + +const widgetsPlugin = [ + "expo-widgets", + { + bundleIdentifier: `${iosBundleIdentifier}.widgets`, + groupIdentifier: `group.${iosBundleIdentifier}`, + enablePushNotifications: true, + widgets: [ + { + name: "AgentActivity", + displayName: "Agent Activity", + description: "Shows the current state of active T3 Code agents.", + supportedFamilies: ["systemSmall", "systemMedium", "accessoryRectangular"], + }, + ], + }, +] satisfies NonNullable[number]; const config: ExpoConfig = { name: variant.appName, @@ -80,11 +106,7 @@ const config: ExpoConfig = { ios: { icon: variant.iosIcon, supportsTablet: true, - bundleIdentifier: variant.iosBundleIdentifier, - associatedDomains: [ - `applinks:${variant.relyingParty}`, - `webcredentials:${variant.relyingParty}`, - ], + bundleIdentifier: iosBundleIdentifier, infoPlist: { NSAppTransportSecurity: { NSAllowsArbitraryLoads: true, @@ -110,9 +132,10 @@ const config: ExpoConfig = { }, plugins: [ "expo-router", + "expo-asset", "expo-font", "expo-secure-store", - ["@clerk/expo", { theme: "./clerk-theme.json" }], + ["@clerk/expo", { theme: "./clerk-theme.json", appleSignIn: !isIosPersonalTeamBuild }], "expo-web-browser", [ "expo-camera", @@ -148,24 +171,10 @@ const config: ExpoConfig = { }, ], "./plugins/withIosCocoaPodsUuidCache.cjs", - [ - "expo-widgets", - { - bundleIdentifier: `${variant.iosBundleIdentifier}.widgets`, - groupIdentifier: `group.${variant.iosBundleIdentifier}`, - enablePushNotifications: true, - widgets: [ - { - name: "AgentActivity", - displayName: "Agent Activity", - description: "Shows the current state of active T3 Code agents.", - supportedFamilies: ["systemSmall", "systemMedium", "accessoryRectangular"], - }, - ], - }, - ], + ...(!isIosPersonalTeamBuild ? [widgetsPlugin] : []), "./plugins/withIosSceneLifecycle.cjs", "./plugins/withAndroidCleartextTraffic.cjs", + ...(isIosPersonalTeamBuild ? ["./plugins/withoutIosPersonalTeamCapabilities.cjs"] : []), ], extra: { appVariant: APP_VARIANT, diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js index d314f6206c6..fe886077697 100644 --- a/apps/mobile/metro.config.js +++ b/apps/mobile/metro.config.js @@ -6,6 +6,7 @@ const { withUniwindConfig } = require("uniwind/metro"); /** @type {import("expo/metro-config").MetroConfig} */ const config = getDefaultConfig(__dirname); const workspaceRoot = path.resolve(__dirname, "../.."); +const escapedWorkspaceRoot = workspaceRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const mobileShikiRoot = path.dirname(require.resolve("shiki/package.json", { paths: [__dirname] })); const resolveShikiDependencyRoot = (packageName) => { const entryPath = require.resolve(packageName, { paths: [mobileShikiRoot] }); @@ -25,6 +26,14 @@ const resolveShikiDependencyRoot = (packageName) => { config.watchFolders = [...new Set([...(config.watchFolders ?? []), workspaceRoot])]; config.resolver = { ...config.resolver, + blockList: [ + ...(Array.isArray(config.resolver?.blockList) + ? config.resolver.blockList + : config.resolver?.blockList + ? [config.resolver.blockList] + : []), + new RegExp(`${escapedWorkspaceRoot}[/\\\\]\\.t3[/\\\\].*`), + ], extraNodeModules: { // oxlint-disable-next-line unicorn/no-useless-fallback-in-spread ...(config.resolver?.extraNodeModules ?? {}), diff --git a/apps/mobile/modules/t3-composer-editor/android/build.gradle b/apps/mobile/modules/t3-composer-editor/android/build.gradle new file mode 100644 index 00000000000..dfdb4d16a14 --- /dev/null +++ b/apps/mobile/modules/t3-composer-editor/android/build.gradle @@ -0,0 +1,19 @@ +apply plugin: 'com.android.library' +apply plugin: 'org.jetbrains.kotlin.android' + +group = 'com.t3tools.composereditor' +version = '0.0.0' + +android { + namespace 'expo.modules.t3composereditor' + compileSdk rootProject.ext.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + } +} + +dependencies { + implementation project(':expo-modules-core') +} diff --git a/apps/mobile/modules/t3-composer-editor/android/src/main/AndroidManifest.xml b/apps/mobile/modules/t3-composer-editor/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..94cbbcfc396 --- /dev/null +++ b/apps/mobile/modules/t3-composer-editor/android/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorModule.kt b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorModule.kt new file mode 100644 index 00000000000..6085f4785c4 --- /dev/null +++ b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorModule.kt @@ -0,0 +1,68 @@ +package expo.modules.t3composereditor + +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class T3ComposerEditorModule : Module() { + override fun definition() = ModuleDefinition { + Name("T3ComposerEditor") + + View(T3ComposerEditorView::class) { + Prop("controlledDocumentJson") { view: T3ComposerEditorView, documentJson: String -> + view.setControlledDocumentJson(documentJson) + } + Prop("themeJson") { view: T3ComposerEditorView, themeJson: String -> + view.setThemeJson(themeJson) + } + Prop("placeholder") { view: T3ComposerEditorView, placeholder: String -> + view.setPlaceholder(placeholder) + } + Prop("fontFamily") { view: T3ComposerEditorView, fontFamily: String -> + view.setFontFamily(fontFamily) + } + Prop("fontSize") { view: T3ComposerEditorView, fontSize: Double -> + view.setFontSize(fontSize.toFloat()) + } + Prop("lineHeight") { view: T3ComposerEditorView, lineHeight: Double -> + view.setLineHeight(lineHeight.toFloat()) + } + Prop("contentInsetVertical") { view: T3ComposerEditorView, contentInsetVertical: Double -> + view.setContentInsetVertical(contentInsetVertical.toInt()) + } + Prop("editable") { view: T3ComposerEditorView, editable: Boolean -> + view.setEditable(editable) + } + Prop("scrollEnabled") { view: T3ComposerEditorView, scrollEnabled: Boolean -> + view.setScrollEnabled(scrollEnabled) + } + Prop("autoFocus") { view: T3ComposerEditorView, autoFocus: Boolean -> + view.setAutoFocus(autoFocus) + } + Prop("autoCorrect") { view: T3ComposerEditorView, autoCorrect: Boolean -> + view.setAutoCorrect(autoCorrect) + } + Prop("spellCheck") { view: T3ComposerEditorView, spellCheck: Boolean -> + view.setSpellCheck(spellCheck) + } + + Events( + "onComposerChange", + "onComposerSelectionChange", + "onComposerFocus", + "onComposerBlur", + "onComposerPasteImages", + "onComposerContentSizeChange", + ) + + AsyncFunction("focus") { view: T3ComposerEditorView -> + view.focusEditor() + } + AsyncFunction("blur") { view: T3ComposerEditorView -> + view.blurEditor() + } + AsyncFunction("setSelection") { view: T3ComposerEditorView, start: Int, end: Int -> + view.setSelection(start, end) + } + } + } +} diff --git a/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt new file mode 100644 index 00000000000..bcb0f03d295 --- /dev/null +++ b/apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt @@ -0,0 +1,454 @@ +package expo.modules.t3composereditor + +import android.content.Context +import android.content.ClipboardManager +import android.graphics.Color +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.graphics.Typeface +import android.text.Editable +import android.text.InputType +import android.text.Spanned +import android.text.TextWatcher +import android.text.style.ReplacementSpan +import android.view.Gravity +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import expo.modules.kotlin.AppContext +import expo.modules.kotlin.viewevent.EventDispatcher +import expo.modules.kotlin.views.ExpoView +import org.json.JSONObject +import kotlin.math.max + +class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { + private val editor = SelectionAwareEditText(context) + private val onComposerChange by EventDispatcher() + private val onComposerSelectionChange by EventDispatcher() + private val onComposerFocus by EventDispatcher() + private val onComposerBlur by EventDispatcher() + private val onComposerPasteImages by EventDispatcher() + private val onComposerContentSizeChange by EventDispatcher() + private var applyingNativeValue = false + private var desiredLineHeightPx = 0 + private var lastContentHeight = 0 + private var contentInsetVertical = 0 + private var tokensJson = "[]" + private var tokens: List = emptyList() + private var chipTheme = ComposerChipTheme.default() + private var autoCorrect = true + private var spellCheck = true + private var nativeEventCount = 0 + + init { + editor.setBackgroundColor(Color.TRANSPARENT) + editor.gravity = Gravity.TOP or Gravity.START + editor.includeFontPadding = false + editor.isSingleLine = false + editor.minLines = 1 + editor.inputType = + InputType.TYPE_CLASS_TEXT or + InputType.TYPE_TEXT_FLAG_MULTI_LINE or + InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + editor.setTextColor(Color.BLACK) + editor.setHintTextColor(Color.GRAY) + editor.setPadding(0, 0, 0, 0) + editor.selectionListener = { start, end -> + if (!applyingNativeValue) { + emitSelectionChange(start, end) + } + } + editor.pasteImagesListener = { uris -> + onComposerPasteImages(mapOf("uris" to uris)) + } + editor.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + onComposerFocus(emptyMap()) + } else { + onComposerBlur(emptyMap()) + } + } + editor.addTextChangedListener( + object : TextWatcher { + override fun beforeTextChanged(text: CharSequence?, start: Int, count: Int, after: Int) = Unit + override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) = Unit + + override fun afterTextChanged(editable: Editable?) { + if (applyingNativeValue) return + val nextValue = editable.toString() + val selection = currentSelectionPayload() + nativeEventCount += 1 + onComposerChange( + mapOf( + "value" to nextValue, + "selection" to selection, + "eventCount" to nativeEventCount, + ), + ) + emitContentSizeIfNeeded() + } + }, + ) + editor.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> emitContentSizeIfNeeded() } + addView( + editor, + LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT), + ) + } + + fun setControlledDocumentJson(documentJson: String) { + val document = try { + JSONObject(documentJson) + } catch (_: Exception) { + return + } + val mostRecentEventCount = document.optInt("mostRecentEventCount", -1) + if (mostRecentEventCount < nativeEventCount) return + + val value = document.optString("value") + if (document.optBoolean("isNativeEcho") && editor.text.toString() != value) return + + val nextTokensJson = document.optString("tokensJson", "[]") + val nextTokens = if (nextTokensJson == tokensJson) tokens else parseTokens(nextTokensJson) + val requestedSelection = document.optJSONObject("selection") + val previousSelectionStart = editor.selectionStart.coerceAtLeast(0) + val previousSelectionEnd = editor.selectionEnd.coerceAtLeast(0) + val valueChanged = editor.text.toString() != value + + applyingNativeValue = true + try { + if (valueChanged) { + editor.setText(value) + } + tokensJson = nextTokensJson + tokens = nextTokens + applyTokenSpans() + if (requestedSelection != null) { + applySelection( + requestedSelection.optInt("start", previousSelectionStart), + requestedSelection.optInt("end", previousSelectionEnd), + ) + } else if (valueChanged) { + applySelection(previousSelectionStart, previousSelectionEnd) + } + } finally { + applyingNativeValue = false + } + emitContentSizeIfNeeded() + } + + fun setThemeJson(themeJson: String) { + try { + val theme = JSONObject(themeJson) + editor.setTextColor(parseColor(theme.optString("text"), Color.BLACK)) + editor.setHintTextColor(parseColor(theme.optString("placeholder"), Color.GRAY)) + chipTheme = ComposerChipTheme( + chipBackground = parseColor(theme.optString("chipBackground"), chipTheme.chipBackground), + chipBorder = parseColor(theme.optString("chipBorder"), chipTheme.chipBorder), + chipText = parseColor(theme.optString("chipText"), chipTheme.chipText), + skillBackground = parseColor( + theme.optString("skillBackground"), + chipTheme.skillBackground, + ), + skillBorder = parseColor(theme.optString("skillBorder"), chipTheme.skillBorder), + skillText = parseColor(theme.optString("skillText"), chipTheme.skillText), + ) + applyTokenSpans() + } catch (_: Exception) { + } + } + + fun setPlaceholder(placeholder: String) { + editor.hint = placeholder + } + + fun setFontFamily(fontFamily: String) { + editor.typeface = if (fontFamily.contains("Mono", ignoreCase = true)) { + Typeface.MONOSPACE + } else { + Typeface.DEFAULT + } + } + + fun setFontSize(fontSize: Float) { + editor.textSize = fontSize + applyLineHeight() + } + + fun setLineHeight(lineHeight: Float) { + desiredLineHeightPx = (lineHeight * resources.displayMetrics.density).toInt() + applyLineHeight() + } + + fun setContentInsetVertical(contentInsetVertical: Int) { + this.contentInsetVertical = + max(0, (contentInsetVertical * resources.displayMetrics.density).toInt()) + editor.setPadding(0, this.contentInsetVertical, 0, this.contentInsetVertical) + emitContentSizeIfNeeded() + } + + fun setEditable(editable: Boolean) { + editor.isEnabled = editable + editor.isFocusable = editable + editor.isFocusableInTouchMode = editable + editor.isCursorVisible = editable + } + + fun setScrollEnabled(scrollEnabled: Boolean) { + editor.isVerticalScrollBarEnabled = scrollEnabled + } + + fun setAutoFocus(autoFocus: Boolean) { + if (autoFocus) { + post { focusEditor() } + } + } + + fun setAutoCorrect(autoCorrect: Boolean) { + this.autoCorrect = autoCorrect + updateInputFlags() + } + + fun setSpellCheck(spellCheck: Boolean) { + this.spellCheck = spellCheck + updateInputFlags() + } + + fun focusEditor() { + editor.requestFocus() + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.showSoftInput(editor, InputMethodManager.SHOW_IMPLICIT) + } + + fun blurEditor() { + editor.clearFocus() + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.hideSoftInputFromWindow(editor.windowToken, 0) + } + + fun setSelection(start: Int, end: Int) { + applySelection(start, end) + } + + private fun applySelection(start: Int, end: Int) { + val textLength = editor.text?.length ?: 0 + val safeStart = start.coerceIn(0, textLength) + val safeEnd = end.coerceIn(0, textLength) + editor.setSelection(safeStart, safeEnd) + } + + private fun updateInputFlags() { + var flags = + InputType.TYPE_CLASS_TEXT or + InputType.TYPE_TEXT_FLAG_MULTI_LINE or + InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + flags = if (autoCorrect && spellCheck) { + flags or InputType.TYPE_TEXT_FLAG_AUTO_CORRECT + } else { + flags or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + } + editor.inputType = flags + } + + private fun applyLineHeight() { + if (desiredLineHeightPx <= 0) return + val fontHeight = editor.paint.fontMetricsInt.descent - editor.paint.fontMetricsInt.ascent + editor.setLineSpacing(max(0, desiredLineHeightPx - fontHeight).toFloat(), 1f) + } + + private fun currentSelectionPayload(): Map = + mapOf( + "start" to editor.selectionStart.coerceAtLeast(0), + "end" to editor.selectionEnd.coerceAtLeast(0), + ) + + private fun emitSelectionChange(start: Int, end: Int) { + onComposerSelectionChange( + mapOf( + "value" to editor.text.toString(), + "selection" to mapOf("start" to start, "end" to end), + "eventCount" to nativeEventCount, + ), + ) + } + + private fun emitContentSizeIfNeeded() { + val height = editor.layout?.height ?: editor.measuredHeight + val contentHeight = height + contentInsetVertical * 2 + if (contentHeight == lastContentHeight) return + lastContentHeight = contentHeight + onComposerContentSizeChange( + mapOf("height" to contentHeight / resources.displayMetrics.density), + ) + } + + private fun applyTokenSpans() { + val editable = editor.text ?: return + editable.getSpans(0, editable.length, ComposerChipSpan::class.java).forEach(editable::removeSpan) + tokens.forEach { token -> + if (token.start < 0 || token.end <= token.start || token.end > editable.length) return@forEach + val expectedSource = editable.substring(token.start, token.end) + if (expectedSource != token.source) return@forEach + editable.setSpan( + ComposerChipSpan(token.label, token.type == "skill", chipTheme, resources.displayMetrics.density), + token.start, + token.end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + } + editor.invalidate() + } + + private fun parseColor(value: String, fallback: Int): Int = + try { + Color.parseColor(value) + } catch (_: Exception) { + fallback + } +} + +private data class ComposerToken( + val type: String, + val source: String, + val label: String, + val start: Int, + val end: Int, +) + +private data class ComposerChipTheme( + val chipBackground: Int, + val chipBorder: Int, + val chipText: Int, + val skillBackground: Int, + val skillBorder: Int, + val skillText: Int, +) { + companion object { + fun default() = ComposerChipTheme( + chipBackground = Color.rgb(238, 240, 243), + chipBorder = Color.rgb(210, 214, 220), + chipText = Color.rgb(35, 39, 45), + skillBackground = Color.rgb(233, 239, 255), + skillBorder = Color.rgb(185, 200, 245), + skillText = Color.rgb(45, 72, 155), + ) + } +} + +private class ComposerChipSpan( + private val label: String, + private val skill: Boolean, + private val theme: ComposerChipTheme, + density: Float, +) : ReplacementSpan() { + private val horizontalPadding = 7f * density + private val verticalPadding = 2f * density + private val cornerRadius = 6f * density + private val borderWidth = density + + override fun getSize( + paint: Paint, + text: CharSequence, + start: Int, + end: Int, + fontMetrics: Paint.FontMetricsInt?, + ): Int { + fontMetrics?.let { + val extra = verticalPadding.toInt() + val base = paint.fontMetricsInt + it.top = base.top - extra + it.ascent = base.ascent - extra + it.descent = base.descent + extra + it.bottom = base.bottom + extra + } + return (paint.measureText(label) + horizontalPadding * 2).toInt() + } + + override fun draw( + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint, + ) { + val width = paint.measureText(label) + horizontalPadding * 2 + val metrics = paint.fontMetrics + val rect = RectF( + x, + y + metrics.ascent - verticalPadding, + x + width, + y + metrics.descent + verticalPadding, + ) + val originalColor = paint.color + val originalStyle = paint.style + val originalStrokeWidth = paint.strokeWidth + + paint.color = if (skill) theme.skillBackground else theme.chipBackground + paint.style = Paint.Style.FILL + canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint) + paint.color = if (skill) theme.skillBorder else theme.chipBorder + paint.style = Paint.Style.STROKE + paint.strokeWidth = borderWidth + canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint) + paint.color = if (skill) theme.skillText else theme.chipText + paint.style = Paint.Style.FILL + canvas.drawText(label, x + horizontalPadding, y.toFloat(), paint) + + paint.color = originalColor + paint.style = originalStyle + paint.strokeWidth = originalStrokeWidth + } +} + +private fun parseTokens(value: String): List = try { + val array = org.json.JSONArray(value) + List(array.length()) { index -> + val token = array.getJSONObject(index) + ComposerToken( + type = token.optString("type"), + source = token.optString("source"), + label = token.optString("label"), + start = token.optInt("start"), + end = token.optInt("end"), + ) + } +} catch (_: Exception) { + emptyList() +} + +private class SelectionAwareEditText(context: Context) : EditText(context) { + var selectionListener: ((Int, Int) -> Unit)? = null + var pasteImagesListener: ((List) -> Unit)? = null + + override fun onSelectionChanged(selStart: Int, selEnd: Int) { + super.onSelectionChanged(selStart, selEnd) + selectionListener?.invoke(selStart, selEnd) + } + + override fun onTextContextMenuItem(id: Int): Boolean { + if (id == android.R.id.paste || id == android.R.id.pasteAsPlainText) { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager + val clip = clipboard?.primaryClip + val imageUris = buildList { + if (clip != null) { + for (index in 0 until clip.itemCount) { + clip.getItemAt(index).uri?.let { uri -> + val mimeType = context.contentResolver.getType(uri) + if (mimeType?.startsWith("image/") == true) add(uri.toString()) + } + } + } + } + if (imageUris.isNotEmpty()) { + pasteImagesListener?.invoke(imageUris) + return true + } + } + return super.onTextContextMenuItem(id) + } +} diff --git a/apps/mobile/modules/t3-composer-editor/expo-module.config.json b/apps/mobile/modules/t3-composer-editor/expo-module.config.json index 0d6384cd91a..56fd899920c 100644 --- a/apps/mobile/modules/t3-composer-editor/expo-module.config.json +++ b/apps/mobile/modules/t3-composer-editor/expo-module.config.json @@ -1,6 +1,9 @@ { - "platforms": ["apple"], + "platforms": ["apple", "android"], "apple": { "modules": ["T3ComposerEditorModule"] + }, + "android": { + "modules": ["expo.modules.t3composereditor.T3ComposerEditorModule"] } } diff --git a/apps/mobile/modules/t3-native-controls/android/build.gradle b/apps/mobile/modules/t3-native-controls/android/build.gradle new file mode 100644 index 00000000000..ba0622b7f0e --- /dev/null +++ b/apps/mobile/modules/t3-native-controls/android/build.gradle @@ -0,0 +1,19 @@ +apply plugin: 'com.android.library' +apply plugin: 'org.jetbrains.kotlin.android' + +group = 'com.t3tools.nativecontrols' +version = '0.0.0' + +android { + namespace 'expo.modules.t3nativecontrols' + compileSdk rootProject.ext.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + } +} + +dependencies { + implementation project(':expo-modules-core') +} diff --git a/apps/mobile/modules/t3-native-controls/android/src/main/AndroidManifest.xml b/apps/mobile/modules/t3-native-controls/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..94cbbcfc396 --- /dev/null +++ b/apps/mobile/modules/t3-native-controls/android/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3HeaderButtonView.kt b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3HeaderButtonView.kt new file mode 100644 index 00000000000..489038aa8ac --- /dev/null +++ b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3HeaderButtonView.kt @@ -0,0 +1,85 @@ +package expo.modules.t3nativecontrols + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.view.View +import expo.modules.kotlin.AppContext +import expo.modules.kotlin.viewevent.EventDispatcher +import expo.modules.kotlin.views.ExpoView + +class T3HeaderButtonView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { + private val iconView = HeaderIconView(context) + private val onTriggered by EventDispatcher() + + init { + isClickable = true + isFocusable = true + setOnClickListener { + onTriggered(emptyMap()) + } + addView(iconView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)) + } + + fun setLabel(label: String) { + contentDescription = label + } + + fun setSystemImage(systemImage: String) { + iconView.systemImage = systemImage + } +} + +private class HeaderIconView(context: Context) : View(context) { + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#6B7280") + strokeCap = Paint.Cap.ROUND + strokeJoin = Paint.Join.ROUND + strokeWidth = 3f * resources.displayMetrics.density + style = Paint.Style.STROKE + } + + var systemImage: String = "gearshape" + set(value) { + field = value + invalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val cx = width / 2f + val cy = height / 2f + val size = minOf(width, height).toFloat() + if (systemImage == "square.and.pencil") { + drawNewTask(canvas, cx, cy, size) + } else { + drawSettings(canvas, cx, cy, size) + } + } + + private fun drawSettings(canvas: Canvas, cx: Float, cy: Float, size: Float) { + val radius = size * 0.12f + canvas.drawCircle(cx, cy, radius, paint) + for (index in 0 until 8) { + val angle = Math.PI * index / 4.0 + val inner = size * 0.19f + val outer = size * 0.27f + val sx = cx + kotlin.math.cos(angle).toFloat() * inner + val sy = cy + kotlin.math.sin(angle).toFloat() * inner + val ex = cx + kotlin.math.cos(angle).toFloat() * outer + val ey = cy + kotlin.math.sin(angle).toFloat() * outer + canvas.drawLine(sx, sy, ex, ey, paint) + } + } + + private fun drawNewTask(canvas: Canvas, cx: Float, cy: Float, size: Float) { + val left = cx - size * 0.2f + val top = cy - size * 0.16f + val right = cx + size * 0.14f + val bottom = cy + size * 0.2f + canvas.drawRoundRect(left, top, right, bottom, size * 0.04f, size * 0.04f, paint) + canvas.drawLine(cx - size * 0.02f, cy + size * 0.13f, cx + size * 0.24f, cy - size * 0.13f, paint) + canvas.drawLine(cx + size * 0.17f, cy - size * 0.2f, cx + size * 0.24f, cy - size * 0.13f, paint) + } +} diff --git a/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3NativeControlsModule.kt b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3NativeControlsModule.kt new file mode 100644 index 00000000000..4d5f02aaa9c --- /dev/null +++ b/apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3NativeControlsModule.kt @@ -0,0 +1,21 @@ +package expo.modules.t3nativecontrols + +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class T3NativeControlsModule : Module() { + override fun definition() = ModuleDefinition { + Name("T3NativeControls") + + View(T3HeaderButtonView::class) { + Prop("label") { view: T3HeaderButtonView, label: String -> + view.setLabel(label) + } + Prop("systemImage") { view: T3HeaderButtonView, systemImage: String -> + view.setSystemImage(systemImage) + } + + Events("onTriggered") + } + } +} diff --git a/apps/mobile/modules/t3-native-controls/expo-module.config.json b/apps/mobile/modules/t3-native-controls/expo-module.config.json index b53b3c50a03..d9a77f14e25 100644 --- a/apps/mobile/modules/t3-native-controls/expo-module.config.json +++ b/apps/mobile/modules/t3-native-controls/expo-module.config.json @@ -1,6 +1,9 @@ { - "platforms": ["apple"], + "platforms": ["apple", "android"], "apple": { "modules": ["T3NativeControlsModule", "T3KeyboardCommandsModule"] + }, + "android": { + "modules": ["expo.modules.t3nativecontrols.T3NativeControlsModule"] } } diff --git a/apps/mobile/modules/t3-review-diff/android/build.gradle b/apps/mobile/modules/t3-review-diff/android/build.gradle new file mode 100644 index 00000000000..22bb070b3b8 --- /dev/null +++ b/apps/mobile/modules/t3-review-diff/android/build.gradle @@ -0,0 +1,19 @@ +apply plugin: 'com.android.library' +apply plugin: 'org.jetbrains.kotlin.android' + +group = 'com.t3tools.reviewdiff' +version = '0.0.0' + +android { + namespace 'expo.modules.t3reviewdiff' + compileSdk rootProject.ext.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + } +} + +dependencies { + implementation project(':expo-modules-core') +} diff --git a/apps/mobile/modules/t3-review-diff/android/src/main/AndroidManifest.xml b/apps/mobile/modules/t3-review-diff/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..94cbbcfc396 --- /dev/null +++ b/apps/mobile/modules/t3-review-diff/android/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/apps/mobile/modules/t3-review-diff/android/src/main/java/expo/modules/t3reviewdiff/T3ReviewDiffModule.kt b/apps/mobile/modules/t3-review-diff/android/src/main/java/expo/modules/t3reviewdiff/T3ReviewDiffModule.kt new file mode 100644 index 00000000000..ee40275ede8 --- /dev/null +++ b/apps/mobile/modules/t3-review-diff/android/src/main/java/expo/modules/t3reviewdiff/T3ReviewDiffModule.kt @@ -0,0 +1,78 @@ +package expo.modules.t3reviewdiff + +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class T3ReviewDiffModule : Module() { + override fun definition() = ModuleDefinition { + Name("T3ReviewDiffSurface") + + View(T3ReviewDiffView::class) { + Prop("tokensResetKey") { view: T3ReviewDiffView, tokensResetKey: String -> + view.setTokensResetKey(tokensResetKey) + } + Prop("contentResetKey") { view: T3ReviewDiffView, contentResetKey: String -> + view.setContentResetKey(contentResetKey) + } + Prop("collapsedFileIdsJson") { view: T3ReviewDiffView, collapsedFileIdsJson: String -> + view.setCollapsedFileIdsJson(collapsedFileIdsJson) + } + Prop("viewedFileIdsJson") { view: T3ReviewDiffView, viewedFileIdsJson: String -> + view.setViewedFileIdsJson(viewedFileIdsJson) + } + Prop("selectedRowIdsJson") { view: T3ReviewDiffView, selectedRowIdsJson: String -> + view.setSelectedRowIdsJson(selectedRowIdsJson) + } + Prop("collapsedCommentIdsJson") { view: T3ReviewDiffView, collapsedCommentIdsJson: String -> + view.setCollapsedCommentIdsJson(collapsedCommentIdsJson) + } + Prop("appearanceScheme") { view: T3ReviewDiffView, appearanceScheme: String -> + view.setAppearanceScheme(appearanceScheme) + } + Prop("themeJson") { view: T3ReviewDiffView, themeJson: String -> + view.setThemeJson(themeJson) + } + Prop("styleJson") { view: T3ReviewDiffView, styleJson: String -> + view.setStyleJson(styleJson) + } + Prop("rowHeight") { view: T3ReviewDiffView, rowHeight: Double -> + view.setRowHeight(rowHeight.toFloat()) + } + Prop("contentWidth") { view: T3ReviewDiffView, contentWidth: Double -> + view.setContentWidth(contentWidth.toFloat()) + } + Prop("initialRowIndex") { view: T3ReviewDiffView, initialRowIndex: Double -> + view.setInitialRowIndex(initialRowIndex) + } + + Events( + "onDebug", + "onVisibleFileChange", + "onToggleFile", + "onToggleViewedFile", + "onPressLine", + "onToggleComment", + ) + + AsyncFunction("scrollToFile") { view: T3ReviewDiffView, fileId: String, animated: Boolean -> + view.scrollToFile(fileId, animated) + } + AsyncFunction("scrollToTop") { view: T3ReviewDiffView, animated: Boolean -> + view.scrollToTop(animated) + } + AsyncFunction("setRowsJson") { view: T3ReviewDiffView, rowsJson: String -> + view.setRowsJson(rowsJson) + } + AsyncFunction("setTokensJson") { view: T3ReviewDiffView, tokensJson: String -> + view.setTokensJson(tokensJson) + } + AsyncFunction("setTokensPatchJson") { view: T3ReviewDiffView, tokensPatchJson: String -> + view.setTokensPatchJson(tokensPatchJson) + } + + OnViewDestroys { view: T3ReviewDiffView -> + view.cleanup() + } + } + } +} diff --git a/apps/mobile/modules/t3-review-diff/android/src/main/java/expo/modules/t3reviewdiff/T3ReviewDiffView.kt b/apps/mobile/modules/t3-review-diff/android/src/main/java/expo/modules/t3reviewdiff/T3ReviewDiffView.kt new file mode 100644 index 00000000000..3997503b82f --- /dev/null +++ b/apps/mobile/modules/t3-review-diff/android/src/main/java/expo/modules/t3reviewdiff/T3ReviewDiffView.kt @@ -0,0 +1,1023 @@ +package expo.modules.t3reviewdiff + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Typeface +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.VelocityTracker +import android.view.View +import android.view.ViewGroup +import android.view.ViewConfiguration +import android.widget.OverScroller +import expo.modules.kotlin.AppContext +import expo.modules.kotlin.viewevent.EventDispatcher +import expo.modules.kotlin.views.ExpoView +import org.json.JSONArray +import org.json.JSONObject +import java.util.concurrent.Executors +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +class T3ReviewDiffView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { + private val canvasView = DiffCanvasView(context) + private val onDebug by EventDispatcher() + private val onVisibleFileChange by EventDispatcher() + private val onToggleFile by EventDispatcher() + private val onToggleViewedFile by EventDispatcher() + private val onPressLine by EventDispatcher() + private val onToggleComment by EventDispatcher() + private var rows: List = emptyList() + private var visibleRows: List = emptyList() + private var collapsedFileIds: Set = emptySet() + private var viewedFileIds: Set = emptySet() + private var selectedRowIds: Set = emptySet() + private var collapsedCommentIds: Set = emptySet() + private var initialRowIndex = 0 + private var pendingInitialScroll = false + private var lastVisibleFileId: String? = null + private var tokensResetKey = "" + private var contentResetKey = "" + private var rowsDecodeGeneration = 0 + private var tokensDecodeGeneration = 0 + private val payloadDecodeExecutor = Executors.newSingleThreadExecutor() + private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop + private val minimumFlingVelocity = ViewConfiguration.get(context).scaledMinimumFlingVelocity + private val verticalScroller = OverScroller(context) + private val horizontalScroller = OverScroller(context) + private var dragAxis: DragAxis? = null + private var lastTouchX = 0f + private var lastTouchY = 0f + private var velocityTracker: VelocityTracker? = null + + init { + canvasView.onRowTap = { row, gesture -> handleRowTap(row, gesture) } + canvasView.onVisibleRowsChanged = { first, last -> + onDebug( + mapOf( + "message" to "visible-range", + "firstRowIndex" to first, + "lastRowIndex" to last, + ), + ) + emitVisibleFile(first) + } + + addView( + canvasView, + LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT), + ) + } + + fun setTokensResetKey(value: String) { + if (tokensResetKey == value) return + tokensResetKey = value + canvasView.tokensByRowId = emptyMap() + } + + fun setContentResetKey(value: String) { + if (contentResetKey == value) return + contentResetKey = value + tokensDecodeGeneration += 1 + canvasView.tokensByRowId = emptyMap() + lastVisibleFileId = null + pendingInitialScroll = true + canvasView.setVerticalOffset(0) + canvasView.setHorizontalOffset(0) + applyPendingInitialScroll() + } + + fun setCollapsedFileIdsJson(value: String) { + collapsedFileIds = parseStringSet(value) + rebuildVisibleRows() + } + + fun setViewedFileIdsJson(value: String) { + viewedFileIds = parseStringSet(value) + canvasView.viewedFileIds = viewedFileIds + } + + fun setSelectedRowIdsJson(value: String) { + selectedRowIds = parseStringSet(value) + canvasView.selectedRowIds = selectedRowIds + } + + fun setCollapsedCommentIdsJson(value: String) { + collapsedCommentIds = parseStringSet(value) + rebuildVisibleRows() + } + + fun setAppearanceScheme(value: String) { + canvasView.theme = DiffTheme.fallback(value) + } + + fun setThemeJson(value: String) { + canvasView.theme = DiffTheme.fromJson(value, canvasView.theme) + } + + fun setStyleJson(value: String) { + canvasView.style = DiffStyle.fromJson(value, canvasView.style, resources.displayMetrics.density) + } + + fun setRowHeight(value: Float) { + canvasView.style = canvasView.style.copy(rowHeightPx = dp(value)) + } + + fun setContentWidth(value: Float) { + canvasView.contentWidthPx = max(width, dp(value).toInt()) + } + + fun setInitialRowIndex(value: Double) { + initialRowIndex = value.toInt().coerceAtLeast(0) + pendingInitialScroll = true + applyPendingInitialScroll() + } + + fun setRowsJson(value: String) { + rowsDecodeGeneration += 1 + val generation = rowsDecodeGeneration + payloadDecodeExecutor.execute { + val decodedRows = parseRows(value) + post { + if (generation != rowsDecodeGeneration) return@post + rows = decodedRows + lastVisibleFileId = null + rebuildVisibleRows() + } + } + } + + fun setTokensJson(value: String) { + tokensDecodeGeneration += 1 + val generation = tokensDecodeGeneration + payloadDecodeExecutor.execute { + val decodedTokens = parseTokensObject(value) + post { + if (generation != tokensDecodeGeneration) return@post + canvasView.tokensByRowId = decodedTokens + } + } + } + + fun setTokensPatchJson(value: String) { + payloadDecodeExecutor.execute { + try { + val payload = JSONObject(value) + val resetKey = payload.optString("resetKey") + val decodedTokens = parseTokensObject( + payload.optJSONObject("tokensByRowId") ?: JSONObject(), + ) + post { + if (resetKey.isNotEmpty() && resetKey != tokensResetKey) return@post + if (decodedTokens.isNotEmpty()) { + canvasView.tokensByRowId = canvasView.tokensByRowId + decodedTokens + } + } + } catch (_: Exception) { + } + } + } + + fun cleanup() { + payloadDecodeExecutor.shutdownNow() + } + + fun scrollToFile(fileId: String, animated: Boolean) { + val index = visibleRows.indexOfFirst { it.kind == "file" && it.resolvedFileId == fileId } + if (index < 0) return + scrollToY(canvasView.rowTop(index), animated) + } + + fun scrollToTop(animated: Boolean) { + scrollToY(0, animated) + } + + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + verticalScroller.forceFinished(true) + horizontalScroller.forceFinished(true) + dragAxis = null + lastTouchX = event.x + lastTouchY = event.y + parent?.requestDisallowInterceptTouchEvent(true) + return false + } + MotionEvent.ACTION_MOVE -> { + if (dragAxis == null) { + val deltaX = event.x - lastTouchX + val deltaY = event.y - lastTouchY + if (max(abs(deltaX), abs(deltaY)) > touchSlop) { + dragAxis = if (abs(deltaY) >= abs(deltaX)) DragAxis.VERTICAL else DragAxis.HORIZONTAL + } + } + return dragAxis != null + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> return dragAxis != null + } + return false + } + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + velocityTracker?.recycle() + velocityTracker = VelocityTracker.obtain() + } + velocityTracker?.addMovement(event) + val handled = super.dispatchTouchEvent(event) + if ( + (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) && + dragAxis == null + ) { + velocityTracker?.recycle() + velocityTracker = null + parent?.requestDisallowInterceptTouchEvent(false) + } + return handled + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + val axis = dragAxis ?: return false + when (event.actionMasked) { + MotionEvent.ACTION_MOVE -> { + val deltaX = (lastTouchX - event.x).toInt() + val deltaY = (lastTouchY - event.y).toInt() + if (axis == DragAxis.VERTICAL) { + canvasView.scrollByVertical(deltaY) + } else { + canvasView.scrollByHorizontal(deltaX) + } + lastTouchX = event.x + lastTouchY = event.y + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + if (event.actionMasked == MotionEvent.ACTION_UP) { + velocityTracker?.computeCurrentVelocity(1000) + if (axis == DragAxis.VERTICAL) { + val velocity = -(velocityTracker?.yVelocity ?: 0f).toInt() + if (abs(velocity) >= minimumFlingVelocity) { + verticalScroller.fling( + 0, + canvasView.verticalOffset(), + 0, + velocity, + 0, + 0, + 0, + canvasView.maxVerticalOffset(), + ) + postInvalidateOnAnimation() + } + } else { + val velocity = -(velocityTracker?.xVelocity ?: 0f).toInt() + if (abs(velocity) >= minimumFlingVelocity) { + horizontalScroller.fling( + canvasView.horizontalOffset(), + 0, + velocity, + 0, + 0, + canvasView.maxHorizontalOffset(), + 0, + 0, + ) + postInvalidateOnAnimation() + } + } + } + dragAxis = null + velocityTracker?.recycle() + velocityTracker = null + parent?.requestDisallowInterceptTouchEvent(false) + } + } + return true + } + + override fun computeScroll() { + var animating = false + if (verticalScroller.computeScrollOffset()) { + canvasView.setVerticalOffset(verticalScroller.currY) + animating = true + } + if (horizontalScroller.computeScrollOffset()) { + canvasView.setHorizontalOffset(horizontalScroller.currX) + animating = true + } + if (animating) { + postInvalidateOnAnimation() + } + } + + private fun rebuildVisibleRows() { + val filtered = ArrayList(rows.size) + var currentFileCollapsed = false + rows.forEach { row -> + if (row.kind == "file") { + currentFileCollapsed = collapsedFileIds.contains(row.resolvedFileId) + filtered.add(row) + } else if (!currentFileCollapsed) { + if (row.kind != "comment" || !collapsedCommentIds.contains(row.id)) { + filtered.add(row) + } else { + filtered.add(row.copy(commentText = "Comment collapsed")) + } + } + } + visibleRows = filtered + canvasView.rows = filtered + canvasView.viewedFileIds = viewedFileIds + canvasView.selectedRowIds = selectedRowIds + applyPendingInitialScroll() + } + + private fun handleRowTap(row: DiffRow, gesture: String) { + when (row.kind) { + "file" -> { + if (gesture == "longPress") { + onToggleViewedFile(mapOf("fileId" to row.resolvedFileId)) + } else { + onToggleFile(mapOf("fileId" to row.resolvedFileId)) + } + } + "comment" -> onToggleComment(mapOf("commentId" to row.id)) + "line" -> { + val payload = mutableMapOf( + "rowId" to row.id, + "fileId" to row.resolvedFileId, + "gesture" to gesture, + "change" to row.change, + ) + row.oldLineNumber?.let { payload["oldLineNumber"] = it } + row.newLineNumber?.let { payload["newLineNumber"] = it } + onPressLine(payload) + } + } + } + + private fun emitVisibleFile(firstVisibleIndex: Int) { + if (visibleRows.isEmpty()) return + val start = firstVisibleIndex.coerceIn(0, visibleRows.lastIndex) + val fileId = (start downTo 0) + .asSequence() + .map { visibleRows[it].resolvedFileId } + .firstOrNull { it.isNotEmpty() } + ?: return + if (fileId == lastVisibleFileId) return + lastVisibleFileId = fileId + onVisibleFileChange(mapOf("fileId" to fileId)) + } + + private fun applyPendingInitialScroll() { + if (!pendingInitialScroll || visibleRows.isEmpty()) return + pendingInitialScroll = false + val index = initialRowIndex.coerceIn(0, visibleRows.lastIndex) + post { canvasView.setVerticalOffset(canvasView.rowTop(index)) } + } + + private fun scrollToY(y: Int, animated: Boolean) { + val target = y.coerceIn(0, canvasView.maxVerticalOffset()) + if (animated) { + verticalScroller.startScroll( + 0, + canvasView.verticalOffset(), + 0, + target - canvasView.verticalOffset(), + 250, + ) + postInvalidateOnAnimation() + } else { + canvasView.setVerticalOffset(target) + } + } + + private fun dp(value: Float): Float = value * resources.displayMetrics.density + + private enum class DragAxis { + VERTICAL, + HORIZONTAL, + } +} + +private data class DiffRow( + val kind: String, + val id: String, + val fileId: String, + val filePath: String, + val previousPath: String?, + val changeType: String, + val additions: Int, + val deletions: Int, + val text: String, + val content: String, + val change: String, + val oldLineNumber: Int?, + val newLineNumber: Int?, + val commentText: String, + val commentRangeLabel: String, + val commentSectionTitle: String, +) { + val resolvedFileId: String get() = fileId.ifEmpty { id } +} + +private data class DiffToken( + val content: String, + val color: Int?, + val fontStyle: Int, +) + +private data class DiffTheme( + val background: Int, + val text: Int, + val mutedText: Int, + val headerBackground: Int, + val border: Int, + val hunkBackground: Int, + val hunkText: Int, + val addBackground: Int, + val deleteBackground: Int, + val addBar: Int, + val deleteBar: Int, + val addText: Int, + val deleteText: Int, +) { + companion object { + fun fallback(scheme: String): DiffTheme = if (scheme == "dark") { + DiffTheme( + background = Color.rgb(20, 22, 25), + text = Color.rgb(236, 238, 240), + mutedText = Color.rgb(153, 160, 170), + headerBackground = Color.rgb(26, 29, 33), + border = Color.rgb(52, 57, 64), + hunkBackground = Color.rgb(7, 31, 40), + hunkText = Color.rgb(0, 159, 255), + addBackground = Color.rgb(13, 47, 40), + deleteBackground = Color.rgb(57, 20, 21), + addBar = Color.rgb(0, 202, 177), + deleteBar = Color.rgb(255, 46, 63), + addText = Color.rgb(94, 204, 113), + deleteText = Color.rgb(255, 103, 98), + ) + } else { + DiffTheme( + background = Color.WHITE, + text = Color.rgb(7, 7, 7), + mutedText = Color.rgb(102, 106, 115), + headerBackground = Color.WHITE, + border = Color.rgb(222, 224, 228), + hunkBackground = Color.rgb(224, 242, 255), + hunkText = Color.rgb(0, 130, 220), + addBackground = Color.rgb(229, 248, 245), + deleteBackground = Color.rgb(255, 230, 231), + addBar = Color.rgb(0, 172, 151), + deleteBar = Color.rgb(213, 44, 54), + addText = Color.rgb(25, 130, 67), + deleteText = Color.rgb(190, 38, 48), + ) + } + + fun fromJson(value: String, fallback: DiffTheme): DiffTheme = try { + val json = JSONObject(value) + DiffTheme( + background = color(json, "background", fallback.background), + text = color(json, "text", fallback.text), + mutedText = color(json, "mutedText", fallback.mutedText), + headerBackground = color(json, "headerBackground", fallback.headerBackground), + border = color(json, "border", fallback.border), + hunkBackground = color(json, "hunkBackground", fallback.hunkBackground), + hunkText = color(json, "hunkText", fallback.hunkText), + addBackground = color(json, "addBackground", fallback.addBackground), + deleteBackground = color(json, "deleteBackground", fallback.deleteBackground), + addBar = color(json, "addBar", fallback.addBar), + deleteBar = color(json, "deleteBar", fallback.deleteBar), + addText = color(json, "addText", fallback.addText), + deleteText = color(json, "deleteText", fallback.deleteText), + ) + } catch (_: Exception) { + fallback + } + + private fun color(json: JSONObject, key: String, fallback: Int): Int = + parseColor(json.optString(key), fallback) + } +} + +private data class DiffStyle( + val rowHeightPx: Float, + val gutterWidthPx: Float, + val codePaddingPx: Float, + val changeBarWidthPx: Float, + val fileHeaderHeightPx: Float, + val codeFontSizePx: Float, + val lineNumberFontSizePx: Float, +) { + companion object { + fun defaults(density: Float): DiffStyle = DiffStyle( + rowHeightPx = 20f * density, + gutterWidthPx = 72f * density, + codePaddingPx = 10f * density, + changeBarWidthPx = 3f * density, + fileHeaderHeightPx = 44f * density, + codeFontSizePx = 12f * density, + lineNumberFontSizePx = 10f * density, + ) + + fun fromJson(value: String, fallback: DiffStyle, density: Float): DiffStyle = try { + val json = JSONObject(value) + DiffStyle( + rowHeightPx = json.floatDp("rowHeight", fallback.rowHeightPx, density), + gutterWidthPx = json.floatDp("gutterWidth", fallback.gutterWidthPx, density), + codePaddingPx = json.floatDp("codePadding", fallback.codePaddingPx, density), + changeBarWidthPx = json.floatDp("changeBarWidth", fallback.changeBarWidthPx, density), + fileHeaderHeightPx = json.floatDp("fileHeaderHeight", fallback.fileHeaderHeightPx, density), + codeFontSizePx = json.floatSp("codeFontSize", fallback.codeFontSizePx, density), + lineNumberFontSizePx = json.floatSp("lineNumberFontSize", fallback.lineNumberFontSizePx, density), + ) + } catch (_: Exception) { + fallback + } + } +} + +private class DiffCanvasView(context: Context) : View(context) { + private val density = resources.displayMetrics.density + private val backgroundPaint = Paint() + private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { typeface = Typeface.MONOSPACE } + private val boldTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) + } + private val gestureDetector = GestureDetector( + context, + object : GestureDetector.SimpleOnGestureListener() { + override fun onDown(event: MotionEvent): Boolean = true + + override fun onSingleTapUp(event: MotionEvent): Boolean { + rowAt(event.y)?.let { onRowTap?.invoke(it, "tap") } + return true + } + + override fun onLongPress(event: MotionEvent) { + rowAt(event.y)?.let { onRowTap?.invoke(it, "longPress") } + } + }, + ) + private var rowOffsets = intArrayOf(0) + private var verticalOffset = 0 + private var horizontalOffset = 0 + private var lastVisibleRange: Pair? = null + + var rows: List = emptyList() + set(value) { + field = value + rebuildOffsets() + } + var tokensByRowId: Map> = emptyMap() + set(value) { + field = value + invalidate() + } + var viewedFileIds: Set = emptySet() + set(value) { + field = value + invalidate() + } + var selectedRowIds: Set = emptySet() + set(value) { + field = value + invalidate() + } + var theme: DiffTheme = DiffTheme.fallback("light") + set(value) { + field = value + invalidate() + } + var style: DiffStyle = DiffStyle.defaults(density) + set(value) { + field = value + rebuildOffsets() + } + var contentWidthPx: Int = (1200 * density).toInt() + set(value) { + field = max(value, suggestedMinimumWidth) + setHorizontalOffset(horizontalOffset) + invalidate() + } + var onRowTap: ((DiffRow, String) -> Unit)? = null + var onVisibleRowsChanged: ((Int, Int) -> Unit)? = null + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + setMeasuredDimension( + MeasureSpec.getSize(widthMeasureSpec), + MeasureSpec.getSize(heightMeasureSpec), + ) + } + + override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) { + super.onSizeChanged(width, height, oldWidth, oldHeight) + setVerticalOffset(verticalOffset) + setHorizontalOffset(horizontalOffset) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + canvas.drawColor(theme.background) + if (rows.isEmpty()) return + val first = rowIndexAt(max(0, verticalOffset + canvas.clipBounds.top)) + val last = rowIndexAt( + min(max(0, rowOffsets.last() - 1), verticalOffset + canvas.clipBounds.bottom), + ).coerceAtLeast(first) + for (index in first..last.coerceAtMost(rows.lastIndex)) { + drawRow( + canvas, + rows[index], + rowOffsets[index] - verticalOffset, + rowOffsets[index + 1] - verticalOffset, + ) + } + drawStickyFileHeader(canvas, first) + drawHorizontalScrollIndicator(canvas) + emitVisibleRange() + } + + override fun onTouchEvent(event: MotionEvent): Boolean = gestureDetector.onTouchEvent(event) + + fun rowTop(index: Int): Int = rowOffsets[index.coerceIn(0, max(0, rowOffsets.size - 2))] + + fun setVerticalOffset(value: Int) { + val maxOffset = max(0, (rowOffsets.lastOrNull() ?: 0) - height) + val nextOffset = value.coerceIn(0, maxOffset) + if (verticalOffset == nextOffset) return + verticalOffset = nextOffset + invalidate() + emitVisibleRange() + } + + fun scrollByVertical(delta: Int) { + setVerticalOffset(verticalOffset + delta) + } + + fun verticalOffset(): Int = verticalOffset + + fun maxVerticalOffset(): Int = max(0, (rowOffsets.lastOrNull() ?: 0) - height) + + fun setHorizontalOffset(value: Int) { + val nextOffset = value.coerceIn(0, maxHorizontalOffset()) + if (horizontalOffset == nextOffset) return + horizontalOffset = nextOffset + invalidate() + } + + fun scrollByHorizontal(delta: Int) { + setHorizontalOffset(horizontalOffset + delta) + } + + fun horizontalOffset(): Int = horizontalOffset + + fun maxHorizontalOffset(): Int = max(0, contentWidthPx - width) + + private fun rebuildOffsets() { + rowOffsets = IntArray(rows.size + 1) + rows.forEachIndexed { index, row -> + rowOffsets[index + 1] = rowOffsets[index] + rowHeight(row) + } + setVerticalOffset(verticalOffset) + requestLayout() + invalidate() + } + + private fun rowHeight(row: DiffRow): Int = when (row.kind) { + "file" -> style.fileHeaderHeightPx.toInt() + "comment" -> max((style.rowHeightPx * 3.2f).toInt(), (56 * density).toInt()) + else -> style.rowHeightPx.toInt() + }.coerceAtLeast(1) + + private fun rowIndexAt(y: Int): Int { + if (rows.isEmpty()) return 0 + var low = 0 + var high = rows.lastIndex + while (low <= high) { + val middle = (low + high) ushr 1 + when { + y < rowOffsets[middle] -> high = middle - 1 + y >= rowOffsets[middle + 1] -> low = middle + 1 + else -> return middle + } + } + return low.coerceIn(0, rows.lastIndex) + } + + private fun rowAt(y: Float): DiffRow? { + stickyFileHeader(firstVisibleRow())?.let { sticky -> + if (y >= max(0, sticky.top).toFloat() && y < sticky.bottom.toFloat()) { + return rows.getOrNull(sticky.index) + } + } + return rows.getOrNull(rowIndexAt(verticalOffset + y.toInt())) + } + + private fun firstVisibleRow(): Int = rowIndexAt(verticalOffset) + + private fun emitVisibleRange() { + if (rows.isEmpty()) return + val first = rowIndexAt(verticalOffset) + val last = rowIndexAt(verticalOffset + max(1, height)) + val range = first to last + if (range == lastVisibleRange) return + lastVisibleRange = range + onVisibleRowsChanged?.invoke(first, last) + } + + private fun drawRow(canvas: Canvas, row: DiffRow, top: Int, bottom: Int) { + when (row.kind) { + "file" -> drawFileRow(canvas, row, top, bottom) + "hunk" -> drawHunkRow(canvas, row, top, bottom) + "notice" -> drawNoticeRow(canvas, row, top, bottom) + "comment" -> drawCommentRow(canvas, row, top, bottom) + else -> drawLineRow(canvas, row, top, bottom) + } + } + + private fun drawFileRow(canvas: Canvas, row: DiffRow, top: Int, bottom: Int) { + fill(canvas, theme.headerBackground, 0f, top.toFloat(), width.toFloat(), bottom.toFloat()) + val baseline = centeredBaseline(top, bottom, boldTextPaint.apply { textSize = 13f * density }) + boldTextPaint.color = theme.text + val marker = if (viewedFileIds.contains(row.resolvedFileId)) "[x] " else "[ ] " + textPaint.color = theme.mutedText + textPaint.textSize = 11f * density + val stats = "+${row.additions} -${row.deletions}" + val statsX = max(12f * density, width - textPaint.measureText(stats) - 16f * density) + val title = ellipsize(marker + row.filePath, boldTextPaint, statsX - 28f * density) + canvas.drawText(title, 12f * density, baseline, boldTextPaint) + canvas.drawText(stats, statsX, baseline, textPaint) + drawBottomBorder(canvas, bottom) + } + + private fun drawHunkRow(canvas: Canvas, row: DiffRow, top: Int, bottom: Int) { + fill(canvas, theme.hunkBackground, 0f, top.toFloat(), width.toFloat(), bottom.toFloat()) + textPaint.color = theme.hunkText + textPaint.textSize = style.codeFontSizePx + drawScrollableCode(canvas, top, bottom) { codeX -> + canvas.drawText( + row.text.ifEmpty { row.content }, + codeX, + centeredBaseline(top, bottom, textPaint), + textPaint, + ) + } + } + + private fun drawNoticeRow(canvas: Canvas, row: DiffRow, top: Int, bottom: Int) { + textPaint.color = theme.mutedText + textPaint.textSize = style.codeFontSizePx + drawScrollableCode(canvas, top, bottom) { codeX -> + canvas.drawText(row.text, codeX, centeredBaseline(top, bottom, textPaint), textPaint) + } + } + + private fun drawCommentRow(canvas: Canvas, row: DiffRow, top: Int, bottom: Int) { + fill(canvas, theme.headerBackground, style.gutterWidthPx, top.toFloat(), width.toFloat(), bottom.toFloat()) + boldTextPaint.color = theme.text + boldTextPaint.textSize = 12f * density + drawScrollableCode(canvas, top, bottom) { codeX -> + canvas.drawText( + row.commentSectionTitle.ifEmpty { row.commentRangeLabel.ifEmpty { "Comment" } }, + codeX, + top + 20f * density, + boldTextPaint, + ) + textPaint.color = theme.mutedText + textPaint.textSize = 12f * density + canvas.drawText(row.commentText, codeX, top + 42f * density, textPaint) + } + drawBottomBorder(canvas, bottom) + } + + private fun drawLineRow(canvas: Canvas, row: DiffRow, top: Int, bottom: Int) { + val background = when (row.change) { + "add" -> theme.addBackground + "delete" -> theme.deleteBackground + else -> theme.background + } + fill(canvas, background, 0f, top.toFloat(), width.toFloat(), bottom.toFloat()) + val selected = selectedRowIds.contains(row.id) + if (selected) { + fill( + canvas, + withAlpha(theme.hunkText, if (theme.background == Color.WHITE) 54 else 76), + 0f, + top.toFloat(), + width.toFloat(), + bottom.toFloat(), + ) + } + val barColor = when (row.change) { + "add" -> theme.addBar + "delete" -> theme.deleteBar + else -> Color.TRANSPARENT + } + if (barColor != Color.TRANSPARENT && style.changeBarWidthPx > 0) { + fill(canvas, barColor, 0f, top.toFloat(), style.changeBarWidthPx, bottom.toFloat()) + } + + val tokens = tokensByRowId[row.id] + drawScrollableCode(canvas, top, bottom) { codeX -> + if (tokens.isNullOrEmpty()) { + textPaint.textSize = style.codeFontSizePx + textPaint.color = when (row.change) { + "add" -> theme.addText + "delete" -> theme.deleteText + else -> theme.text + } + canvas.drawText(row.content, codeX, centeredBaseline(top, bottom, textPaint), textPaint) + } else { + var x = codeX + tokens.forEach { token -> + textPaint.textSize = style.codeFontSizePx + textPaint.color = token.color ?: theme.text + textPaint.typeface = when { + token.fontStyle and 1 != 0 -> Typeface.create(Typeface.MONOSPACE, Typeface.ITALIC) + token.fontStyle and 2 != 0 -> Typeface.create(Typeface.MONOSPACE, Typeface.BOLD) + else -> Typeface.MONOSPACE + } + canvas.drawText(token.content, x, centeredBaseline(top, bottom, textPaint), textPaint) + x += textPaint.measureText(token.content) + } + textPaint.typeface = Typeface.MONOSPACE + } + } + + textPaint.textSize = style.lineNumberFontSizePx + textPaint.color = if (selected) theme.text else theme.mutedText + val oldNumber = row.oldLineNumber?.toString().orEmpty() + val newNumber = row.newLineNumber?.toString().orEmpty() + val baseline = centeredBaseline(top, bottom, textPaint) + canvas.drawText(oldNumber, style.changeBarWidthPx + 6f * density, baseline, textPaint) + canvas.drawText(newNumber, style.changeBarWidthPx + style.gutterWidthPx / 2f, baseline, textPaint) + } + + private fun drawScrollableCode( + canvas: Canvas, + top: Int, + bottom: Int, + draw: (Float) -> Unit, + ) { + val gutterEnd = style.changeBarWidthPx + style.gutterWidthPx + canvas.save() + canvas.clipRect(gutterEnd, top.toFloat(), width.toFloat(), bottom.toFloat()) + draw(gutterEnd + style.codePaddingPx - horizontalOffset) + canvas.restore() + } + + private fun drawStickyFileHeader(canvas: Canvas, firstVisibleIndex: Int) { + val sticky = stickyFileHeader(firstVisibleIndex) ?: return + val naturalTop = rowOffsets[sticky.index] - verticalOffset + if (naturalTop == sticky.top) return + drawFileRow(canvas, rows[sticky.index], sticky.top, sticky.bottom) + } + + private fun stickyFileHeader(firstVisibleIndex: Int): StickyFileHeader? { + if (rows.isEmpty()) return null + val fileIndex = (firstVisibleIndex.coerceIn(0, rows.lastIndex) downTo 0) + .firstOrNull { rows[it].kind == "file" } + ?: return null + val headerHeight = rowHeight(rows[fileIndex]) + val nextFileIndex = ((fileIndex + 1)..rows.lastIndex).firstOrNull { rows[it].kind == "file" } + val top = nextFileIndex + ?.let { min(0, rowOffsets[it] - verticalOffset - headerHeight) } + ?: 0 + return StickyFileHeader(fileIndex, top, top + headerHeight) + } + + private fun drawHorizontalScrollIndicator(canvas: Canvas) { + val maxOffset = maxHorizontalOffset() + if (maxOffset <= 0 || width <= 0) return + val trackWidth = width.toFloat() + val thumbWidth = max(24f * density, trackWidth * trackWidth / contentWidthPx) + val thumbTravel = trackWidth - thumbWidth + val left = thumbTravel * horizontalOffset / maxOffset + fill( + canvas, + withAlpha(theme.mutedText, 110), + left, + height - 2f * density, + left + thumbWidth, + height.toFloat(), + ) + } + + private fun fill(canvas: Canvas, color: Int, left: Float, top: Float, right: Float, bottom: Float) { + backgroundPaint.color = color + canvas.drawRect(left, top, right, bottom, backgroundPaint) + } + + private fun drawBottomBorder(canvas: Canvas, bottom: Int) { + borderPaint.color = theme.border + borderPaint.strokeWidth = density + canvas.drawLine(0f, bottom - density / 2f, width.toFloat(), bottom - density / 2f, borderPaint) + } + + private fun centeredBaseline(top: Int, bottom: Int, paint: Paint): Float { + val metrics = paint.fontMetrics + return (top + bottom) / 2f - (metrics.ascent + metrics.descent) / 2f + } + + private fun ellipsize(value: String, paint: Paint, width: Float): String { + if (paint.measureText(value) <= width) return value + val suffix = "..." + val available = max(0f, width - paint.measureText(suffix)) + var end = value.length + while (end > 0 && paint.measureText(value, 0, end) > available) end -= 1 + return value.substring(0, end) + suffix + } + + private data class StickyFileHeader( + val index: Int, + val top: Int, + val bottom: Int, + ) +} + +private fun withAlpha(color: Int, alpha: Int): Int = + Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)) + +private fun parseRows(value: String): List = try { + val array = JSONArray(value) + List(array.length()) { index -> + val row = array.getJSONObject(index) + DiffRow( + kind = row.optString("kind"), + id = row.optString("id"), + fileId = row.optString("fileId"), + filePath = row.optString("filePath"), + previousPath = row.optNullableString("previousPath"), + changeType = row.optString("changeType"), + additions = row.optInt("additions"), + deletions = row.optInt("deletions"), + text = row.optString("text"), + content = row.optString("content"), + change = row.optString("change", "context"), + oldLineNumber = row.optNullableInt("oldLineNumber"), + newLineNumber = row.optNullableInt("newLineNumber"), + commentText = row.optString("commentText"), + commentRangeLabel = row.optString("commentRangeLabel"), + commentSectionTitle = row.optString("commentSectionTitle"), + ) + } +} catch (_: Exception) { + emptyList() +} + +private fun parseTokensObject(value: String): Map> = try { + parseTokensObject(JSONObject(value)) +} catch (_: Exception) { + emptyMap() +} + +private fun parseTokensObject(value: JSONObject): Map> { + val result = LinkedHashMap>() + val keys = value.keys() + while (keys.hasNext()) { + val rowId = keys.next() + val array = value.optJSONArray(rowId) ?: continue + result[rowId] = List(array.length()) { index -> + val token = array.getJSONObject(index) + DiffToken( + content = token.optString("content"), + color = token.optNullableString("color")?.let { parseColor(it, Color.TRANSPARENT) }, + fontStyle = token.optInt("fontStyle"), + ) + } + } + return result +} + +private fun parseStringSet(value: String): Set = try { + val array = JSONArray(value) + buildSet { + for (index in 0 until array.length()) add(array.getString(index)) + } +} catch (_: Exception) { + emptySet() +} + +private fun parseColor(value: String, fallback: Int): Int = try { + Color.parseColor(value) +} catch (_: Exception) { + fallback +} + +private fun JSONObject.optNullableString(key: String): String? = + if (isNull(key)) null else optString(key).takeIf { it.isNotEmpty() } + +private fun JSONObject.optNullableInt(key: String): Int? = + if (isNull(key) || !has(key)) null else optInt(key) + +private fun JSONObject.floatDp(key: String, fallbackPx: Float, density: Float): Float = + if (has(key)) optDouble(key).toFloat() * density else fallbackPx + +private fun JSONObject.floatSp(key: String, fallbackPx: Float, density: Float): Float = + if (has(key)) optDouble(key).toFloat() * density else fallbackPx diff --git a/apps/mobile/modules/t3-review-diff/expo-module.config.json b/apps/mobile/modules/t3-review-diff/expo-module.config.json index fe6b11b649c..bf5baf4fc5c 100644 --- a/apps/mobile/modules/t3-review-diff/expo-module.config.json +++ b/apps/mobile/modules/t3-review-diff/expo-module.config.json @@ -1,7 +1,10 @@ { - "platforms": ["apple"], + "platforms": ["apple", "android"], "apple": { "modules": ["T3ReviewDiffModule"], "podspecPath": "T3ReviewDiffNative.podspec" + }, + "android": { + "modules": ["expo.modules.t3reviewdiff.T3ReviewDiffModule"] } } diff --git a/apps/mobile/modules/t3-review-diff/package.json b/apps/mobile/modules/t3-review-diff/package.json index 75ac49e5a99..4b3f91dbd86 100644 --- a/apps/mobile/modules/t3-review-diff/package.json +++ b/apps/mobile/modules/t3-review-diff/package.json @@ -7,6 +7,11 @@ "modules": [ "T3ReviewDiffModule" ] + }, + "android": { + "modules": [ + "expo.modules.t3reviewdiff.T3ReviewDiffModule" + ] } } } diff --git a/apps/mobile/modules/t3-terminal/README.md b/apps/mobile/modules/t3-terminal/README.md index 09d927f4733..768e3c0704c 100644 --- a/apps/mobile/modules/t3-terminal/README.md +++ b/apps/mobile/modules/t3-terminal/README.md @@ -18,9 +18,9 @@ uses that callback I/O model: 4. send user input back to JS with the write callback 5. emit Ghostty's measured terminal size through `onResize` -Android currently implements the same view name (`T3TerminalSurface`) and event payloads so the -React Native screen and RPC code stay platform-neutral. The renderer backend can be replaced with a -future Android Ghostty build without changing JS. +Android implements the same view contract with upstream `libghostty-vt` for terminal state, parsing, +reflow, and scrollback. An Android Canvas view renders compact snapshots produced by the JNI bridge, +so the React Native screen and RPC code stay platform-neutral. Vendored Ghostty revision and license details are in `THIRD_PARTY_NOTICES.md`. @@ -36,3 +36,15 @@ apps/mobile/modules/t3-terminal/scripts/build-libghostty-ios16.sh The script builds Ghostty with Zig 0.15.2, strips the iOS archives, and replaces only the `ios-arm64` and `ios-arm64-simulator` slices. Xcode's Metal toolchain must be installed; if `metal` fails, run `xcodebuild -downloadComponent MetalToolchain`. + +## Rebuilding libghostty-vt for Android + +The checked-in Android shared libraries and headers are pinned to the revision recorded in +`Vendor/libghostty-vt/VERSION`. Set `ANDROID_NDK_HOME` and run: + +```bash +apps/mobile/modules/t3-terminal/scripts/build-libghostty-android.sh +``` + +The script downloads Zig 0.15.2 when needed, checks out the pinned upstream Ghostty revision, and +rebuilds all four Android ABIs with 16 KB page-size support. diff --git a/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md b/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md index 0ed6e1d1487..8838a61073b 100644 --- a/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md +++ b/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md @@ -14,3 +14,14 @@ iOS 16 support fork. That fork was created from VVTerm's custom-I/O Ghostty fork Ghostty's MIT license applies to the vendored framework. Keep this notice in sync when updating `Vendor/libghostty`. + +## Ghostty / libghostty-vt + +The Android terminal renderer vendors upstream `libghostty-vt` shared libraries and C headers. + +- Upstream project: https://github.com/ghostty-org/ghostty +- Vendored revision: `9f62873bf195e4d8a762d768a1405a5f2f7b1697` +- License: MIT + +Ghostty's MIT license applies to the vendored Android libraries. Keep this notice in sync when +updating `Vendor/libghostty-vt`. diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/LICENSE b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/LICENSE new file mode 100644 index 00000000000..0a07a66cd1a --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Mitchell Hashimoto, Ghostty contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/VERSION b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/VERSION new file mode 100644 index 00000000000..aa5d5e7421e --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/VERSION @@ -0,0 +1 @@ +9f62873bf195e4d8a762d768a1405a5f2f7b1697 diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt.h new file mode 100644 index 00000000000..94a85033434 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt.h @@ -0,0 +1,154 @@ +/** + * @file vt.h + * + * libghostty-vt - Virtual terminal emulator library + * + * This library provides functionality for parsing and handling terminal + * escape sequences as well as maintaining terminal state such as styles, + * cursor position, screen, scrollback, and more. + * + * WARNING: This is an incomplete, work-in-progress API. It is not yet + * stable and is definitely going to change. + */ + +/** + * @mainpage libghostty-vt - Virtual Terminal Emulator Library + * + * libghostty-vt is a C library which implements a modern terminal emulator, + * extracted from the [Ghostty](https://ghostty.org) terminal emulator. + * + * libghostty-vt contains the logic for handling the core parts of a terminal + * emulator: parsing terminal escape sequences, maintaining terminal state, + * encoding input events, etc. It can handle scrollback, line wrapping, + * reflow on resize, and more. + * + * @warning This library is currently in development and the API is not yet stable. + * Breaking changes are expected in future versions. Use with caution in production code. + * + * @section groups_sec API Reference + * + * The API is organized into the following groups: + * - @ref terminal "Terminal" - Complete terminal emulator state and rendering + * - @ref render "Render State" - Incremental render state updates for custom renderers + * - @ref formatter "Formatter" - Format terminal content as plain text, VT sequences, or HTML + * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences + * - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences + * - @ref paste "Paste Utilities" - Validate paste data safety + * - @ref build_info "Build Info" - Query compile-time build configuration + * - @ref allocator "Memory Management" - Memory management and custom allocators + * - @ref wasm "WebAssembly Utilities" - WebAssembly convenience functions + * + * Encoding related APIs: + * - @ref focus "Focus Encoding" - Encode focus in/out events into terminal sequences + * - @ref key "Key Encoding" - Encode key events into terminal sequences + * - @ref mouse "Mouse Encoding" - Encode mouse events into terminal sequences + * + * @section examples_sec Examples + * + * Complete working examples: + * - @ref c-vt-build-info/src/main.c - Build info query example + * - @ref c-vt/src/main.c - OSC parser example + * - @ref c-vt-encode-key/src/main.c - Key encoding example + * - @ref c-vt-encode-mouse/src/main.c - Mouse encoding example + * - @ref c-vt-paste/src/main.c - Paste safety check example + * - @ref c-vt-sgr/src/main.c - SGR parser example + * - @ref c-vt-formatter/src/main.c - Terminal formatter example + * - @ref c-vt-grid-traverse/src/main.c - Grid traversal example using grid refs + * - @ref c-vt-grid-ref-tracked/src/main.c - Tracked grid ref example + * + */ + +/** @example c-vt-build-info/src/main.c + * This example demonstrates how to query compile-time build configuration + * such as SIMD support, Kitty graphics, and tmux control mode availability. + */ + +/** @example c-vt/src/main.c + * This example demonstrates how to use the OSC parser to parse an OSC sequence, + * extract command information, and retrieve command-specific data like window titles. + */ + +/** @example c-vt-encode-key/src/main.c + * This example demonstrates how to use the key encoder to convert key events + * into terminal escape sequences using the Kitty keyboard protocol. + */ + +/** @example c-vt-encode-mouse/src/main.c + * This example demonstrates how to use the mouse encoder to convert mouse events + * into terminal escape sequences using the SGR mouse format. + */ + +/** @example c-vt-paste/src/main.c + * This example demonstrates how to use the paste utilities to check if + * paste data is safe before sending it to the terminal. + */ + +/** @example c-vt-sgr/src/main.c + * This example demonstrates how to use the SGR parser to parse terminal + * styling sequences and extract text attributes like colors and underline styles. + */ + +/** @example c-vt-formatter/src/main.c + * This example demonstrates how to use the terminal and formatter APIs to + * create a terminal, write VT-encoded content into it, and format the screen + * contents as plain text. + */ + +/** @example c-vt-grid-traverse/src/main.c + * This example demonstrates how to traverse the entire terminal grid using + * grid refs to inspect cell codepoints, row wrap state, and cell styles. + */ + +/** @example c-vt-grid-ref-tracked/src/main.c + * This example demonstrates how to track a grid ref as the terminal scrolls, + * detect when it loses its value, and move it to a new point. + */ + +/** @example c-vt-selection-gesture/src/main.c + * This example demonstrates how to use synthetic selection gesture events to + * derive drag and deep-press selection snapshots. + */ + +/** @example c-vt-kitty-graphics/src/main.c + * This example demonstrates how to use the system interface to install a + * PNG decoder callback and send a Kitty Graphics Protocol image. + */ + +#ifndef GHOSTTY_VT_H +#define GHOSTTY_VT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/allocator.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/allocator.h new file mode 100644 index 00000000000..2e8685e8445 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/allocator.h @@ -0,0 +1,255 @@ +/** + * @file allocator.h + * + * Memory management interface for libghostty-vt. + */ + +#ifndef GHOSTTY_VT_ALLOCATOR_H +#define GHOSTTY_VT_ALLOCATOR_H + +#include +#include +#include +#include + +/** @defgroup allocator Memory Management + * + * libghostty-vt does require memory allocation for various operations, + * but is resilient to allocation failures and will gracefully handle + * out-of-memory situations by returning error codes. + * + * The exact memory management semantics are documented in the relevant + * functions and data structures. + * + * libghostty-vt uses explicit memory allocation via an allocator + * interface provided by GhosttyAllocator. The interface is based on the + * [Zig](https://ziglang.org) allocator interface, since this has been + * shown to be a flexible and powerful interface in practice and enables + * a wide variety of allocation strategies. + * + * **For the common case, you can pass NULL as the allocator for any + * function that accepts one,** and libghostty will use a default allocator. + * The default allocator will be libc malloc/free if libc is linked. + * Otherwise, a custom allocator is used (currently Zig's SMP allocator) + * that doesn't require any external dependencies. + * + * ## Basic Usage + * + * For simple use cases, you can ignore this interface entirely by passing NULL + * as the allocator parameter to functions that accept one. This will use the + * default allocator (typically libc malloc/free, if libc is linked, but + * we provide our own default allocator if libc isn't linked). + * + * To use a custom allocator: + * 1. Implement the GhosttyAllocatorVtable function pointers + * 2. Create a GhosttyAllocator struct with your vtable and context + * 3. Pass the allocator to functions that accept one + * + * ## Alloc/Free Helpers + * + * ghostty_alloc() and ghostty_free() provide a simple malloc/free-style + * interface for allocating and freeing byte buffers through the library's + * allocator. These are useful when: + * + * - You need to allocate a buffer to pass into a libghostty-vt function + * (e.g. preparing input data for ghostty_terminal_vt_write()). + * - You need to free a buffer returned by a libghostty-vt function + * (e.g. the output of ghostty_formatter_format_alloc()). + * - You are on a platform where the library's internal allocator differs + * from the consumer's C runtime (e.g. Windows, where Zig's libc and + * MSVC's CRT maintain separate heaps), so calling the standard C + * free() on library-allocated memory would be undefined behavior. + * + * Always use the same allocator (or NULL) for both the allocation and + * the corresponding free. + * + * @{ + */ + +/** + * Function table for custom memory allocator operations. + * + * This vtable defines the interface for a custom memory allocator. All + * function pointers must be valid and non-NULL. + * + * @ingroup allocator + * + * If you're not going to use a custom allocator, you can ignore all of + * this. All functions that take an allocator pointer allow NULL to use a + * default allocator. + * + * The interface is based on the Zig allocator interface. I'll say up front + * that it is easy to look at this interface and think "wow, this is really + * overcomplicated". The reason for this complexity is well thought out by + * the Zig folks, and it enables a diverse set of allocation strategies + * as shown by the Zig ecosystem. As a consolation, please note that many + * of the arguments are only needed for advanced use cases and can be + * safely ignored in simple implementations. For example, if you look at + * the Zig implementation of the libc allocator in `lib/std/heap.zig` + * (search for CAllocator), you'll see it is very simple. + * + * We chose to align with the Zig allocator interface because: + * + * 1. It is a proven interface that serves a wide variety of use cases + * in the real world via the Zig ecosystem. It's shown to work. + * + * 2. Our core implementation itself is Zig, and this lets us very + * cheaply and easily convert between C and Zig allocators. + * + * NOTE(mitchellh): In the future, we can have default implementations of + * resize/remap and allow those to be null. + */ +typedef struct { + /** + * Return a pointer to `len` bytes with specified `alignment`, or return + * `NULL` indicating the allocation failed. + * + * @param ctx The allocator context + * @param len Number of bytes to allocate + * @param alignment Required alignment for the allocation. Guaranteed to + * be a power of two between 1 and 16 inclusive. + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return Pointer to allocated memory, or NULL if allocation failed + */ + void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr); + + /** + * Attempt to expand or shrink memory in place. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * `new_len` must be greater than zero. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to resize + * @param memory_len Current size of the memory block + * @param alignment Alignment (must match original allocation) + * @param new_len New requested size + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return true if resize was successful in-place, false if relocation would be required + */ + bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); + + /** + * Attempt to expand or shrink memory, allowing relocation. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * A non-`NULL` return value indicates the resize was successful. The + * allocation may have same address, or may have been relocated. In either + * case, the allocation now has size of `new_len`. A `NULL` return value + * indicates that the resize would be equivalent to allocating new memory, + * copying the bytes from the old memory, and then freeing the old memory. + * In such case, it is more efficient for the caller to perform the copy. + * + * `new_len` must be greater than zero. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to remap + * @param memory_len Current size of the memory block + * @param alignment Alignment (must match original allocation) + * @param new_len New requested size + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed + */ + void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); + + /** + * Free and invalidate a region of memory. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to free + * @param memory_len Size of the memory block + * @param alignment Alignment (must match original allocation) + * @param ret_addr First return address of the allocation call stack (0 if not provided) + */ + void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr); +} GhosttyAllocatorVtable; + +/** + * Custom memory allocator. + * + * For functions that take an allocator pointer, a NULL pointer indicates + * that the default allocator should be used. The default allocator will + * be libc malloc/free if we're linking to libc. If libc isn't linked, + * a custom allocator is used (currently Zig's SMP allocator). + * + * @ingroup allocator + * + * Usage example: + * @code + * GhosttyAllocator allocator = { + * .vtable = &my_allocator_vtable, + * .ctx = my_allocator_state + * }; + * @endcode + */ +typedef struct GhosttyAllocator { + /** + * Opaque context pointer passed to all vtable functions. + * This allows the allocator implementation to maintain state + * or reference external resources needed for memory management. + */ + void *ctx; + + /** + * Pointer to the allocator's vtable containing function pointers + * for memory operations (alloc, resize, remap, free). + */ + const GhosttyAllocatorVtable *vtable; +} GhosttyAllocator; + +/** + * Allocate a buffer of `len` bytes. + * + * Uses the provided allocator, or the default allocator if NULL is passed. + * The returned buffer must be freed with ghostty_free() using the same + * allocator. + * + * @param allocator Pointer to the allocator to use, or NULL for the default + * @param len Number of bytes to allocate + * @return Pointer to the allocated buffer, or NULL if allocation failed + * + * @ingroup allocator + */ +GHOSTTY_API uint8_t* ghostty_alloc(const GhosttyAllocator* allocator, size_t len); + +/** + * Free memory that was allocated by a libghostty-vt function. + * + * Use this to free buffers returned by functions such as + * ghostty_formatter_format_alloc(). Pass the same allocator that was + * used for the allocation, or NULL if the default allocator was used. + * + * On platforms where the library's internal allocator differs from the + * consumer's C runtime (e.g. Windows, where Zig's libc and MSVC's CRT + * maintain separate heaps), calling the standard C free() on memory + * allocated by the library causes undefined behavior. This function + * guarantees the correct allocator is used regardless of platform. + * + * It is safe to pass a NULL pointer; the call is a no-op in that case. + * + * @param allocator Pointer to the allocator that was used to allocate the + * memory, or NULL if the default allocator was used + * @param ptr Pointer to the memory to free (may be NULL) + * @param len Length of the allocation in bytes (must match the original + * allocation size) + * + * @ingroup allocator + */ +GHOSTTY_API void ghostty_free(const GhosttyAllocator* allocator, uint8_t* ptr, size_t len); + +/** @} */ + +#endif /* GHOSTTY_VT_ALLOCATOR_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/build_info.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/build_info.h new file mode 100644 index 00000000000..8573556f7f8 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/build_info.h @@ -0,0 +1,150 @@ +/** + * @file build_info.h + * + * Build info - query compile-time build configuration of libghostty-vt. + */ + +#ifndef GHOSTTY_VT_BUILD_INFO_H +#define GHOSTTY_VT_BUILD_INFO_H + +/** @defgroup build_info Build Info + * + * Query compile-time build configuration of libghostty-vt. + * + * These values reflect the options the library was built with and are + * constant for the lifetime of the process. + * + * ## Basic Usage + * + * Use ghostty_build_info() to query individual build options: + * + * @snippet c-vt-build-info/src/main.c build-info-query + * + * @{ + */ + +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Build optimization mode. + */ +typedef enum GHOSTTY_ENUM_TYPED { + GHOSTTY_OPTIMIZE_DEBUG = 0, + GHOSTTY_OPTIMIZE_RELEASE_SAFE = 1, + GHOSTTY_OPTIMIZE_RELEASE_SMALL = 2, + GHOSTTY_OPTIMIZE_RELEASE_FAST = 3, + GHOSTTY_OPTIMIZE_MODE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyOptimizeMode; + +/** + * Build info data types that can be queried. + * + * Each variant documents the expected output pointer type. + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Invalid data type. Never results in any data extraction. */ + GHOSTTY_BUILD_INFO_INVALID = 0, + + /** + * Whether SIMD-accelerated code paths are enabled. + * + * Output type: bool * + */ + GHOSTTY_BUILD_INFO_SIMD = 1, + + /** + * Whether Kitty graphics protocol support is available. + * + * Output type: bool * + */ + GHOSTTY_BUILD_INFO_KITTY_GRAPHICS = 2, + + /** + * Whether tmux control mode support is available. + * + * Output type: bool * + */ + GHOSTTY_BUILD_INFO_TMUX_CONTROL_MODE = 3, + + /** + * The optimization mode the library was built with. + * + * Output type: GhosttyOptimizeMode * + */ + GHOSTTY_BUILD_INFO_OPTIMIZE = 4, + + /** + * The full version string (e.g. "1.2.3" or "1.2.3-dev+abcdef"). + * + * Output type: GhosttyString * + */ + GHOSTTY_BUILD_INFO_VERSION_STRING = 5, + + /** + * The major version number. + * + * Output type: size_t * + */ + GHOSTTY_BUILD_INFO_VERSION_MAJOR = 6, + + /** + * The minor version number. + * + * Output type: size_t * + */ + GHOSTTY_BUILD_INFO_VERSION_MINOR = 7, + + /** + * The patch version number. + * + * Output type: size_t * + */ + GHOSTTY_BUILD_INFO_VERSION_PATCH = 8, + + /** + * The pre metadata string (e.g. "alpha", "beta", "dev"). Has zero length if + * no pre metadata is present. + * + * Output type: GhosttyString * + */ + GHOSTTY_BUILD_INFO_VERSION_PRE = 9, + + /** + * The build metadata string (e.g. commit hash). Has zero length if + * no build metadata is present. + * + * Output type: GhosttyString * + */ + GHOSTTY_BUILD_INFO_VERSION_BUILD = 10, + GHOSTTY_BUILD_INFO_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyBuildInfo; + +/** + * Query a compile-time build configuration value. + * + * The caller must pass a pointer to the correct output type for the + * requested data (see GhosttyBuildInfo variants for types). + * + * @param data The build info field to query + * @param out Pointer to store the result (type depends on data parameter) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the + * data type is invalid + * + * @ingroup build_info + */ +GHOSTTY_API GhosttyResult ghostty_build_info(GhosttyBuildInfo data, void *out); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_BUILD_INFO_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/color.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/color.h new file mode 100644 index 00000000000..9dc21864eb9 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/color.h @@ -0,0 +1,97 @@ +/** + * @file color.h + * + * Color types and utilities. + */ + +#ifndef GHOSTTY_VT_COLOR_H +#define GHOSTTY_VT_COLOR_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * RGB color value. + * + * @ingroup sgr + */ +typedef struct { + uint8_t r; /**< Red component (0-255) */ + uint8_t g; /**< Green component (0-255) */ + uint8_t b; /**< Blue component (0-255) */ +} GhosttyColorRgb; + +/** + * Palette color index (0-255). + * + * @ingroup sgr + */ +typedef uint8_t GhosttyColorPaletteIndex; + +/** @addtogroup sgr + * @{ + */ + +/** Black color (0) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BLACK 0 +/** Red color (1) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_RED 1 +/** Green color (2) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_GREEN 2 +/** Yellow color (3) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_YELLOW 3 +/** Blue color (4) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BLUE 4 +/** Magenta color (5) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_MAGENTA 5 +/** Cyan color (6) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_CYAN 6 +/** White color (7) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_WHITE 7 +/** Bright black color (8) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_BLACK 8 +/** Bright red color (9) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_RED 9 +/** Bright green color (10) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_GREEN 10 +/** Bright yellow color (11) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW 11 +/** Bright blue color (12) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_BLUE 12 +/** Bright magenta color (13) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA 13 +/** Bright cyan color (14) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_CYAN 14 +/** Bright white color (15) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_WHITE 15 + +/** @} */ + +/** + * Get the RGB color components. + * + * This function extracts the individual red, green, and blue components + * from a GhosttyColorRgb value. Primarily useful in WebAssembly environments + * where accessing struct fields directly is difficult. + * + * @param color The RGB color value + * @param r Pointer to store the red component (0-255) + * @param g Pointer to store the green component (0-255) + * @param b Pointer to store the blue component (0-255) + * + * @ingroup sgr + */ +GHOSTTY_API void ghostty_color_rgb_get(GhosttyColorRgb color, + uint8_t* r, + uint8_t* g, + uint8_t* b); + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_COLOR_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/device.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/device.h new file mode 100644 index 00000000000..0a1567280b8 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/device.h @@ -0,0 +1,151 @@ +/** + * @file device.h + * + * Device types used by the terminal for device status and device attribute + * queries. + */ + +#ifndef GHOSTTY_VT_DEVICE_H +#define GHOSTTY_VT_DEVICE_H + +#include +#include + +/* DA1 conformance levels (Pp parameter). */ +#define GHOSTTY_DA_CONFORMANCE_VT100 1 +#define GHOSTTY_DA_CONFORMANCE_VT101 1 +#define GHOSTTY_DA_CONFORMANCE_VT102 6 +#define GHOSTTY_DA_CONFORMANCE_VT125 12 +#define GHOSTTY_DA_CONFORMANCE_VT131 7 +#define GHOSTTY_DA_CONFORMANCE_VT132 4 +#define GHOSTTY_DA_CONFORMANCE_VT220 62 +#define GHOSTTY_DA_CONFORMANCE_VT240 62 +#define GHOSTTY_DA_CONFORMANCE_VT320 63 +#define GHOSTTY_DA_CONFORMANCE_VT340 63 +#define GHOSTTY_DA_CONFORMANCE_VT420 64 +#define GHOSTTY_DA_CONFORMANCE_VT510 65 +#define GHOSTTY_DA_CONFORMANCE_VT520 65 +#define GHOSTTY_DA_CONFORMANCE_VT525 65 +#define GHOSTTY_DA_CONFORMANCE_LEVEL_2 62 +#define GHOSTTY_DA_CONFORMANCE_LEVEL_3 63 +#define GHOSTTY_DA_CONFORMANCE_LEVEL_4 64 +#define GHOSTTY_DA_CONFORMANCE_LEVEL_5 65 + +/* DA1 feature codes (Ps parameters). */ +#define GHOSTTY_DA_FEATURE_COLUMNS_132 1 +#define GHOSTTY_DA_FEATURE_PRINTER 2 +#define GHOSTTY_DA_FEATURE_REGIS 3 +#define GHOSTTY_DA_FEATURE_SIXEL 4 +#define GHOSTTY_DA_FEATURE_SELECTIVE_ERASE 6 +#define GHOSTTY_DA_FEATURE_USER_DEFINED_KEYS 8 +#define GHOSTTY_DA_FEATURE_NATIONAL_REPLACEMENT 9 +#define GHOSTTY_DA_FEATURE_TECHNICAL_CHARACTERS 15 +#define GHOSTTY_DA_FEATURE_LOCATOR 16 +#define GHOSTTY_DA_FEATURE_TERMINAL_STATE 17 +#define GHOSTTY_DA_FEATURE_WINDOWING 18 +#define GHOSTTY_DA_FEATURE_HORIZONTAL_SCROLLING 21 +#define GHOSTTY_DA_FEATURE_ANSI_COLOR 22 +#define GHOSTTY_DA_FEATURE_RECTANGULAR_EDITING 28 +#define GHOSTTY_DA_FEATURE_ANSI_TEXT_LOCATOR 29 +#define GHOSTTY_DA_FEATURE_CLIPBOARD 52 + +/* DA2 device type identifiers (Pp parameter). */ +#define GHOSTTY_DA_DEVICE_TYPE_VT100 0 +#define GHOSTTY_DA_DEVICE_TYPE_VT220 1 +#define GHOSTTY_DA_DEVICE_TYPE_VT240 2 +#define GHOSTTY_DA_DEVICE_TYPE_VT330 18 +#define GHOSTTY_DA_DEVICE_TYPE_VT340 19 +#define GHOSTTY_DA_DEVICE_TYPE_VT320 24 +#define GHOSTTY_DA_DEVICE_TYPE_VT382 32 +#define GHOSTTY_DA_DEVICE_TYPE_VT420 41 +#define GHOSTTY_DA_DEVICE_TYPE_VT510 61 +#define GHOSTTY_DA_DEVICE_TYPE_VT520 64 +#define GHOSTTY_DA_DEVICE_TYPE_VT525 65 + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Color scheme reported in response to a CSI ? 996 n query. + * + * @ingroup terminal + */ +typedef enum GHOSTTY_ENUM_TYPED { + GHOSTTY_COLOR_SCHEME_LIGHT = 0, + GHOSTTY_COLOR_SCHEME_DARK = 1, + GHOSTTY_COLOR_SCHEME_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyColorScheme; + +/** + * Primary device attributes (DA1) response data. + * + * Returned as part of GhosttyDeviceAttributes in response to a CSI c query. + * The conformance_level is the Pp parameter and features contains the Ps + * feature codes. + * + * @ingroup terminal + */ +typedef struct { + /** Conformance level (Pp parameter). E.g. 62 for VT220. */ + uint16_t conformance_level; + + /** DA1 feature codes. Only the first num_features entries are valid. */ + uint16_t features[64]; + + /** Number of valid entries in the features array. */ + size_t num_features; +} GhosttyDeviceAttributesPrimary; + +/** + * Secondary device attributes (DA2) response data. + * + * Returned as part of GhosttyDeviceAttributes in response to a CSI > c query. + * Response format: CSI > Pp ; Pv ; Pc c + * + * @ingroup terminal + */ +typedef struct { + /** Terminal type identifier (Pp). E.g. 1 for VT220. */ + uint16_t device_type; + + /** Firmware/patch version number (Pv). */ + uint16_t firmware_version; + + /** ROM cartridge registration number (Pc). Always 0 for emulators. */ + uint16_t rom_cartridge; +} GhosttyDeviceAttributesSecondary; + +/** + * Tertiary device attributes (DA3) response data. + * + * Returned as part of GhosttyDeviceAttributes in response to a CSI = c query. + * Response format: DCS ! | D...D ST (DECRPTUI). + * + * @ingroup terminal + */ +typedef struct { + /** Unit ID encoded as 8 uppercase hex digits in the response. */ + uint32_t unit_id; +} GhosttyDeviceAttributesTertiary; + +/** + * Device attributes response data for all three DA levels. + * + * Filled by the device_attributes callback in response to CSI c, + * CSI > c, or CSI = c queries. The terminal uses whichever sub-struct + * matches the request type. + * + * @ingroup terminal + */ +typedef struct { + GhosttyDeviceAttributesPrimary primary; + GhosttyDeviceAttributesSecondary secondary; + GhosttyDeviceAttributesTertiary tertiary; +} GhosttyDeviceAttributes; + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_DEVICE_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/focus.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/focus.h new file mode 100644 index 00000000000..b9940f79247 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/focus.h @@ -0,0 +1,76 @@ +/** + * @file focus.h + * + * Focus encoding - encode focus in/out events into terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_FOCUS_H +#define GHOSTTY_VT_FOCUS_H + +/** @defgroup focus Focus Encoding + * + * Utilities for encoding focus gained/lost events into terminal escape + * sequences (CSI I / CSI O) for focus reporting mode (mode 1004). + * + * ## Basic Usage + * + * Use ghostty_focus_encode() to encode a focus event into a caller-provided + * buffer. If the buffer is too small, the function returns + * GHOSTTY_OUT_OF_SPACE and sets the required size in the output parameter. + * + * ## Example + * + * @snippet c-vt-encode-focus/src/main.c focus-encode + * + * @{ + */ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Focus event types for focus reporting mode (mode 1004). + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Terminal window gained focus */ + GHOSTTY_FOCUS_GAINED = 0, + /** Terminal window lost focus */ + GHOSTTY_FOCUS_LOST = 1, + GHOSTTY_FOCUS_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyFocusEvent; + +/** + * Encode a focus event into a terminal escape sequence. + * + * Encodes a focus gained (CSI I) or focus lost (CSI O) report into the + * provided buffer. + * + * If the buffer is too small, the function returns GHOSTTY_OUT_OF_SPACE + * and writes the required buffer size to @p out_written. The caller can + * then retry with a sufficiently sized buffer. + * + * @param event The focus event to encode + * @param buf Output buffer to write the encoded sequence into (may be NULL) + * @param buf_len Size of the output buffer in bytes + * @param[out] out_written On success, the number of bytes written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer + * is too small + */ +GHOSTTY_API GhosttyResult ghostty_focus_encode( + GhosttyFocusEvent event, + char* buf, + size_t buf_len, + size_t* out_written); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_FOCUS_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/formatter.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/formatter.h new file mode 100644 index 00000000000..5cdcd11a3a7 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/formatter.h @@ -0,0 +1,207 @@ +/** + * @file formatter.h + * + * Format terminal content as plain text, VT sequences, or HTML. + */ + +#ifndef GHOSTTY_VT_FORMATTER_H +#define GHOSTTY_VT_FORMATTER_H + +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup formatter Formatter + * + * Format terminal content as plain text, VT sequences, or HTML. + * + * A formatter captures a reference to a terminal and formatting options. + * It can be used repeatedly to produce output that reflects the current + * terminal state at the time of each format call. + * + * The terminal must outlive the formatter. + * + * @{ + */ + +/** + * Extra screen state to include in styled output. + * + * @ingroup formatter + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyFormatterScreenExtra). */ + size_t size; + + /** Emit cursor position using CUP (CSI H). */ + bool cursor; + + /** Emit current SGR style state based on the cursor's active style_id. */ + bool style; + + /** Emit current hyperlink state using OSC 8 sequences. */ + bool hyperlink; + + /** Emit character protection mode using DECSCA. */ + bool protection; + + /** Emit Kitty keyboard protocol state using CSI > u and CSI = sequences. */ + bool kitty_keyboard; + + /** Emit character set designations and invocations. */ + bool charsets; +} GhosttyFormatterScreenExtra; + +/** + * Extra terminal state to include in styled output. + * + * @ingroup formatter + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyFormatterTerminalExtra). */ + size_t size; + + /** Emit the palette using OSC 4 sequences. */ + bool palette; + + /** Emit terminal modes that differ from their defaults using CSI h/l. */ + bool modes; + + /** Emit scrolling region state using DECSTBM and DECSLRM sequences. */ + bool scrolling_region; + + /** Emit tabstop positions by clearing all tabs and setting each one. */ + bool tabstops; + + /** Emit the present working directory using OSC 7. */ + bool pwd; + + /** Emit keyboard modes such as ModifyOtherKeys. */ + bool keyboard; + + /** Screen-level extras. */ + GhosttyFormatterScreenExtra screen; +} GhosttyFormatterTerminalExtra; + +/** + * Options for creating a terminal formatter. + * + * @ingroup formatter + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyFormatterTerminalOptions). */ + size_t size; + + /** Output format to emit. */ + GhosttyFormatterFormat emit; + + /** Whether to unwrap soft-wrapped lines. */ + bool unwrap; + + /** Whether to trim trailing whitespace on non-blank lines. */ + bool trim; + + /** Extra terminal state to include in styled output. */ + GhosttyFormatterTerminalExtra extra; + + /** Optional selection to restrict output to a range. + * If NULL, the entire screen is formatted. */ + const GhosttySelection *selection; +} GhosttyFormatterTerminalOptions; + +/** + * Create a formatter for a terminal's active screen. + * + * The terminal must outlive the formatter. The formatter stores a borrowed + * reference to the terminal and reads its current state on each format call. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param formatter Pointer to store the created formatter handle + * @param terminal The terminal to format (must not be NULL) + * @param options Formatting options + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup formatter + */ +GHOSTTY_API GhosttyResult ghostty_formatter_terminal_new( + const GhosttyAllocator* allocator, + GhosttyFormatter* formatter, + GhosttyTerminal terminal, + GhosttyFormatterTerminalOptions options); + +/** + * Run the formatter and produce output into the caller-provided buffer. + * + * Each call formats the current terminal state. Pass NULL for buf to + * query the required buffer size without writing any output; in that case + * out_written receives the required size and the return value is + * GHOSTTY_OUT_OF_SPACE. + * + * If the buffer is too small, returns GHOSTTY_OUT_OF_SPACE and sets + * out_written to the required size. The caller can then retry with a + * larger buffer. + * + * @param formatter The formatter handle (must not be NULL) + * @param buf Pointer to the output buffer, or NULL to query size + * @param buf_len Length of the output buffer in bytes + * @param out_written Pointer to receive the number of bytes written, + * or the required size on failure + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup formatter + */ +GHOSTTY_API GhosttyResult ghostty_formatter_format_buf(GhosttyFormatter formatter, + uint8_t* buf, + size_t buf_len, + size_t* out_written); + +/** + * Run the formatter and return an allocated buffer with the output. + * + * Each call formats the current terminal state. The buffer is allocated + * using the provided allocator (or the default allocator if NULL). + * The caller is responsible for freeing the returned buffer with + * ghostty_free(), passing the same allocator (or NULL for the default) + * that was used for the allocation. + * + * @param formatter The formatter handle (must not be NULL) + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param out_ptr Pointer to receive the allocated buffer + * @param out_len Pointer to receive the length of the output in bytes + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation + * failure + * + * @ingroup formatter + */ +GHOSTTY_API GhosttyResult ghostty_formatter_format_alloc(GhosttyFormatter formatter, + const GhosttyAllocator* allocator, + uint8_t** out_ptr, + size_t* out_len); + +/** + * Free a formatter instance. + * + * Releases all resources associated with the formatter. After this call, + * the formatter handle becomes invalid. + * + * @param formatter The formatter handle to free (may be NULL) + * + * @ingroup formatter + */ +GHOSTTY_API void ghostty_formatter_free(GhosttyFormatter formatter); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_FORMATTER_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/grid_ref.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/grid_ref.h new file mode 100644 index 00000000000..c43791dc238 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/grid_ref.h @@ -0,0 +1,212 @@ +/** + * @file grid_ref.h + * + * Terminal grid reference type for referencing a resolved position in the + * terminal grid. + */ + +#ifndef GHOSTTY_VT_GRID_REF_H +#define GHOSTTY_VT_GRID_REF_H + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup grid_ref Grid Reference + * + * A grid reference is a reference to a specific cell position in the + * terminal. Obtain a grid reference from `ghostty_terminal_grid_ref` + * for untracked or `ghostty_terminal_grid_ref_track` for tracked. Untracked + * vs tracked is explained next. + * + * Important: The grid reference APIs are not meant to be used as the core of a render + * loop. They are not built to sustain the framerates needed for rendering large + * screens. Use the render state API for that. + * + * ## Untracked vs Tracked References + * + * ### Untracked Reference + * + * An untracked grid reference is a value type that snapshots a specific + * cell. It is only valid until the next update to the terminal instance. + * There is no guarantee that it will remain valid after any operation, + * even if a seemingly unrelated part of the grid is changed. These are meant + * to be read and have their values cached immediately after obtaining it. + * + * An untracked grid reference has a performance cost in its initial lookup, + * but doesn't affect the ongoing performance of the terminal in any way, + * since it is a one-time snapshot. + * + * ### Tracked Reference + * + * A tracked grid reference follows its cell across normal screen operations. + * For example scrolling, scrollback pruning, resize/reflow, and other + * terminal mutations update the tracked reference automatically. + * + * A tracked reference can still lose its original semantic location. This can + * happen when the underlying grid is reset, pruned, or otherwise discarded in a + * way that cannot be mapped to a meaningful new cell. In that state, + * ghostty_tracked_grid_ref_has_value() returns false and + * ghostty_tracked_grid_ref_snapshot() / ghostty_tracked_grid_ref_point() return + * GHOSTTY_NO_VALUE. The handle remains valid, and callers may move it to a new + * point with ghostty_tracked_grid_ref_set(). + * + * To read cell data from a tracked reference, first snapshot it with + * ghostty_tracked_grid_ref_snapshot(). The returned `GhosttyGridRef` is again + * an untracked reference and follows the same short lifetime rules as any other + * untracked grid reference. + * + * A tracked reference belongs to the terminal screen/page-list that was active + * when it was created or last set. Converting it to a point uses that owning + * screen/page-list, even if the terminal has since switched between primary and + * alternate screens. Calling ghostty_tracked_grid_ref_set() resolves the new + * point against the terminal's currently active screen/page-list and may move + * the tracked reference between screens. + * + * Tracked references are owned by the caller and must be freed with + * ghostty_tracked_grid_ref_free(). If the terminal that created a tracked + * reference is freed first, the handle remains valid only for tracked-grid-ref + * APIs: it reports no value and can still be freed. + * + * Each tracked reference adds bookkeeping to terminal mutations. Use them + * sparingly for long-lived anchors such as selections, search state, marks, + * or application-side bookmarks. + * + * ## Lifetime + * + * An untracked reference is a snapshot. It doesn't need to be freed. + * The safety of accessing the value is documented explicitly above: it + * is only safe to access any data until the next terminal mutating + * operation (including free). + * + * A tracked reference is allocated and must be freed when it is no + * longer needed. A tracked reference may outlive the terminal that created it; + * after terminal free, it reports no value and can still be freed. + * + * ## Examples + * + * @snippet c-vt-grid-traverse/src/main.c grid-ref-traverse + * @snippet c-vt-grid-ref-tracked/src/main.c grid-ref-tracked + * + * @{ + */ + +/** + * A resolved reference to a terminal cell position. + * + * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it. + * + * @ingroup grid_ref + */ +typedef struct { + size_t size; + void *node; + uint16_t x; + uint16_t y; +} GhosttyGridRef; + +/** + * Get the cell from a grid reference. + * + * @param ref Pointer to the grid reference + * @param[out] out_cell On success, set to the cell at the ref's position (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's + * node is NULL + * + * @ingroup grid_ref + */ +GHOSTTY_API GhosttyResult ghostty_grid_ref_cell(const GhosttyGridRef *ref, + GhosttyCell *out_cell); + +/** + * Get the row from a grid reference. + * + * @param ref Pointer to the grid reference + * @param[out] out_row On success, set to the row at the ref's position (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's + * node is NULL + * + * @ingroup grid_ref + */ +GHOSTTY_API GhosttyResult ghostty_grid_ref_row(const GhosttyGridRef *ref, + GhosttyRow *out_row); + +/** + * Get the grapheme cluster codepoints for the cell at the grid reference's + * position. + * + * Writes the full grapheme cluster (the cell's primary codepoint followed by + * any combining codepoints) into the provided buffer. If the cell has no text, + * out_len is set to 0 and GHOSTTY_SUCCESS is returned. + * + * If the buffer is too small (or NULL), the function returns + * GHOSTTY_OUT_OF_SPACE and writes the required number of codepoints to + * out_len. The caller can then retry with a sufficiently sized buffer. + * + * @param ref Pointer to the grid reference + * @param buf Output buffer of uint32_t codepoints (may be NULL) + * @param buf_len Number of uint32_t elements in the buffer + * @param[out] out_len On success, the number of codepoints written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size in codepoints. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's + * node is NULL, GHOSTTY_OUT_OF_SPACE if the buffer is too small + * + * @ingroup grid_ref + */ +GHOSTTY_API GhosttyResult ghostty_grid_ref_graphemes(const GhosttyGridRef *ref, + uint32_t *buf, + size_t buf_len, + size_t *out_len); + +/** + * Get the hyperlink URI for the cell at the grid reference's position. + * + * Writes the URI bytes into the provided buffer. If the cell has no + * hyperlink, out_len is set to 0 and GHOSTTY_SUCCESS is returned. + * + * If the buffer is too small (or NULL), the function returns + * GHOSTTY_OUT_OF_SPACE and writes the required number of bytes to + * out_len. The caller can then retry with a sufficiently sized buffer. + * + * @param ref Pointer to the grid reference + * @param buf Output buffer for the URI bytes (may be NULL) + * @param buf_len Size of the output buffer in bytes + * @param[out] out_len On success, the number of bytes written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size in bytes. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's + * node is NULL, GHOSTTY_OUT_OF_SPACE if the buffer is too small + * + * @ingroup grid_ref + */ +GHOSTTY_API GhosttyResult ghostty_grid_ref_hyperlink_uri( + const GhosttyGridRef *ref, + uint8_t *buf, + size_t buf_len, + size_t *out_len); + +/** + * Get the style of the cell at the grid reference's position. + * + * @param ref Pointer to the grid reference + * @param[out] out_style On success, set to the cell's style (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's + * node is NULL + * + * @ingroup grid_ref + */ +GHOSTTY_API GhosttyResult ghostty_grid_ref_style(const GhosttyGridRef *ref, + GhosttyStyle *out_style); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_GRID_REF_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/grid_ref_tracked.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/grid_ref_tracked.h new file mode 100644 index 00000000000..b56aefacdb6 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/grid_ref_tracked.h @@ -0,0 +1,139 @@ +/** + * @file grid_ref_tracked.h + * + * Tracked terminal grid references. + */ + +#ifndef GHOSTTY_VT_GRID_REF_TRACKED_H +#define GHOSTTY_VT_GRID_REF_TRACKED_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Tracked grid references are owned grid references that move with the + * terminal. See @ref grid_ref for the full overview of tracked and untracked + * grid reference behavior. + * + * @ingroup grid_ref + */ + +/** + * Free a tracked grid reference. + * + * Passing NULL is allowed and has no effect. A tracked reference may be freed + * after the terminal that created it is freed. + * + * @param ref Tracked grid reference to free. + * + * @ingroup grid_ref + */ +GHOSTTY_API void ghostty_tracked_grid_ref_free(GhosttyTrackedGridRef ref); + +/** + * Return whether a tracked grid reference currently has a meaningful value. + * + * If the terminal that created the tracked reference has been freed, this + * returns false. + * + * @param ref Tracked grid reference. + * @return true if the reference currently has a meaningful value. + * + * @ingroup grid_ref + */ +GHOSTTY_API bool ghostty_tracked_grid_ref_has_value( + GhosttyTrackedGridRef ref); + +/** + * Convert a tracked grid reference to a point in the requested coordinate + * space. + * + * This is the tracked equivalent of ghostty_terminal_point_from_grid_ref(). + * Unlike snapshotting, this does not expose an intermediate untracked + * GhosttyGridRef. + * + * A tracked reference is resolved against the terminal screen/page-list that + * currently owns the reference. If the terminal has switched between primary + * and alternate screens since the reference was created or last set, this may + * be different from the terminal's currently active screen. + * + * If the tracked reference no longer has a meaningful value, this returns + * GHOSTTY_NO_VALUE. GHOSTTY_NO_VALUE is also returned when the reference cannot + * be represented in the requested coordinate space, including after the + * terminal that created the tracked reference has been freed. + * + * @param ref Tracked grid reference. + * @param tag Coordinate space to convert into. + * @param[out] out_point On success, receives the coordinate. May be NULL. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if ref is invalid, + * or GHOSTTY_NO_VALUE if there is no representable value. + * + * @ingroup grid_ref + */ +GHOSTTY_API GhosttyResult ghostty_tracked_grid_ref_point( + GhosttyTrackedGridRef ref, + GhosttyPointTag tag, + GhosttyPointCoordinate *out_point); + +/** + * Move an existing tracked grid reference to a new terminal point. + * + * On success, the tracked reference begins tracking the new point and any prior + * "no value" state is cleared. On GHOSTTY_OUT_OF_MEMORY, the original tracked + * reference is left unchanged. + * + * The terminal must be the same terminal that created the tracked reference. + * The point is resolved against the terminal screen/page-list that is active at + * the time this function is called. If the terminal has switched between + * primary and alternate screens, this may move the tracked reference from one + * screen/page-list to the other. + * + * @param ref Tracked grid reference. + * @param terminal Terminal instance that owns the reference. + * @param point New point to track. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if ref, terminal, + * or point is invalid, or GHOSTTY_OUT_OF_MEMORY if allocation fails. + * + * @ingroup grid_ref + */ +GHOSTTY_API GhosttyResult ghostty_tracked_grid_ref_set( + GhosttyTrackedGridRef ref, + GhosttyTerminal terminal, + GhosttyPoint point); + +/** + * Snapshot a tracked grid reference into a regular GhosttyGridRef. + * + * The returned GhosttyGridRef is an untracked snapshot and has the same + * lifetime rules as ghostty_terminal_grid_ref(): it is only valid until the + * next terminal update. Snapshot immediately before calling + * ghostty_grid_ref_cell(), ghostty_grid_ref_row(), + * ghostty_grid_ref_graphemes(), ghostty_grid_ref_hyperlink_uri(), or + * ghostty_grid_ref_style(). + * + * If the tracked reference no longer has a meaningful value, this returns + * GHOSTTY_NO_VALUE. This includes references whose owning terminal has been + * freed. + * + * @param ref Tracked grid reference. + * @param[out] out_ref On success, receives an untracked snapshot. May be NULL. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if ref is invalid, + * or GHOSTTY_NO_VALUE if the tracked location was discarded. + * + * @ingroup grid_ref + */ +GHOSTTY_API GhosttyResult ghostty_tracked_grid_ref_snapshot( + GhosttyTrackedGridRef ref, + GhosttyGridRef *out_ref); + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_GRID_REF_TRACKED_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key.h new file mode 100644 index 00000000000..61b95475357 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key.h @@ -0,0 +1,73 @@ +/** + * @file key.h + * + * Key encoding module - encode key events into terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_KEY_H +#define GHOSTTY_VT_KEY_H + +/** @defgroup key Key Encoding + * + * Utilities for encoding key events into terminal escape sequences, + * supporting both legacy encoding as well as Kitty Keyboard Protocol. + * + * ## Basic Usage + * + * 1. Create an encoder instance with ghostty_key_encoder_new() + * 2. Configure encoder options with ghostty_key_encoder_setopt() + * or ghostty_key_encoder_setopt_from_terminal() if you have a + * GhosttyTerminal. + * 3. For each key event: + * - Create a key event with ghostty_key_event_new() + * - Set event properties (action, key, modifiers, etc.) + * - Encode with ghostty_key_encoder_encode() + * - Free the event with ghostty_key_event_free() + * - Note: You can also reuse the same key event multiple times by + * changing its properties. + * 4. Free the encoder with ghostty_key_encoder_free() when done + * + * For a complete working example, see example/c-vt-encode-key in the + * repository. + * + * ## Example + * + * @snippet c-vt-encode-key/src/main.c key-encode + * + * ## Example: Encoding with Terminal State + * + * When you have a GhosttyTerminal, you can sync its modes (cursor key + * application, Kitty flags, etc.) into the encoder automatically: + * + * @code{.c} + * // Create a terminal and feed it some VT data that changes modes + * GhosttyTerminal terminal; + * ghostty_terminal_new(NULL, &terminal, + * (GhosttyTerminalOptions){.cols = 80, .rows = 24, .max_scrollback = 0}); + * + * // Application might write data that enables Kitty keyboard protocol, etc. + * ghostty_terminal_vt_write(terminal, vt_data, vt_len); + * + * // Create an encoder and sync its options from the terminal + * GhosttyKeyEncoder encoder; + * ghostty_key_encoder_new(NULL, &encoder); + * ghostty_key_encoder_setopt_from_terminal(encoder, terminal); + * + * // Encode a key event using the terminal-derived options + * char buf[128]; + * size_t written = 0; + * ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * + * ghostty_key_encoder_free(encoder); + * ghostty_terminal_free(terminal); + * @endcode + * + * @{ + */ + +#include +#include + +/** @} */ + +#endif /* GHOSTTY_VT_KEY_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key/encoder.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key/encoder.h new file mode 100644 index 00000000000..3aeec6597b1 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key/encoder.h @@ -0,0 +1,255 @@ +/** + * @file encoder.h + * + * Key event encoding to terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_KEY_ENCODER_H +#define GHOSTTY_VT_KEY_ENCODER_H + +#include +#include +#include +#include +#include +#include + +/** + * Opaque handle to a key encoder instance. + * + * This handle represents a key encoder that converts key events into terminal + * escape sequences. + * + * @ingroup key + */ +typedef struct GhosttyKeyEncoderImpl *GhosttyKeyEncoder; + +/** + * Kitty keyboard protocol flags. + * + * Bitflags representing the various modes of the Kitty keyboard protocol. + * These can be combined using bitwise OR operations. Valid values all + * start with `GHOSTTY_KITTY_KEY_`. + * + * @ingroup key + */ +typedef uint8_t GhosttyKittyKeyFlags; + +/** Kitty keyboard protocol disabled (all flags off) */ +#define GHOSTTY_KITTY_KEY_DISABLED 0 + +/** Disambiguate escape codes */ +#define GHOSTTY_KITTY_KEY_DISAMBIGUATE (1 << 0) + +/** Report key press and release events */ +#define GHOSTTY_KITTY_KEY_REPORT_EVENTS (1 << 1) + +/** Report alternate key codes */ +#define GHOSTTY_KITTY_KEY_REPORT_ALTERNATES (1 << 2) + +/** Report all key events including those normally handled by the terminal */ +#define GHOSTTY_KITTY_KEY_REPORT_ALL (1 << 3) + +/** Report associated text with key events */ +#define GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED (1 << 4) + +/** All Kitty keyboard protocol flags enabled */ +#define GHOSTTY_KITTY_KEY_ALL (GHOSTTY_KITTY_KEY_DISAMBIGUATE | GHOSTTY_KITTY_KEY_REPORT_EVENTS | GHOSTTY_KITTY_KEY_REPORT_ALTERNATES | GHOSTTY_KITTY_KEY_REPORT_ALL | GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED) + +/** + * macOS option key behavior. + * + * Determines whether the "option" key on macOS is treated as "alt" or not. + * See the Ghostty `macos-option-as-alt` configuration option for more details. + * + * @ingroup key + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Option key is not treated as alt */ + GHOSTTY_OPTION_AS_ALT_FALSE = 0, + /** Option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_TRUE = 1, + /** Only left option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_LEFT = 2, + /** Only right option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_RIGHT = 3, + GHOSTTY_OPTION_AS_ALT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyOptionAsAlt; + +/** + * Key encoder option identifiers. + * + * These values are used with ghostty_key_encoder_setopt() to configure + * the behavior of the key encoder. + * + * @ingroup key + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Terminal DEC mode 1: cursor key application mode (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0, + + /** Terminal DEC mode 66: keypad key application mode (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION = 1, + + /** Terminal DEC mode 1035: ignore keypad with numlock (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK = 2, + + /** Terminal DEC mode 1036: alt sends escape prefix (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX = 3, + + /** xterm modifyOtherKeys mode 2 (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2 = 4, + + /** Kitty keyboard protocol flags (value: GhosttyKittyKeyFlags bitmask) */ + GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS = 5, + + /** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */ + GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6, + + /** Backarrow key mode (value: bool) + * See https://vt100.net/dec/ek-vt3xx-tp-002.pdf page 170 + * If `false` (the default), `backspace` emits 0x7f + * If `true`, `backspace` emits 0x08 + */ + GHOSTTY_KEY_ENCODER_OPT_BACKARROW_KEY_MODE = 7, + + GHOSTTY_KEY_ENCODER_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyKeyEncoderOption; + +/** + * Create a new key encoder instance. + * + * Creates a new key encoder with default options. The encoder can be configured + * using ghostty_key_encoder_setopt() and must be freed using + * ghostty_key_encoder_free() when no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param encoder Pointer to store the created encoder handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup key + */ +GHOSTTY_API GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder); + +/** + * Free a key encoder instance. + * + * Releases all resources associated with the key encoder. After this call, + * the encoder handle becomes invalid and must not be used. + * + * @param encoder The encoder handle to free (may be NULL) + * + * @ingroup key + */ +GHOSTTY_API void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); + +/** + * Set an option on the key encoder. + * + * Configures the behavior of the key encoder. Options control various aspects + * of encoding such as terminal modes (cursor key application mode, keypad mode), + * protocol selection (Kitty keyboard protocol flags), and platform-specific + * behaviors (macOS option-as-alt). + * + * If you are using a terminal instance, you can set the key encoding + * options based on the active terminal state (e.g. legacy vs Kitty mode + * and associated flags) with ghostty_key_encoder_setopt_from_terminal(). + * + * A null pointer value does nothing. It does not reset the value to the + * default. The setopt call will do nothing. + * + * @param encoder The encoder handle, must not be NULL + * @param option The option to set + * @param value Pointer to the value to set (type depends on the option) + * + * @ingroup key + */ +GHOSTTY_API void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value); + +/** + * Set encoder options from a terminal's current state. + * + * Reads the terminal's current modes and flags and applies them to the + * encoder's options. This sets cursor key application mode, keypad mode, + * alt escape prefix, modifyOtherKeys state, and Kitty keyboard protocol + * flags from the terminal state. + * + * Note that the `macos_option_as_alt` option cannot be determined from + * terminal state and is reset to `GHOSTTY_OPTION_AS_ALT_FALSE` by this + * call. Use ghostty_key_encoder_setopt() to set it afterward if needed. + * + * @param encoder The encoder handle, must not be NULL + * @param terminal The terminal handle, must not be NULL + * + * @ingroup key + */ +GHOSTTY_API void ghostty_key_encoder_setopt_from_terminal(GhosttyKeyEncoder encoder, GhosttyTerminal terminal); + +/** + * Encode a key event into a terminal escape sequence. + * + * Converts a key event into the appropriate terminal escape sequence based on + * the encoder's current options. The sequence is written to the provided buffer. + * + * Not all key events produce output. For example, unmodified modifier keys + * typically don't generate escape sequences. Check the out_len parameter to + * determine if any data was written. + * + * If the output buffer is too small, this function returns GHOSTTY_OUT_OF_SPACE + * and out_len will contain the required buffer size. The caller can then + * allocate a larger buffer and call the function again. + * + * @param encoder The encoder handle, must not be NULL + * @param event The key event to encode, must not be NULL + * @param out_buf Buffer to write the encoded sequence to + * @param out_buf_size Size of the output buffer in bytes + * @param out_len Pointer to store the number of bytes written (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if buffer too small, or other error code + * + * ## Example: Calculate required buffer size + * + * @code{.c} + * // Query the required size with a NULL buffer (always returns OUT_OF_SPACE) + * size_t required = 0; + * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required); + * assert(result == GHOSTTY_OUT_OF_SPACE); + * + * // Allocate buffer of required size + * char *buf = malloc(required); + * + * // Encode with properly sized buffer + * size_t written = 0; + * result = ghostty_key_encoder_encode(encoder, event, buf, required, &written); + * assert(result == GHOSTTY_SUCCESS); + * + * // Use the encoded sequence... + * + * free(buf); + * @endcode + * + * ## Example: Direct encoding with static buffer + * + * @code{.c} + * // Most escape sequences are short, so a static buffer often suffices + * char buf[128]; + * size_t written = 0; + * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * + * if (result == GHOSTTY_SUCCESS) { + * // Write the encoded sequence to the terminal + * write(pty_fd, buf, written); + * } else if (result == GHOSTTY_OUT_OF_SPACE) { + * // Buffer too small, written contains required size + * char *dynamic_buf = malloc(written); + * result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written); + * assert(result == GHOSTTY_SUCCESS); + * write(pty_fd, dynamic_buf, written); + * free(dynamic_buf); + * } + * @endcode + * + * @ingroup key + */ +GHOSTTY_API GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len); + +#endif /* GHOSTTY_VT_KEY_ENCODER_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key/event.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key/event.h new file mode 100644 index 00000000000..eba433c6a55 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key/event.h @@ -0,0 +1,482 @@ +/** + * @file event.h + * + * Key event representation and manipulation. + */ + +#ifndef GHOSTTY_VT_KEY_EVENT_H +#define GHOSTTY_VT_KEY_EVENT_H + +#include +#include +#include +#include +#include + +/** + * Opaque handle to a key event. + * + * This handle represents a keyboard input event containing information about + * the physical key pressed, modifiers, and generated text. + * + * @ingroup key + */ +typedef struct GhosttyKeyEventImpl *GhosttyKeyEvent; + +/** + * Keyboard input event types. + * + * @ingroup key + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Key was released */ + GHOSTTY_KEY_ACTION_RELEASE = 0, + /** Key was pressed */ + GHOSTTY_KEY_ACTION_PRESS = 1, + /** Key is being repeated (held down) */ + GHOSTTY_KEY_ACTION_REPEAT = 2, + GHOSTTY_KEY_ACTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyKeyAction; + +/** + * Keyboard modifier keys bitmask. + * + * A bitmask representing all keyboard modifiers. This tracks which modifier keys + * are pressed and, where supported by the platform, which side (left or right) + * of each modifier is active. + * + * Use the GHOSTTY_MODS_* constants to test and set individual modifiers. + * + * Modifier side bits are only meaningful when the corresponding modifier bit is set. + * Not all platforms support distinguishing between left and right modifier + * keys and Ghostty is built to expect that some platforms may not provide this + * information. + * + * @ingroup key + */ +typedef uint16_t GhosttyMods; + +/** Shift key is pressed */ +#define GHOSTTY_MODS_SHIFT (1 << 0) +/** Control key is pressed */ +#define GHOSTTY_MODS_CTRL (1 << 1) +/** Alt/Option key is pressed */ +#define GHOSTTY_MODS_ALT (1 << 2) +/** Super/Command/Windows key is pressed */ +#define GHOSTTY_MODS_SUPER (1 << 3) +/** Caps Lock is active */ +#define GHOSTTY_MODS_CAPS_LOCK (1 << 4) +/** Num Lock is active */ +#define GHOSTTY_MODS_NUM_LOCK (1 << 5) + +/** + * Right shift is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_SHIFT is set. + */ +#define GHOSTTY_MODS_SHIFT_SIDE (1 << 6) +/** + * Right ctrl is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_CTRL is set. + */ +#define GHOSTTY_MODS_CTRL_SIDE (1 << 7) +/** + * Right alt is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_ALT is set. + */ +#define GHOSTTY_MODS_ALT_SIDE (1 << 8) +/** + * Right super is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_SUPER is set. + */ +#define GHOSTTY_MODS_SUPER_SIDE (1 << 9) + +/** + * Physical key codes. + * + * The set of key codes that Ghostty is aware of. These represent physical keys + * on the keyboard and are layout-independent. For example, the "a" key on a US + * keyboard is the same as the "ф" key on a Russian keyboard, but both will + * report the same key_a value. + * + * Layout-dependent strings are provided separately as UTF-8 text and are produced + * by the platform. These values are based on the W3C UI Events KeyboardEvent code + * standard. See: https://www.w3.org/TR/uievents-code + * + * @ingroup key + */ +typedef enum GHOSTTY_ENUM_TYPED { + GHOSTTY_KEY_UNIDENTIFIED = 0, + + // Writing System Keys (W3C § 3.1.1) + GHOSTTY_KEY_BACKQUOTE, + GHOSTTY_KEY_BACKSLASH, + GHOSTTY_KEY_BRACKET_LEFT, + GHOSTTY_KEY_BRACKET_RIGHT, + GHOSTTY_KEY_COMMA, + GHOSTTY_KEY_DIGIT_0, + GHOSTTY_KEY_DIGIT_1, + GHOSTTY_KEY_DIGIT_2, + GHOSTTY_KEY_DIGIT_3, + GHOSTTY_KEY_DIGIT_4, + GHOSTTY_KEY_DIGIT_5, + GHOSTTY_KEY_DIGIT_6, + GHOSTTY_KEY_DIGIT_7, + GHOSTTY_KEY_DIGIT_8, + GHOSTTY_KEY_DIGIT_9, + GHOSTTY_KEY_EQUAL, + GHOSTTY_KEY_INTL_BACKSLASH, + GHOSTTY_KEY_INTL_RO, + GHOSTTY_KEY_INTL_YEN, + GHOSTTY_KEY_A, + GHOSTTY_KEY_B, + GHOSTTY_KEY_C, + GHOSTTY_KEY_D, + GHOSTTY_KEY_E, + GHOSTTY_KEY_F, + GHOSTTY_KEY_G, + GHOSTTY_KEY_H, + GHOSTTY_KEY_I, + GHOSTTY_KEY_J, + GHOSTTY_KEY_K, + GHOSTTY_KEY_L, + GHOSTTY_KEY_M, + GHOSTTY_KEY_N, + GHOSTTY_KEY_O, + GHOSTTY_KEY_P, + GHOSTTY_KEY_Q, + GHOSTTY_KEY_R, + GHOSTTY_KEY_S, + GHOSTTY_KEY_T, + GHOSTTY_KEY_U, + GHOSTTY_KEY_V, + GHOSTTY_KEY_W, + GHOSTTY_KEY_X, + GHOSTTY_KEY_Y, + GHOSTTY_KEY_Z, + GHOSTTY_KEY_MINUS, + GHOSTTY_KEY_PERIOD, + GHOSTTY_KEY_QUOTE, + GHOSTTY_KEY_SEMICOLON, + GHOSTTY_KEY_SLASH, + + // Functional Keys (W3C § 3.1.2) + GHOSTTY_KEY_ALT_LEFT, + GHOSTTY_KEY_ALT_RIGHT, + GHOSTTY_KEY_BACKSPACE, + GHOSTTY_KEY_CAPS_LOCK, + GHOSTTY_KEY_CONTEXT_MENU, + GHOSTTY_KEY_CONTROL_LEFT, + GHOSTTY_KEY_CONTROL_RIGHT, + GHOSTTY_KEY_ENTER, + GHOSTTY_KEY_META_LEFT, + GHOSTTY_KEY_META_RIGHT, + GHOSTTY_KEY_SHIFT_LEFT, + GHOSTTY_KEY_SHIFT_RIGHT, + GHOSTTY_KEY_SPACE, + GHOSTTY_KEY_TAB, + GHOSTTY_KEY_CONVERT, + GHOSTTY_KEY_KANA_MODE, + GHOSTTY_KEY_NON_CONVERT, + + // Control Pad Section (W3C § 3.2) + GHOSTTY_KEY_DELETE, + GHOSTTY_KEY_END, + GHOSTTY_KEY_HELP, + GHOSTTY_KEY_HOME, + GHOSTTY_KEY_INSERT, + GHOSTTY_KEY_PAGE_DOWN, + GHOSTTY_KEY_PAGE_UP, + + // Arrow Pad Section (W3C § 3.3) + GHOSTTY_KEY_ARROW_DOWN, + GHOSTTY_KEY_ARROW_LEFT, + GHOSTTY_KEY_ARROW_RIGHT, + GHOSTTY_KEY_ARROW_UP, + + // Numpad Section (W3C § 3.4) + GHOSTTY_KEY_NUM_LOCK, + GHOSTTY_KEY_NUMPAD_0, + GHOSTTY_KEY_NUMPAD_1, + GHOSTTY_KEY_NUMPAD_2, + GHOSTTY_KEY_NUMPAD_3, + GHOSTTY_KEY_NUMPAD_4, + GHOSTTY_KEY_NUMPAD_5, + GHOSTTY_KEY_NUMPAD_6, + GHOSTTY_KEY_NUMPAD_7, + GHOSTTY_KEY_NUMPAD_8, + GHOSTTY_KEY_NUMPAD_9, + GHOSTTY_KEY_NUMPAD_ADD, + GHOSTTY_KEY_NUMPAD_BACKSPACE, + GHOSTTY_KEY_NUMPAD_CLEAR, + GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY, + GHOSTTY_KEY_NUMPAD_COMMA, + GHOSTTY_KEY_NUMPAD_DECIMAL, + GHOSTTY_KEY_NUMPAD_DIVIDE, + GHOSTTY_KEY_NUMPAD_ENTER, + GHOSTTY_KEY_NUMPAD_EQUAL, + GHOSTTY_KEY_NUMPAD_MEMORY_ADD, + GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR, + GHOSTTY_KEY_NUMPAD_MEMORY_RECALL, + GHOSTTY_KEY_NUMPAD_MEMORY_STORE, + GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT, + GHOSTTY_KEY_NUMPAD_MULTIPLY, + GHOSTTY_KEY_NUMPAD_PAREN_LEFT, + GHOSTTY_KEY_NUMPAD_PAREN_RIGHT, + GHOSTTY_KEY_NUMPAD_SUBTRACT, + GHOSTTY_KEY_NUMPAD_SEPARATOR, + GHOSTTY_KEY_NUMPAD_UP, + GHOSTTY_KEY_NUMPAD_DOWN, + GHOSTTY_KEY_NUMPAD_RIGHT, + GHOSTTY_KEY_NUMPAD_LEFT, + GHOSTTY_KEY_NUMPAD_BEGIN, + GHOSTTY_KEY_NUMPAD_HOME, + GHOSTTY_KEY_NUMPAD_END, + GHOSTTY_KEY_NUMPAD_INSERT, + GHOSTTY_KEY_NUMPAD_DELETE, + GHOSTTY_KEY_NUMPAD_PAGE_UP, + GHOSTTY_KEY_NUMPAD_PAGE_DOWN, + + // Function Section (W3C § 3.5) + GHOSTTY_KEY_ESCAPE, + GHOSTTY_KEY_F1, + GHOSTTY_KEY_F2, + GHOSTTY_KEY_F3, + GHOSTTY_KEY_F4, + GHOSTTY_KEY_F5, + GHOSTTY_KEY_F6, + GHOSTTY_KEY_F7, + GHOSTTY_KEY_F8, + GHOSTTY_KEY_F9, + GHOSTTY_KEY_F10, + GHOSTTY_KEY_F11, + GHOSTTY_KEY_F12, + GHOSTTY_KEY_F13, + GHOSTTY_KEY_F14, + GHOSTTY_KEY_F15, + GHOSTTY_KEY_F16, + GHOSTTY_KEY_F17, + GHOSTTY_KEY_F18, + GHOSTTY_KEY_F19, + GHOSTTY_KEY_F20, + GHOSTTY_KEY_F21, + GHOSTTY_KEY_F22, + GHOSTTY_KEY_F23, + GHOSTTY_KEY_F24, + GHOSTTY_KEY_F25, + GHOSTTY_KEY_FN, + GHOSTTY_KEY_FN_LOCK, + GHOSTTY_KEY_PRINT_SCREEN, + GHOSTTY_KEY_SCROLL_LOCK, + GHOSTTY_KEY_PAUSE, + + // Media Keys (W3C § 3.6) + GHOSTTY_KEY_BROWSER_BACK, + GHOSTTY_KEY_BROWSER_FAVORITES, + GHOSTTY_KEY_BROWSER_FORWARD, + GHOSTTY_KEY_BROWSER_HOME, + GHOSTTY_KEY_BROWSER_REFRESH, + GHOSTTY_KEY_BROWSER_SEARCH, + GHOSTTY_KEY_BROWSER_STOP, + GHOSTTY_KEY_EJECT, + GHOSTTY_KEY_LAUNCH_APP_1, + GHOSTTY_KEY_LAUNCH_APP_2, + GHOSTTY_KEY_LAUNCH_MAIL, + GHOSTTY_KEY_MEDIA_PLAY_PAUSE, + GHOSTTY_KEY_MEDIA_SELECT, + GHOSTTY_KEY_MEDIA_STOP, + GHOSTTY_KEY_MEDIA_TRACK_NEXT, + GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS, + GHOSTTY_KEY_POWER, + GHOSTTY_KEY_SLEEP, + GHOSTTY_KEY_AUDIO_VOLUME_DOWN, + GHOSTTY_KEY_AUDIO_VOLUME_MUTE, + GHOSTTY_KEY_AUDIO_VOLUME_UP, + GHOSTTY_KEY_WAKE_UP, + + // Legacy, Non-standard, and Special Keys (W3C § 3.7) + GHOSTTY_KEY_COPY, + GHOSTTY_KEY_CUT, + GHOSTTY_KEY_PASTE, + GHOSTTY_KEY_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyKey; + +/** + * Create a new key event instance. + * + * Creates a new key event with default values. The event must be freed using + * ghostty_key_event_free() when no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param event Pointer to store the created key event handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup key + */ +GHOSTTY_API GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event); + +/** + * Free a key event instance. + * + * Releases all resources associated with the key event. After this call, + * the event handle becomes invalid and must not be used. + * + * @param event The key event handle to free (may be NULL) + * + * @ingroup key + */ +GHOSTTY_API void ghostty_key_event_free(GhosttyKeyEvent event); + +/** + * Set the key action (press, release, repeat). + * + * @param event The key event handle, must not be NULL + * @param action The action to set + * + * @ingroup key + */ +GHOSTTY_API void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action); + +/** + * Get the key action (press, release, repeat). + * + * @param event The key event handle, must not be NULL + * @return The key action + * + * @ingroup key + */ +GHOSTTY_API GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event); + +/** + * Set the physical key code. + * + * @param event The key event handle, must not be NULL + * @param key The physical key code to set + * + * @ingroup key + */ +GHOSTTY_API void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key); + +/** + * Get the physical key code. + * + * @param event The key event handle, must not be NULL + * @return The physical key code + * + * @ingroup key + */ +GHOSTTY_API GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event); + +/** + * Set the modifier keys bitmask. + * + * @param event The key event handle, must not be NULL + * @param mods The modifier keys bitmask to set + * + * @ingroup key + */ +GHOSTTY_API void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods); + +/** + * Get the modifier keys bitmask. + * + * @param event The key event handle, must not be NULL + * @return The modifier keys bitmask + * + * @ingroup key + */ +GHOSTTY_API GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event); + +/** + * Set the consumed modifiers bitmask. + * + * @param event The key event handle, must not be NULL + * @param consumed_mods The consumed modifiers bitmask to set + * + * @ingroup key + */ +GHOSTTY_API void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods); + +/** + * Get the consumed modifiers bitmask. + * + * @param event The key event handle, must not be NULL + * @return The consumed modifiers bitmask + * + * @ingroup key + */ +GHOSTTY_API GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event); + +/** + * Set whether the key event is part of a composition sequence. + * + * @param event The key event handle, must not be NULL + * @param composing Whether the key event is part of a composition sequence + * + * @ingroup key + */ +GHOSTTY_API void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing); + +/** + * Get whether the key event is part of a composition sequence. + * + * @param event The key event handle, must not be NULL + * @return Whether the key event is part of a composition sequence + * + * @ingroup key + */ +GHOSTTY_API bool ghostty_key_event_get_composing(GhosttyKeyEvent event); + +/** + * Set the UTF-8 text generated by the key for the current keyboard layout. + * + * Must contain the unmodified character before any Ctrl/Meta transformations. + * The encoder derives modifier sequences from the logical key and mods + * bitmask, not from this text. Do not pass C0 control characters + * (U+0000-U+001F, U+007F) or platform function key codes (e.g. macOS PUA + * U+F700-U+F8FF); pass NULL instead and let the encoder use the logical key. + * + * The key event does NOT take ownership of the text pointer. The caller + * must ensure the string remains valid for the lifetime needed by the event. + * + * @param event The key event handle, must not be NULL + * @param utf8 The UTF-8 text to set (or NULL for empty) + * @param len Length of the UTF-8 text in bytes + * + * @ingroup key + */ +GHOSTTY_API void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len); + +/** + * Get the UTF-8 text generated by the key event. + * + * The returned pointer is valid until the event is freed or the UTF-8 text is modified. + * + * @param event The key event handle, must not be NULL + * @param len Pointer to store the length of the UTF-8 text in bytes (may be NULL) + * @return The UTF-8 text (or NULL for empty) + * + * @ingroup key + */ +GHOSTTY_API const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len); + +/** + * Set the unshifted Unicode codepoint. + * + * @param event The key event handle, must not be NULL + * @param codepoint The unshifted Unicode codepoint to set + * + * @ingroup key + */ +GHOSTTY_API void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint); + +/** + * Get the unshifted Unicode codepoint. + * + * @param event The key event handle, must not be NULL + * @return The unshifted Unicode codepoint + * + * @ingroup key + */ +GHOSTTY_API uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event); + +#endif /* GHOSTTY_VT_KEY_EVENT_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/kitty_graphics.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/kitty_graphics.h new file mode 100644 index 00000000000..9bace3a3ccf --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/kitty_graphics.h @@ -0,0 +1,775 @@ +/** + * @file kitty_graphics.h + * + * Kitty graphics protocol + * + * See @ref kitty_graphics for a full usage guide. + */ + +#ifndef GHOSTTY_VT_KITTY_GRAPHICS_H +#define GHOSTTY_VT_KITTY_GRAPHICS_H + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup kitty_graphics Kitty Graphics + * + * API for inspecting images and placements stored via the + * [Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/). + * + * The central object is @ref GhosttyKittyGraphics, an opaque handle to + * the image storage associated with a terminal's active screen. From it + * you can iterate over placements and look up individual images. + * + * ## Obtaining a KittyGraphics Handle + * + * A @ref GhosttyKittyGraphics handle is obtained from a terminal via + * ghostty_terminal_get() with @ref GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS. + * The handle is borrowed from the terminal and remains valid until the + * next mutating terminal call (e.g. ghostty_terminal_vt_write() or + * ghostty_terminal_reset()). + * + * Before images can be stored, Kitty graphics must be enabled on the + * terminal by setting a non-zero storage limit with + * @ref GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT, and a PNG + * decoder callback must be installed via ghostty_sys_set() with + * @ref GHOSTTY_SYS_OPT_DECODE_PNG. + * + * @snippet c-vt-kitty-graphics/src/main.c kitty-graphics-decode-png + * + * ## Iterating Placements + * + * Placements are inspected through a @ref GhosttyKittyGraphicsPlacementIterator. + * The typical workflow is: + * + * 1. Create an iterator with ghostty_kitty_graphics_placement_iterator_new(). + * 2. Populate it from the storage with ghostty_kitty_graphics_get() using + * @ref GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR. + * 3. Optionally filter by z-layer with + * ghostty_kitty_graphics_placement_iterator_set(). + * 4. Advance with ghostty_kitty_graphics_placement_next() and read + * per-placement data with ghostty_kitty_graphics_placement_get(). + * 5. For each placement, look up its image with + * ghostty_kitty_graphics_image() to access pixel data and dimensions. + * 6. Free the iterator with ghostty_kitty_graphics_placement_iterator_free(). + * + * ## Looking Up Images + * + * Given an image ID (obtained from a placement via + * @ref GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID), call + * ghostty_kitty_graphics_image() to get a @ref GhosttyKittyGraphicsImage + * handle. From this handle, ghostty_kitty_graphics_image_get() provides + * the image dimensions, pixel format, compression, and a borrowed pointer + * to the raw pixel data. + * + * ## Rendering Helpers + * + * Several functions assist with rendering a placement: + * + * - ghostty_kitty_graphics_placement_pixel_size() — rendered pixel + * dimensions accounting for source rect and aspect ratio. + * - ghostty_kitty_graphics_placement_grid_size() — number of grid + * columns and rows the placement occupies. + * - ghostty_kitty_graphics_placement_viewport_pos() — viewport-relative + * grid position (may be negative for partially scrolled placements). + * - ghostty_kitty_graphics_placement_source_rect() — resolved source + * rectangle in pixels, clamped to image bounds. + * - ghostty_kitty_graphics_placement_rect() — bounding rectangle as a + * @ref GhosttySelection. + * + * ## Lifetime and Thread Safety + * + * All handles borrowed from the terminal (GhosttyKittyGraphics, + * GhosttyKittyGraphicsImage) are invalidated by any mutating terminal + * call. The placement iterator is independently owned and must be freed + * by the caller, but the data it yields is only valid while the + * underlying terminal is not mutated. + * + * ## Example + * + * The following example creates a terminal, sends a Kitty graphics + * image, then iterates placements and prints image metadata: + * + * @snippet c-vt-kitty-graphics/src/main.c kitty-graphics-main + * + * @{ + */ + +/** + * Queryable data kinds for ghostty_kitty_graphics_get(). + * + * @ingroup kitty_graphics + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Invalid / sentinel value. */ + GHOSTTY_KITTY_GRAPHICS_DATA_INVALID = 0, + + /** + * Populate a pre-allocated placement iterator with placement data from + * the storage. Iterator data is only valid as long as the underlying + * terminal is not mutated. + * + * Output type: GhosttyKittyGraphicsPlacementIterator * + */ + GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR = 1, + GHOSTTY_KITTY_GRAPHICS_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyKittyGraphicsData; + +/** + * Queryable data kinds for ghostty_kitty_graphics_placement_get(). + * + * @ingroup kitty_graphics + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Invalid / sentinel value. */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_INVALID = 0, + + /** + * The image ID this placement belongs to. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID = 1, + + /** + * The placement ID. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID = 2, + + /** + * Whether this is a virtual placement (unicode placeholder). + * + * Output type: bool * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL = 3, + + /** + * Pixel offset from the left edge of the cell. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET = 4, + + /** + * Pixel offset from the top edge of the cell. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET = 5, + + /** + * Source rectangle x origin in pixels. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_X = 6, + + /** + * Source rectangle y origin in pixels. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_Y = 7, + + /** + * Source rectangle width in pixels (0 = full image width). + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_WIDTH = 8, + + /** + * Source rectangle height in pixels (0 = full image height). + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_HEIGHT = 9, + + /** + * Number of columns this placement occupies. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_COLUMNS = 10, + + /** + * Number of rows this placement occupies. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_ROWS = 11, + + /** + * Z-index for this placement. + * + * Output type: int32_t * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z = 12, + + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyKittyGraphicsPlacementData; + +/** + * Z-layer classification for kitty graphics placements. + * + * Based on the kitty protocol z-index conventions: + * - BELOW_BG: z < INT32_MIN/2 (drawn below cell background) + * - BELOW_TEXT: INT32_MIN/2 <= z < 0 (above background, below text) + * - ABOVE_TEXT: z >= 0 (above text) + * - ALL: no filtering (current behavior) + * + * @ingroup kitty_graphics + */ +typedef enum GHOSTTY_ENUM_TYPED { + GHOSTTY_KITTY_PLACEMENT_LAYER_ALL = 0, + GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_BG = 1, + GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_TEXT = 2, + GHOSTTY_KITTY_PLACEMENT_LAYER_ABOVE_TEXT = 3, + GHOSTTY_KITTY_PLACEMENT_LAYER_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyKittyPlacementLayer; + +/** + * Settable options for ghostty_kitty_graphics_placement_iterator_set(). + * + * @ingroup kitty_graphics + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** + * Set the z-layer filter for the iterator. + * + * Input type: GhosttyKittyPlacementLayer * + */ + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_LAYER = 0, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyKittyGraphicsPlacementIteratorOption; + +/** + * Pixel format of a Kitty graphics image. + * + * @ingroup kitty_graphics + */ +typedef enum GHOSTTY_ENUM_TYPED { + GHOSTTY_KITTY_IMAGE_FORMAT_RGB = 0, + GHOSTTY_KITTY_IMAGE_FORMAT_RGBA = 1, + GHOSTTY_KITTY_IMAGE_FORMAT_PNG = 2, + GHOSTTY_KITTY_IMAGE_FORMAT_GRAY_ALPHA = 3, + GHOSTTY_KITTY_IMAGE_FORMAT_GRAY = 4, + GHOSTTY_KITTY_IMAGE_FORMAT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyKittyImageFormat; + +/** + * Compression of a Kitty graphics image. + * + * @ingroup kitty_graphics + */ +typedef enum GHOSTTY_ENUM_TYPED { + GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE = 0, + GHOSTTY_KITTY_IMAGE_COMPRESSION_ZLIB_DEFLATE = 1, + GHOSTTY_KITTY_IMAGE_COMPRESSION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyKittyImageCompression; + +/** + * Queryable data kinds for ghostty_kitty_graphics_image_get(). + * + * @ingroup kitty_graphics + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Invalid / sentinel value. */ + GHOSTTY_KITTY_IMAGE_DATA_INVALID = 0, + + /** + * The image ID. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_IMAGE_DATA_ID = 1, + + /** + * The image number. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_IMAGE_DATA_NUMBER = 2, + + /** + * Image width in pixels. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_IMAGE_DATA_WIDTH = 3, + + /** + * Image height in pixels. + * + * Output type: uint32_t * + */ + GHOSTTY_KITTY_IMAGE_DATA_HEIGHT = 4, + + /** + * Pixel format of the image. + * + * Output type: GhosttyKittyImageFormat * + */ + GHOSTTY_KITTY_IMAGE_DATA_FORMAT = 5, + + /** + * Compression of the image. + * + * Output type: GhosttyKittyImageCompression * + */ + GHOSTTY_KITTY_IMAGE_DATA_COMPRESSION = 6, + + /** + * Borrowed pointer to the raw pixel data. Valid as long as the + * underlying terminal is not mutated. + * + * Output type: const uint8_t ** + */ + GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR = 7, + + /** + * Length of the raw pixel data in bytes. + * + * Output type: size_t * + */ + GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN = 8, + + GHOSTTY_KITTY_IMAGE_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyKittyGraphicsImageData; + +/** + * Combined rendering geometry for a placement in a single sized struct. + * + * Combines the results of ghostty_kitty_graphics_placement_pixel_size(), + * ghostty_kitty_graphics_placement_grid_size(), + * ghostty_kitty_graphics_placement_viewport_pos(), and + * ghostty_kitty_graphics_placement_source_rect() into one call. This is + * an optimization over calling those four functions individually, + * particularly useful in environments with high per-call overhead such + * as FFI or Cgo. + * + * This struct uses the sized-struct ABI pattern. Initialize with + * GHOSTTY_INIT_SIZED(GhosttyKittyGraphicsPlacementRenderInfo) before calling + * ghostty_kitty_graphics_placement_render_info(). + * + * @ingroup kitty_graphics + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyKittyGraphicsPlacementRenderInfo). */ + size_t size; + /** Rendered width in pixels. */ + uint32_t pixel_width; + /** Rendered height in pixels. */ + uint32_t pixel_height; + /** Number of grid columns the placement occupies. */ + uint32_t grid_cols; + /** Number of grid rows the placement occupies. */ + uint32_t grid_rows; + /** Viewport-relative column (may be negative for partially visible placements). */ + int32_t viewport_col; + /** Viewport-relative row (may be negative for partially visible placements). */ + int32_t viewport_row; + /** False when the placement is fully off-screen or virtual. */ + bool viewport_visible; + /** Resolved source rectangle x origin in pixels. */ + uint32_t source_x; + /** Resolved source rectangle y origin in pixels. */ + uint32_t source_y; + /** Resolved source rectangle width in pixels. */ + uint32_t source_width; + /** Resolved source rectangle height in pixels. */ + uint32_t source_height; +} GhosttyKittyGraphicsPlacementRenderInfo; + +/** + * Get data from a kitty graphics storage instance. + * + * The output pointer must be of the appropriate type for the requested + * data kind. + * + * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time. + * + * @param graphics The kitty graphics handle + * @param data The type of data to extract + * @param[out] out Pointer to store the extracted data + * @return GHOSTTY_SUCCESS on success + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_get( + GhosttyKittyGraphics graphics, + GhosttyKittyGraphicsData data, + void* out); + +/** + * Look up a Kitty graphics image by its image ID. + * + * Returns NULL if no image with the given ID exists or if Kitty graphics + * are disabled at build time. + * + * @param graphics The kitty graphics handle + * @param image_id The image ID to look up + * @return An opaque image handle, or NULL if not found + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyKittyGraphicsImage ghostty_kitty_graphics_image( + GhosttyKittyGraphics graphics, + uint32_t image_id); + +/** + * Get data from a Kitty graphics image. + * + * The output pointer must be of the appropriate type for the requested + * data kind. + * + * @param image The image handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param data The data kind to query + * @param[out] out Pointer to receive the queried value + * @return GHOSTTY_SUCCESS on success + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_image_get( + GhosttyKittyGraphicsImage image, + GhosttyKittyGraphicsImageData data, + void* out); + +/** + * Get multiple data fields from a Kitty graphics image in a single call. + * + * This is an optimization over calling ghostty_kitty_graphics_image_get() + * repeatedly, particularly useful in environments with high per-call + * overhead such as FFI or Cgo. + * + * Each element in the keys array specifies a data kind, and the + * corresponding element in the values array receives the result. + * The type of each values[i] pointer must match the output type + * documented for keys[i]. + * + * Processing stops at the first error; on success out_written + * is set to count, on error it is set to the index of the + * failing key (i.e. the number of values successfully written). + * + * @param image The image handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param count Number of key/value pairs + * @param keys Array of data kinds to query + * @param values Array of output pointers (types must match each key's + * documented output type) + * @param[out] out_written On return, receives the number of values + * successfully written (may be NULL) + * @return GHOSTTY_SUCCESS if all queries succeed + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_image_get_multi( + GhosttyKittyGraphicsImage image, + size_t count, + const GhosttyKittyGraphicsImageData* keys, + void** values, + size_t* out_written); + +/** + * Create a new placement iterator instance. + * + * All fields except the allocator are left undefined until populated + * via ghostty_kitty_graphics_get() with + * GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param[out] out_iterator On success, receives the created iterator handle + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation + * failure + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_iterator_new( + const GhosttyAllocator* allocator, + GhosttyKittyGraphicsPlacementIterator* out_iterator); + +/** + * Free a placement iterator. + * + * @param iterator The iterator handle to free (may be NULL) + * + * @ingroup kitty_graphics + */ +GHOSTTY_API void ghostty_kitty_graphics_placement_iterator_free( + GhosttyKittyGraphicsPlacementIterator iterator); + +/** + * Set an option on a placement iterator. + * + * Use GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_LAYER with a + * GhosttyKittyPlacementLayer value to filter placements by z-layer. + * The filter is applied during iteration: ghostty_kitty_graphics_placement_next() + * will skip placements that do not match the configured layer. + * + * The default layer is GHOSTTY_KITTY_PLACEMENT_LAYER_ALL (no filtering). + * + * @param iterator The iterator handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param option The option to set + * @param value Pointer to the value (type depends on option; NULL returns + * GHOSTTY_INVALID_VALUE) + * @return GHOSTTY_SUCCESS on success + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_iterator_set( + GhosttyKittyGraphicsPlacementIterator iterator, + GhosttyKittyGraphicsPlacementIteratorOption option, + const void* value); + +/** + * Advance the placement iterator to the next placement. + * + * If a layer filter has been set via + * ghostty_kitty_graphics_placement_iterator_set(), only placements + * matching that layer are returned. + * + * @param iterator The iterator handle (may be NULL) + * @return true if advanced to the next placement, false if at the end + * + * @ingroup kitty_graphics + */ +GHOSTTY_API bool ghostty_kitty_graphics_placement_next( + GhosttyKittyGraphicsPlacementIterator iterator); + +/** + * Get data from the current placement in a placement iterator. + * + * Call ghostty_kitty_graphics_placement_next() at least once before + * calling this function. + * + * @param iterator The iterator handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param data The data kind to query + * @param[out] out Pointer to receive the queried value + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the + * iterator is NULL or not positioned on a placement + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_get( + GhosttyKittyGraphicsPlacementIterator iterator, + GhosttyKittyGraphicsPlacementData data, + void* out); + +/** + * Get multiple data fields from the current placement in a single call. + * + * This is an optimization over calling ghostty_kitty_graphics_placement_get() + * repeatedly, particularly useful in environments with high per-call + * overhead such as FFI or Cgo. + * + * Each element in the keys array specifies a data kind, and the + * corresponding element in the values array receives the result. + * The type of each values[i] pointer must match the output type + * documented for keys[i]. + * + * Processing stops at the first error; on success out_written + * is set to count, on error it is set to the index of the + * failing key (i.e. the number of values successfully written). + * + * @param iterator The iterator handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param count Number of key/value pairs + * @param keys Array of data kinds to query + * @param values Array of output pointers (types must match each key's + * documented output type) + * @param[out] out_written On return, receives the number of values + * successfully written (may be NULL) + * @return GHOSTTY_SUCCESS if all queries succeed + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_get_multi( + GhosttyKittyGraphicsPlacementIterator iterator, + size_t count, + const GhosttyKittyGraphicsPlacementData* keys, + void** values, + size_t* out_written); + +/** + * Compute the grid rectangle occupied by the current placement. + * + * Uses the placement's pin, the image dimensions, and the terminal's + * cell/pixel geometry to calculate the bounding rectangle. Virtual + * placements (unicode placeholders) return GHOSTTY_NO_VALUE. + * + * @param terminal The terminal handle + * @param image The image handle for this placement's image + * @param iterator The placement iterator positioned on a placement + * @param[out] out_selection On success, receives the bounding rectangle + * as a selection with rectangle=true + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle + * is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE for + * virtual placements or when Kitty graphics are disabled + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_rect( + GhosttyKittyGraphicsPlacementIterator iterator, + GhosttyKittyGraphicsImage image, + GhosttyTerminal terminal, + GhosttySelection* out_selection); + +/** + * Compute the rendered pixel size of the current placement. + * + * Takes into account the placement's source rectangle, specified + * columns/rows, and aspect ratio to calculate the final rendered + * pixel dimensions. + * + * @param iterator The placement iterator positioned on a placement + * @param image The image handle for this placement's image + * @param terminal The terminal handle + * @param[out] out_width On success, receives the width in pixels + * @param[out] out_height On success, receives the height in pixels + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle + * is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE when + * Kitty graphics are disabled + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_pixel_size( + GhosttyKittyGraphicsPlacementIterator iterator, + GhosttyKittyGraphicsImage image, + GhosttyTerminal terminal, + uint32_t* out_width, + uint32_t* out_height); + +/** + * Compute the grid cell size of the current placement. + * + * Returns the number of columns and rows that the placement occupies + * in the terminal grid. If the placement specifies explicit columns + * and rows, those are returned directly; otherwise they are calculated + * from the pixel size and cell dimensions. + * + * @param iterator The placement iterator positioned on a placement + * @param image The image handle for this placement's image + * @param terminal The terminal handle + * @param[out] out_cols On success, receives the number of columns + * @param[out] out_rows On success, receives the number of rows + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle + * is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE when + * Kitty graphics are disabled + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_grid_size( + GhosttyKittyGraphicsPlacementIterator iterator, + GhosttyKittyGraphicsImage image, + GhosttyTerminal terminal, + uint32_t* out_cols, + uint32_t* out_rows); + +/** + * Get the viewport-relative grid position of the current placement. + * + * Converts the placement's internal pin to viewport-relative column and + * row coordinates. The returned coordinates represent the top-left + * corner of the placement in the viewport's grid coordinate space. + * + * The row value can be negative when the placement's origin has + * scrolled above the top of the viewport. For example, a 4-row + * image that has scrolled up by 2 rows returns row=-2, meaning + * its top 2 rows are above the visible area but its bottom 2 rows + * are still on screen. Embedders should use these coordinates + * directly when computing the destination rectangle for rendering; + * the embedder is responsible for clipping the portion of the image + * that falls outside the viewport. + * + * Returns GHOSTTY_SUCCESS for any placement that is at least + * partially visible in the viewport. Returns GHOSTTY_NO_VALUE when + * the placement is completely outside the viewport (its bottom edge + * is above the viewport or its top edge is at or below the last + * viewport row), or when the placement is a virtual (unicode + * placeholder) placement. + * + * @param iterator The placement iterator positioned on a placement + * @param image The image handle for this placement's image + * @param terminal The terminal handle + * @param[out] out_col On success, receives the viewport-relative column + * @param[out] out_row On success, receives the viewport-relative row + * (may be negative for partially visible placements) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if fully + * off-screen or virtual, GHOSTTY_INVALID_VALUE if any handle + * is NULL or the iterator is not positioned + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_viewport_pos( + GhosttyKittyGraphicsPlacementIterator iterator, + GhosttyKittyGraphicsImage image, + GhosttyTerminal terminal, + int32_t* out_col, + int32_t* out_row); + +/** + * Get the resolved source rectangle for the current placement. + * + * Applies kitty protocol semantics: a width or height of 0 in the + * placement means "use the full image dimension", and the resulting + * rectangle is clamped to the actual image bounds. The returned + * values are in pixels and are ready to use for texture sampling. + * + * @param iterator The placement iterator positioned on a placement + * @param image The image handle for this placement's image + * @param[out] out_x Source rect x origin in pixels + * @param[out] out_y Source rect y origin in pixels + * @param[out] out_width Source rect width in pixels + * @param[out] out_height Source rect height in pixels + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any + * handle is NULL or the iterator is not positioned + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_source_rect( + GhosttyKittyGraphicsPlacementIterator iterator, + GhosttyKittyGraphicsImage image, + uint32_t* out_x, + uint32_t* out_y, + uint32_t* out_width, + uint32_t* out_height); + +/** + * Get all rendering geometry for a placement in a single call. + * + * Combines pixel size, grid size, viewport position, and source + * rectangle into one struct. Initialize with + * GHOSTTY_INIT_SIZED(GhosttyKittyGraphicsPlacementRenderInfo). + * + * When viewport_visible is false, the placement is fully off-screen + * or is a virtual placement; viewport_col and viewport_row may + * contain meaningless values in that case. + * + * @param iterator The iterator positioned on a placement + * @param image The image handle for this placement's image + * @param terminal The terminal handle + * @param[out] out_info Pointer to receive the rendering geometry + * @return GHOSTTY_SUCCESS on success + * + * @ingroup kitty_graphics + */ +GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_render_info( + GhosttyKittyGraphicsPlacementIterator iterator, + GhosttyKittyGraphicsImage image, + GhosttyTerminal terminal, + GhosttyKittyGraphicsPlacementRenderInfo* out_info); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_KITTY_GRAPHICS_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/modes.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/modes.h new file mode 100644 index 00000000000..8e1fd91179e --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/modes.h @@ -0,0 +1,198 @@ +/** + * @file modes.h + * + * Terminal mode utilities - pack and unpack ANSI/DEC mode identifiers. + */ + +#ifndef GHOSTTY_VT_MODES_H +#define GHOSTTY_VT_MODES_H + +/** @defgroup modes Mode Utilities + * + * Utilities for working with terminal modes. A mode is a compact + * 16-bit representation of a terminal mode identifier that encodes both + * the numeric mode value (up to 15 bits) and whether the mode is an ANSI + * mode or a DEC private mode (?-prefixed). + * + * The packed layout (least-significant bit first) is: + * - Bits 0–14: mode value (u15) + * - Bit 15: ANSI flag (0 = DEC private mode, 1 = ANSI mode) + * + * ## Example + * + * @snippet c-vt-modes/src/main.c modes-pack-unpack + * + * ## DECRPM Report Encoding + * + * Use ghostty_mode_report_encode() to encode a DECRPM response into a + * caller-provided buffer: + * + * @snippet c-vt-modes/src/main.c modes-decrpm + * + * @{ + */ + +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @name ANSI Modes + * Modes for standard ANSI modes. + * @{ + */ +#define GHOSTTY_MODE_KAM (ghostty_mode_new(2, true)) /**< Keyboard action (disable keyboard) */ +#define GHOSTTY_MODE_INSERT (ghostty_mode_new(4, true)) /**< Insert mode */ +#define GHOSTTY_MODE_SRM (ghostty_mode_new(12, true)) /**< Send/receive mode */ +#define GHOSTTY_MODE_LINEFEED (ghostty_mode_new(20, true)) /**< Linefeed/new line mode */ +/** @} */ + +/** @name DEC Private Modes + * Modes for DEC private modes (?-prefixed). + * @{ + */ +#define GHOSTTY_MODE_DECCKM (ghostty_mode_new(1, false)) /**< Cursor keys */ +#define GHOSTTY_MODE_132_COLUMN (ghostty_mode_new(3, false)) /**< 132/80 column mode */ +#define GHOSTTY_MODE_SLOW_SCROLL (ghostty_mode_new(4, false)) /**< Slow scroll */ +#define GHOSTTY_MODE_REVERSE_COLORS (ghostty_mode_new(5, false)) /**< Reverse video */ +#define GHOSTTY_MODE_ORIGIN (ghostty_mode_new(6, false)) /**< Origin mode */ +#define GHOSTTY_MODE_WRAPAROUND (ghostty_mode_new(7, false)) /**< Auto-wrap mode */ +#define GHOSTTY_MODE_AUTOREPEAT (ghostty_mode_new(8, false)) /**< Auto-repeat keys */ +#define GHOSTTY_MODE_X10_MOUSE (ghostty_mode_new(9, false)) /**< X10 mouse reporting */ +#define GHOSTTY_MODE_CURSOR_BLINKING (ghostty_mode_new(12, false)) /**< Cursor blink */ +#define GHOSTTY_MODE_CURSOR_VISIBLE (ghostty_mode_new(25, false)) /**< Cursor visible (DECTCEM) */ +#define GHOSTTY_MODE_ENABLE_MODE_3 (ghostty_mode_new(40, false)) /**< Allow 132 column mode */ +#define GHOSTTY_MODE_REVERSE_WRAP (ghostty_mode_new(45, false)) /**< Reverse wrap */ +#define GHOSTTY_MODE_ALT_SCREEN_LEGACY (ghostty_mode_new(47, false)) /**< Alternate screen (legacy) */ +#define GHOSTTY_MODE_KEYPAD_KEYS (ghostty_mode_new(66, false)) /**< Application keypad */ +#define GHOSTTY_MODE_BACKARROW_KEY_MODE (ghostty_mode_new(67, false)) /**< Backarrow key mode (DECBKM) */ +#define GHOSTTY_MODE_LEFT_RIGHT_MARGIN (ghostty_mode_new(69, false)) /**< Left/right margin mode */ +#define GHOSTTY_MODE_NORMAL_MOUSE (ghostty_mode_new(1000, false)) /**< Normal mouse tracking */ +#define GHOSTTY_MODE_BUTTON_MOUSE (ghostty_mode_new(1002, false)) /**< Button-event mouse tracking */ +#define GHOSTTY_MODE_ANY_MOUSE (ghostty_mode_new(1003, false)) /**< Any-event mouse tracking */ +#define GHOSTTY_MODE_FOCUS_EVENT (ghostty_mode_new(1004, false)) /**< Focus in/out events */ +#define GHOSTTY_MODE_UTF8_MOUSE (ghostty_mode_new(1005, false)) /**< UTF-8 mouse format */ +#define GHOSTTY_MODE_SGR_MOUSE (ghostty_mode_new(1006, false)) /**< SGR mouse format */ +#define GHOSTTY_MODE_ALT_SCROLL (ghostty_mode_new(1007, false)) /**< Alternate scroll mode */ +#define GHOSTTY_MODE_URXVT_MOUSE (ghostty_mode_new(1015, false)) /**< URxvt mouse format */ +#define GHOSTTY_MODE_SGR_PIXELS_MOUSE (ghostty_mode_new(1016, false)) /**< SGR-Pixels mouse format */ +#define GHOSTTY_MODE_NUMLOCK_KEYPAD (ghostty_mode_new(1035, false)) /**< Ignore keypad with NumLock */ +#define GHOSTTY_MODE_ALT_ESC_PREFIX (ghostty_mode_new(1036, false)) /**< Alt key sends ESC prefix */ +#define GHOSTTY_MODE_ALT_SENDS_ESC (ghostty_mode_new(1039, false)) /**< Alt sends escape */ +#define GHOSTTY_MODE_REVERSE_WRAP_EXT (ghostty_mode_new(1045, false)) /**< Extended reverse wrap */ +#define GHOSTTY_MODE_ALT_SCREEN (ghostty_mode_new(1047, false)) /**< Alternate screen */ +#define GHOSTTY_MODE_SAVE_CURSOR (ghostty_mode_new(1048, false)) /**< Save cursor (DECSC) */ +#define GHOSTTY_MODE_ALT_SCREEN_SAVE (ghostty_mode_new(1049, false)) /**< Alt screen + save cursor + clear */ +#define GHOSTTY_MODE_BRACKETED_PASTE (ghostty_mode_new(2004, false)) /**< Bracketed paste mode */ +#define GHOSTTY_MODE_SYNC_OUTPUT (ghostty_mode_new(2026, false)) /**< Synchronized output */ +#define GHOSTTY_MODE_GRAPHEME_CLUSTER (ghostty_mode_new(2027, false)) /**< Grapheme cluster mode */ +#define GHOSTTY_MODE_COLOR_SCHEME_REPORT (ghostty_mode_new(2031, false)) /**< Report color scheme */ +#define GHOSTTY_MODE_IN_BAND_RESIZE (ghostty_mode_new(2048, false)) /**< In-band size reports */ +/** @} */ + +/** + * A packed 16-bit terminal mode. + * + * Encodes a mode value (bits 0–14) and an ANSI flag (bit 15) into a + * single 16-bit integer. Use the inline helper functions to construct + * and inspect modes rather than manipulating bits directly. + */ +typedef uint16_t GhosttyMode; + +/** + * Create a mode from a mode value and ANSI flag. + * + * @param value The numeric mode value (0–32767) + * @param ansi true for an ANSI mode, false for a DEC private mode + * @return The packed mode + * + * @ingroup modes + */ +static inline GhosttyMode ghostty_mode_new(uint16_t value, bool ansi) { + return (GhosttyMode)((value & 0x7FFF) | ((uint16_t)ansi << 15)); +} + +/** + * Extract the numeric mode value from a mode. + * + * @param mode The mode + * @return The mode value (0–32767) + * + * @ingroup modes + */ +static inline uint16_t ghostty_mode_value(GhosttyMode mode) { + return mode & 0x7FFF; +} + +/** + * Check whether a mode represents an ANSI mode. + * + * @param mode The mode + * @return true if this is an ANSI mode, false if it is a DEC private mode + * + * @ingroup modes + */ +static inline bool ghostty_mode_ansi(GhosttyMode mode) { + return (mode >> 15) != 0; +} + +/** + * DECRPM report state values. + * + * These correspond to the Ps2 parameter in a DECRPM response + * sequence (CSI ? Ps1 ; Ps2 $ y). + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Mode is not recognized */ + GHOSTTY_MODE_REPORT_NOT_RECOGNIZED = 0, + /** Mode is set (enabled) */ + GHOSTTY_MODE_REPORT_SET = 1, + /** Mode is reset (disabled) */ + GHOSTTY_MODE_REPORT_RESET = 2, + /** Mode is permanently set */ + GHOSTTY_MODE_REPORT_PERMANENTLY_SET = 3, + /** Mode is permanently reset */ + GHOSTTY_MODE_REPORT_PERMANENTLY_RESET = 4, + GHOSTTY_MODE_REPORT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyModeReportState; + +/** + * Encode a DECRPM (DEC Private Mode Report) response sequence. + * + * Writes a mode report escape sequence into the provided buffer. + * The generated sequence has the form: + * - DEC private mode: CSI ? Ps1 ; Ps2 $ y + * - ANSI mode: CSI Ps1 ; Ps2 $ y + * + * If the buffer is too small, the function returns GHOSTTY_OUT_OF_SPACE + * and writes the required buffer size to @p out_written. The caller can + * then retry with a sufficiently sized buffer. + * + * @param mode The mode identifying the mode to report on + * @param state The report state for this mode + * @param buf Output buffer to write the encoded sequence into (may be NULL) + * @param buf_len Size of the output buffer in bytes + * @param[out] out_written On success, the number of bytes written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer + * is too small + */ +GHOSTTY_API GhosttyResult ghostty_mode_report_encode( + GhosttyMode mode, + GhosttyModeReportState state, + char* buf, + size_t buf_len, + size_t* out_written); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_MODES_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse.h new file mode 100644 index 00000000000..4ba5f52e38b --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse.h @@ -0,0 +1,70 @@ +/** + * @file mouse.h + * + * Mouse encoding module - encode mouse events into terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_MOUSE_H +#define GHOSTTY_VT_MOUSE_H + +/** @defgroup mouse Mouse Encoding + * + * Utilities for encoding mouse events into terminal escape sequences, + * supporting X10, UTF-8, SGR, URxvt, and SGR-Pixels mouse protocols. + * + * ## Basic Usage + * + * 1. Create an encoder instance with ghostty_mouse_encoder_new(). + * 2. Configure encoder options with ghostty_mouse_encoder_setopt() or + * ghostty_mouse_encoder_setopt_from_terminal(). + * 3. For each mouse event: + * - Create a mouse event with ghostty_mouse_event_new(). + * - Set event properties (action, button, modifiers, position). + * - Encode with ghostty_mouse_encoder_encode(). + * - Free the event with ghostty_mouse_event_free() or reuse it. + * 4. Free the encoder with ghostty_mouse_encoder_free() when done. + * + * For a complete working example, see example/c-vt-encode-mouse in the + * repository. + * + * ## Example + * + * @snippet c-vt-encode-mouse/src/main.c mouse-encode + * + * ## Example: Encoding with Terminal State + * + * When you have a GhosttyTerminal, you can sync its tracking mode and + * output format into the encoder automatically: + * + * @code{.c} + * // Create a terminal and feed it some VT data that enables mouse tracking + * GhosttyTerminal terminal; + * ghostty_terminal_new(NULL, &terminal, + * (GhosttyTerminalOptions){.cols = 80, .rows = 24, .max_scrollback = 0}); + * + * // Application might write data that enables mouse reporting, etc. + * ghostty_terminal_vt_write(terminal, vt_data, vt_len); + * + * // Create an encoder and sync its options from the terminal + * GhosttyMouseEncoder encoder; + * ghostty_mouse_encoder_new(NULL, &encoder); + * ghostty_mouse_encoder_setopt_from_terminal(encoder, terminal); + * + * // Encode a mouse event using the terminal-derived options + * char buf[128]; + * size_t written = 0; + * ghostty_mouse_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * + * ghostty_mouse_encoder_free(encoder); + * ghostty_terminal_free(terminal); + * @endcode + * + * @{ + */ + +#include +#include + +/** @} */ + +#endif /* GHOSTTY_VT_MOUSE_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse/encoder.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse/encoder.h new file mode 100644 index 00000000000..d84d863c8d7 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse/encoder.h @@ -0,0 +1,214 @@ +/** + * @file encoder.h + * + * Mouse event encoding to terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_MOUSE_ENCODER_H +#define GHOSTTY_VT_MOUSE_ENCODER_H + +#include +#include +#include +#include +#include +#include +#include + +/** + * Opaque handle to a mouse encoder instance. + * + * This handle represents a mouse encoder that converts normalized + * mouse events into terminal escape sequences. + * + * @ingroup mouse + */ +typedef struct GhosttyMouseEncoderImpl *GhosttyMouseEncoder; + +/** + * Mouse tracking mode. + * + * @ingroup mouse + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Mouse reporting disabled. */ + GHOSTTY_MOUSE_TRACKING_NONE = 0, + + /** X10 mouse mode. */ + GHOSTTY_MOUSE_TRACKING_X10 = 1, + + /** Normal mouse mode (button press/release only). */ + GHOSTTY_MOUSE_TRACKING_NORMAL = 2, + + /** Button-event tracking mode. */ + GHOSTTY_MOUSE_TRACKING_BUTTON = 3, + + /** Any-event tracking mode. */ + GHOSTTY_MOUSE_TRACKING_ANY = 4, + GHOSTTY_MOUSE_TRACKING_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyMouseTrackingMode; + +/** + * Mouse output format. + * + * @ingroup mouse + */ +typedef enum GHOSTTY_ENUM_TYPED { + GHOSTTY_MOUSE_FORMAT_X10 = 0, + GHOSTTY_MOUSE_FORMAT_UTF8 = 1, + GHOSTTY_MOUSE_FORMAT_SGR = 2, + GHOSTTY_MOUSE_FORMAT_URXVT = 3, + GHOSTTY_MOUSE_FORMAT_SGR_PIXELS = 4, + GHOSTTY_MOUSE_FORMAT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyMouseFormat; + +/** + * Mouse encoder size and geometry context. + * + * This describes the rendered terminal geometry used to convert + * surface-space positions into encoded coordinates. + * + * @ingroup mouse + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyMouseEncoderSize). */ + size_t size; + + /** Full screen width in pixels. */ + uint32_t screen_width; + + /** Full screen height in pixels. */ + uint32_t screen_height; + + /** Cell width in pixels. Must be non-zero. */ + uint32_t cell_width; + + /** Cell height in pixels. Must be non-zero. */ + uint32_t cell_height; + + /** Top padding in pixels. */ + uint32_t padding_top; + + /** Bottom padding in pixels. */ + uint32_t padding_bottom; + + /** Right padding in pixels. */ + uint32_t padding_right; + + /** Left padding in pixels. */ + uint32_t padding_left; +} GhosttyMouseEncoderSize; + +/** + * Mouse encoder option identifiers. + * + * These values are used with ghostty_mouse_encoder_setopt() to configure + * the behavior of the mouse encoder. + * + * @ingroup mouse + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Mouse tracking mode (value: GhosttyMouseTrackingMode). */ + GHOSTTY_MOUSE_ENCODER_OPT_EVENT = 0, + + /** Mouse output format (value: GhosttyMouseFormat). */ + GHOSTTY_MOUSE_ENCODER_OPT_FORMAT = 1, + + /** Renderer size context (value: GhosttyMouseEncoderSize). */ + GHOSTTY_MOUSE_ENCODER_OPT_SIZE = 2, + + /** Whether any mouse button is currently pressed (value: bool). */ + GHOSTTY_MOUSE_ENCODER_OPT_ANY_BUTTON_PRESSED = 3, + + /** Whether to enable motion deduplication by last cell (value: bool). */ + GHOSTTY_MOUSE_ENCODER_OPT_TRACK_LAST_CELL = 4, + GHOSTTY_MOUSE_ENCODER_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyMouseEncoderOption; + +/** + * Create a new mouse encoder instance. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param encoder Pointer to store the created encoder handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup mouse + */ +GHOSTTY_API GhosttyResult ghostty_mouse_encoder_new(const GhosttyAllocator *allocator, + GhosttyMouseEncoder *encoder); + +/** + * Free a mouse encoder instance. + * + * @param encoder The encoder handle to free (may be NULL) + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_encoder_free(GhosttyMouseEncoder encoder); + +/** + * Set an option on the mouse encoder. + * + * A null pointer value does nothing. It does not reset to defaults. + * + * @param encoder The encoder handle, must not be NULL + * @param option The option to set + * @param value Pointer to option value (type depends on option) + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_encoder_setopt(GhosttyMouseEncoder encoder, + GhosttyMouseEncoderOption option, + const void *value); + +/** + * Set encoder options from a terminal's current state. + * + * This sets tracking mode and output format from terminal state. + * It does not modify size or any-button state. + * + * @param encoder The encoder handle, must not be NULL + * @param terminal The terminal handle, must not be NULL + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_encoder_setopt_from_terminal(GhosttyMouseEncoder encoder, + GhosttyTerminal terminal); + +/** + * Reset internal encoder state. + * + * This clears motion deduplication state (last tracked cell). + * + * @param encoder The encoder handle (may be NULL) + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_encoder_reset(GhosttyMouseEncoder encoder); + +/** + * Encode a mouse event into a terminal escape sequence. + * + * Not all mouse events produce output. In such cases this returns + * GHOSTTY_SUCCESS with out_len set to 0. + * + * If the output buffer is too small, this returns GHOSTTY_OUT_OF_SPACE + * and out_len contains the required size. + * + * @param encoder The encoder handle, must not be NULL + * @param event The mouse event to encode, must not be NULL + * @param out_buf Buffer to write encoded bytes to, or NULL to query required size + * @param out_buf_size Size of out_buf in bytes + * @param out_len Pointer to store bytes written (or required bytes on failure) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if buffer is too small, + * or another error code + * + * @ingroup mouse + */ +GHOSTTY_API GhosttyResult ghostty_mouse_encoder_encode(GhosttyMouseEncoder encoder, + GhosttyMouseEvent event, + char *out_buf, + size_t out_buf_size, + size_t *out_len); + +#endif /* GHOSTTY_VT_MOUSE_ENCODER_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse/event.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse/event.h new file mode 100644 index 00000000000..a24b0c079bb --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse/event.h @@ -0,0 +1,195 @@ +/** + * @file event.h + * + * Mouse event representation and manipulation. + */ + +#ifndef GHOSTTY_VT_MOUSE_EVENT_H +#define GHOSTTY_VT_MOUSE_EVENT_H + +#include +#include +#include +#include + +/** + * Opaque handle to a mouse event. + * + * This handle represents a normalized mouse input event containing + * action, button, modifiers, and surface-space position. + * + * @ingroup mouse + */ +typedef struct GhosttyMouseEventImpl *GhosttyMouseEvent; + +/** + * Mouse event action type. + * + * @ingroup mouse + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Mouse button was pressed. */ + GHOSTTY_MOUSE_ACTION_PRESS = 0, + + /** Mouse button was released. */ + GHOSTTY_MOUSE_ACTION_RELEASE = 1, + + /** Mouse moved. */ + GHOSTTY_MOUSE_ACTION_MOTION = 2, + GHOSTTY_MOUSE_ACTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyMouseAction; + +/** + * Mouse button identity. + * + * @ingroup mouse + */ +typedef enum GHOSTTY_ENUM_TYPED { + GHOSTTY_MOUSE_BUTTON_UNKNOWN = 0, + GHOSTTY_MOUSE_BUTTON_LEFT = 1, + GHOSTTY_MOUSE_BUTTON_RIGHT = 2, + GHOSTTY_MOUSE_BUTTON_MIDDLE = 3, + GHOSTTY_MOUSE_BUTTON_FOUR = 4, + GHOSTTY_MOUSE_BUTTON_FIVE = 5, + GHOSTTY_MOUSE_BUTTON_SIX = 6, + GHOSTTY_MOUSE_BUTTON_SEVEN = 7, + GHOSTTY_MOUSE_BUTTON_EIGHT = 8, + GHOSTTY_MOUSE_BUTTON_NINE = 9, + GHOSTTY_MOUSE_BUTTON_TEN = 10, + GHOSTTY_MOUSE_BUTTON_ELEVEN = 11, + GHOSTTY_MOUSE_BUTTON_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyMouseButton; + +/** + * Mouse position in surface-space pixels. + * + * @ingroup mouse + */ +typedef struct { + float x; + float y; +} GhosttyMousePosition; + +/** + * Create a new mouse event instance. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param event Pointer to store the created event handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup mouse + */ +GHOSTTY_API GhosttyResult ghostty_mouse_event_new(const GhosttyAllocator *allocator, + GhosttyMouseEvent *event); + +/** + * Free a mouse event instance. + * + * @param event The mouse event handle to free (may be NULL) + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_event_free(GhosttyMouseEvent event); + +/** + * Set the event action. + * + * @param event The event handle, must not be NULL + * @param action The action to set + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_event_set_action(GhosttyMouseEvent event, + GhosttyMouseAction action); + +/** + * Get the event action. + * + * @param event The event handle, must not be NULL + * @return The event action + * + * @ingroup mouse + */ +GHOSTTY_API GhosttyMouseAction ghostty_mouse_event_get_action(GhosttyMouseEvent event); + +/** + * Set the event button. + * + * This sets a concrete button identity for the event. + * To represent "no button" (for motion events), use + * ghostty_mouse_event_clear_button(). + * + * @param event The event handle, must not be NULL + * @param button The button to set + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_event_set_button(GhosttyMouseEvent event, + GhosttyMouseButton button); + +/** + * Clear the event button. + * + * This sets the event button to "none". + * + * @param event The event handle, must not be NULL + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_event_clear_button(GhosttyMouseEvent event); + +/** + * Get the event button. + * + * @param event The event handle, must not be NULL + * @param out_button Output pointer for the button value (may be NULL) + * @return true if a button is set, false if no button is set + * + * @ingroup mouse + */ +GHOSTTY_API bool ghostty_mouse_event_get_button(GhosttyMouseEvent event, + GhosttyMouseButton *out_button); + +/** + * Set keyboard modifiers held during the event. + * + * @param event The event handle, must not be NULL + * @param mods Modifier bitmask + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_event_set_mods(GhosttyMouseEvent event, + GhosttyMods mods); + +/** + * Get keyboard modifiers held during the event. + * + * @param event The event handle, must not be NULL + * @return Modifier bitmask + * + * @ingroup mouse + */ +GHOSTTY_API GhosttyMods ghostty_mouse_event_get_mods(GhosttyMouseEvent event); + +/** + * Set the event position in surface-space pixels. + * + * @param event The event handle, must not be NULL + * @param position The position to set + * + * @ingroup mouse + */ +GHOSTTY_API void ghostty_mouse_event_set_position(GhosttyMouseEvent event, + GhosttyMousePosition position); + +/** + * Get the event position in surface-space pixels. + * + * @param event The event handle, must not be NULL + * @return The current event position + * + * @ingroup mouse + */ +GHOSTTY_API GhosttyMousePosition ghostty_mouse_event_get_position(GhosttyMouseEvent event); + +#endif /* GHOSTTY_VT_MOUSE_EVENT_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/osc.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/osc.h new file mode 100644 index 00000000000..9409ebc738f --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/osc.h @@ -0,0 +1,215 @@ +/** + * @file osc.h + * + * OSC (Operating System Command) sequence parser and command handling. + */ + +#ifndef GHOSTTY_VT_OSC_H +#define GHOSTTY_VT_OSC_H + +#include +#include +#include +#include +#include + +/** @defgroup osc OSC Parser + * + * OSC (Operating System Command) sequence parser and command handling. + * + * The parser operates in a streaming fashion, processing input byte-by-byte + * to handle OSC sequences that may arrive in fragments across multiple reads. + * This interface makes it easy to integrate into most environments and avoids + * over-allocating buffers. + * + * ## Basic Usage + * + * 1. Create a parser instance with ghostty_osc_new() + * 2. Feed bytes to the parser using ghostty_osc_next() + * 3. Finalize parsing with ghostty_osc_end() to get the command + * 4. Query command type and extract data using ghostty_osc_command_type() + * and ghostty_osc_command_data() + * 5. Free the parser with ghostty_osc_free() when done + * + * @{ + */ + +/** + * OSC command types. + * + * @ingroup osc + */ +typedef enum GHOSTTY_ENUM_TYPED { + GHOSTTY_OSC_COMMAND_INVALID = 0, + GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1, + GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2, + GHOSTTY_OSC_COMMAND_SEMANTIC_PROMPT = 3, + GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 4, + GHOSTTY_OSC_COMMAND_REPORT_PWD = 5, + GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 6, + GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 7, + GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 8, + GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 9, + GHOSTTY_OSC_COMMAND_HYPERLINK_START = 10, + GHOSTTY_OSC_COMMAND_HYPERLINK_END = 11, + GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 12, + GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 13, + GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 14, + GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 15, + GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 16, + GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 17, + GHOSTTY_OSC_COMMAND_CONEMU_RUN_PROCESS = 18, + GHOSTTY_OSC_COMMAND_CONEMU_OUTPUT_ENVIRONMENT_VARIABLE = 19, + GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20, + GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21, + GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22, + GHOSTTY_OSC_COMMAND_TYPE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyOscCommandType; + +/** + * OSC command data types. + * + * These values specify what type of data to extract from an OSC command + * using `ghostty_osc_command_data`. + * + * @ingroup osc + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Invalid data type. Never results in any data extraction. */ + GHOSTTY_OSC_DATA_INVALID = 0, + + /** + * Window title string data. + * + * Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE + * + * Output type: const char ** (pointer to null-terminated string) + * + * Lifetime: Valid until the next call to any ghostty_osc_* function with + * the same parser instance. Memory is owned by the parser. + */ + GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1, + GHOSTTY_OSC_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyOscCommandData; + +/** + * Create a new OSC parser instance. + * + * Creates a new OSC (Operating System Command) parser using the provided + * allocator. The parser must be freed using ghostty_vt_osc_free() when + * no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param parser Pointer to store the created parser handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup osc + */ +GHOSTTY_API GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser); + +/** + * Free an OSC parser instance. + * + * Releases all resources associated with the OSC parser. After this call, + * the parser handle becomes invalid and must not be used. + * + * @param parser The parser handle to free (may be NULL) + * + * @ingroup osc + */ +GHOSTTY_API void ghostty_osc_free(GhosttyOscParser parser); + +/** + * Reset an OSC parser instance to its initial state. + * + * Resets the parser state, clearing any partially parsed OSC sequences + * and returning the parser to its initial state. This is useful for + * reusing a parser instance or recovering from parse errors. + * + * @param parser The parser handle to reset, must not be null. + * + * @ingroup osc + */ +GHOSTTY_API void ghostty_osc_reset(GhosttyOscParser parser); + +/** + * Parse the next byte in an OSC sequence. + * + * Processes a single byte as part of an OSC sequence. The parser maintains + * internal state to track the progress through the sequence. Call this + * function for each byte in the sequence data. + * + * When finished pumping the parser with bytes, call ghostty_osc_end + * to get the final result. + * + * @param parser The parser handle, must not be null. + * @param byte The next byte to parse + * + * @ingroup osc + */ +GHOSTTY_API void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); + +/** + * Finalize OSC parsing and retrieve the parsed command. + * + * Call this function after feeding all bytes of an OSC sequence to the parser + * using ghostty_osc_next() with the exception of the terminating character + * (ESC or ST). This function finalizes the parsing process and returns the + * parsed OSC command. + * + * The return value is never NULL. Invalid commands will return a command + * with type GHOSTTY_OSC_COMMAND_INVALID. + * + * The terminator parameter specifies the byte that terminated the OSC sequence + * (typically 0x07 for BEL or 0x5C for ST after ESC). This information is + * preserved in the parsed command so that responses can use the same terminator + * format for better compatibility with the calling program. For commands that + * do not require a response, this parameter is ignored and the resulting + * command will not retain the terminator information. + * + * The returned command handle is valid until the next call to any + * `ghostty_osc_*` function with the same parser instance with the exception + * of command introspection functions such as `ghostty_osc_command_type`. + * + * @param parser The parser handle, must not be null. + * @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST) + * @return Handle to the parsed OSC command + * + * @ingroup osc + */ +GHOSTTY_API GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator); + +/** + * Get the type of an OSC command. + * + * Returns the type identifier for the given OSC command. This can be used + * to determine what kind of command was parsed and what data might be + * available from it. + * + * @param command The OSC command handle to query (may be NULL) + * @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL + * + * @ingroup osc + */ +GHOSTTY_API GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); + +/** + * Extract data from an OSC command. + * + * Extracts typed data from the given OSC command based on the specified + * data type. The output pointer must be of the appropriate type for the + * requested data kind. Valid command types, output types, and memory + * safety information are documented in the `GhosttyOscCommandData` enum. + * + * @param command The OSC command handle to query (may be NULL) + * @param data The type of data to extract + * @param out Pointer to store the extracted data (type depends on data parameter) + * @return true if data extraction was successful, false otherwise + * + * @ingroup osc + */ +GHOSTTY_API bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out); + +/** @} */ + +#endif /* GHOSTTY_VT_OSC_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/paste.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/paste.h new file mode 100644 index 00000000000..b3df5be4e0d --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/paste.h @@ -0,0 +1,101 @@ +/** + * @file paste.h + * + * Paste utilities - validate and encode paste data for terminal input. + */ + +#ifndef GHOSTTY_VT_PASTE_H +#define GHOSTTY_VT_PASTE_H + +/** @defgroup paste Paste Utilities + * + * Utilities for validating and encoding paste data for terminal input. + * + * ## Basic Usage + * + * Use ghostty_paste_is_safe() to check if paste data contains potentially + * dangerous sequences before sending it to the terminal. + * + * Use ghostty_paste_encode() to encode paste data for writing to the pty, + * including bracketed paste wrapping and unsafe byte stripping. + * + * ## Examples + * + * ### Safety Check + * + * @snippet c-vt-paste/src/main.c paste-safety + * + * ### Encoding + * + * @snippet c-vt-paste/src/main.c paste-encode + * + * @{ + */ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Check if paste data is safe to paste into the terminal. + * + * Data is considered unsafe if it contains: + * - Newlines (`\n`) which can inject commands + * - The bracketed paste end sequence (`\x1b[201~`) which can be used + * to exit bracketed paste mode and inject commands + * + * This check is conservative and considers data unsafe regardless of + * current terminal state. + * + * @param data The paste data to check (must not be NULL) + * @param len The length of the data in bytes + * @return true if the data is safe to paste, false otherwise + */ +GHOSTTY_API bool ghostty_paste_is_safe(const char* data, size_t len); + +/** + * Encode paste data for writing to the terminal pty. + * + * This function prepares paste data for terminal input by: + * - Stripping unsafe control bytes (NUL, ESC, DEL, etc.) by replacing + * them with spaces + * - Wrapping the data in bracketed paste sequences if @p bracketed is true + * - Replacing newlines with carriage returns if @p bracketed is false + * + * The input @p data buffer is modified in place during encoding. The + * encoded result (potentially with bracketed paste prefix/suffix) is + * written to the output buffer. + * + * If the output buffer is too small, the function returns + * GHOSTTY_OUT_OF_SPACE and sets the required size in @p out_written. + * The caller can then retry with a sufficiently sized buffer. + * + * @param data The paste data to encode (modified in place, may be NULL) + * @param data_len The length of the input data in bytes + * @param bracketed Whether bracketed paste mode is active + * @param buf Output buffer to write the encoded result into (may be NULL) + * @param buf_len Size of the output buffer in bytes + * @param[out] out_written On success, the number of bytes written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer + * is too small + */ +GHOSTTY_API GhosttyResult ghostty_paste_encode( + char* data, + size_t data_len, + bool bracketed, + char* buf, + size_t buf_len, + size_t* out_written); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_PASTE_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/point.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/point.h new file mode 100644 index 00000000000..8b717f4940c --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/point.h @@ -0,0 +1,89 @@ +/** + * @file point.h + * + * Terminal point types for referencing locations in the terminal grid. + */ + +#ifndef GHOSTTY_VT_POINT_H +#define GHOSTTY_VT_POINT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup point Point + * + * Types for referencing x/y positions in the terminal grid under + * different coordinate systems (active area, viewport, full screen, + * scrollback history). + * + * @{ + */ + +/** + * A coordinate in the terminal grid. + * + * @ingroup point + */ +typedef struct { + /** Column (0-indexed). */ + uint16_t x; + + /** Row (0-indexed). May exceed page size for screen/history tags. */ + uint32_t y; +} GhosttyPointCoordinate; + +/** + * Point reference tag. + * + * Determines which coordinate system a point uses. + * + * @ingroup point + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Active area where the cursor can move. */ + GHOSTTY_POINT_TAG_ACTIVE = 0, + + /** Visible viewport (changes when scrolled). */ + GHOSTTY_POINT_TAG_VIEWPORT = 1, + + /** Full screen including scrollback. */ + GHOSTTY_POINT_TAG_SCREEN = 2, + + /** Scrollback history only (before active area). */ + GHOSTTY_POINT_TAG_HISTORY = 3, + GHOSTTY_POINT_TAG_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, + } GhosttyPointTag; + +/** + * Point value union. + * + * @ingroup point + */ +typedef union { + /** Coordinate (used for all tag variants). */ + GhosttyPointCoordinate coordinate; + + /** Padding for ABI compatibility. Do not use. */ + uint64_t _padding[2]; +} GhosttyPointValue; + +/** + * Tagged union for a point in the terminal grid. + * + * @ingroup point + */ +typedef struct { + GhosttyPointTag tag; + GhosttyPointValue value; +} GhosttyPoint; + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_POINT_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/render.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/render.h new file mode 100644 index 00000000000..c5b1d0d4fc2 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/render.h @@ -0,0 +1,729 @@ +/** + * @file render.h + * + * Render state for creating high performance renderers. + */ + +#ifndef GHOSTTY_VT_RENDER_H +#define GHOSTTY_VT_RENDER_H + +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup render Render State + * + * Represents the state required to render a visible screen (a viewport) + * of a terminal instance. This is stateful and optimized for repeated + * updates from a single terminal instance and only updating dirty regions + * of the screen. + * + * The key design principle of this API is that it only needs read/write + * access to the terminal instance during the update call. This allows + * the render state to minimally impact terminal IO performance and also + * allows the renderer to be safely multi-threaded (as long as a lock is + * held during the update call to ensure exclusive access to the terminal + * instance). + * + * The basic usage of this API is: + * + * 1. Create an empty render state + * 2. Update it from a terminal instance whenever you need. + * 3. Read from the render state to get the data needed to draw your frame. + * + * ## Dirty Tracking + * + * Dirty tracking is a key feature of the render state that allows renderers + * to efficiently determine what parts of the screen have changed and only + * redraw changed regions. + * + * The render state API keeps track of dirty state at two independent layers: + * a global dirty state that indicates whether the entire frame is clean, + * partially dirty, or fully dirty, and a per-row dirty state that allows + * tracking which rows in a partially dirty frame have changed. + * + * The user of the render state API is expected to unset both of these. + * The `update` call does not unset dirty state, it only updates it. + * + * An extremely important detail: setting one dirty state doesn't unset + * the other. For example, setting the global dirty state to false does not + * reset the row-level dirty flags. So, the caller of the render state API must + * be careful to manage both layers of dirty state correctly. + * + * ## Examples + * + * ### Creating and updating render state + * @snippet c-vt-render/src/main.c render-state-update + * + * ### Checking dirty state + * @snippet c-vt-render/src/main.c render-dirty-check + * + * ### Reading colors + * @snippet c-vt-render/src/main.c render-colors + * + * ### Reading cursor state + * @snippet c-vt-render/src/main.c render-cursor + * + * ### Iterating rows and cells + * @snippet c-vt-render/src/main.c render-row-iterate + * + * ### Resetting dirty state after rendering + * @snippet c-vt-render/src/main.c render-dirty-reset + * + * @{ + */ + +/** + * Dirty state of a render state after update. + * + * @ingroup render + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Not dirty at all; rendering can be skipped. */ + GHOSTTY_RENDER_STATE_DIRTY_FALSE = 0, + + /** Some rows changed; renderer can redraw incrementally. */ + GHOSTTY_RENDER_STATE_DIRTY_PARTIAL = 1, + + /** Global state changed; renderer should redraw everything. */ + GHOSTTY_RENDER_STATE_DIRTY_FULL = 2, + GHOSTTY_RENDER_STATE_DIRTY_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyRenderStateDirty; + +/** + * Visual style of the cursor. + * + * @ingroup render + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Bar cursor (DECSCUSR 5, 6). */ + GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR = 0, + + /** Block cursor (DECSCUSR 1, 2). */ + GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK = 1, + + /** Underline cursor (DECSCUSR 3, 4). */ + GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE = 2, + + /** Hollow block cursor. */ + GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW = 3, + GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyRenderStateCursorVisualStyle; + +/** + * Queryable data kinds for ghostty_render_state_get(). + * + * @ingroup render + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Invalid / sentinel value. */ + GHOSTTY_RENDER_STATE_DATA_INVALID = 0, + + /** Viewport width in cells (uint16_t). */ + GHOSTTY_RENDER_STATE_DATA_COLS = 1, + + /** Viewport height in cells (uint16_t). */ + GHOSTTY_RENDER_STATE_DATA_ROWS = 2, + + /** Current dirty state (GhosttyRenderStateDirty). */ + GHOSTTY_RENDER_STATE_DATA_DIRTY = 3, + + /** Populate a pre-allocated GhosttyRenderStateRowIterator with row data + * from the render state (GhosttyRenderStateRowIterator). Row data is + * only valid as long as the underlying render state is not updated. + * It is unsafe to use row data after updating the render state. + * */ + GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR = 4, + + /** Default/current background color (GhosttyColorRgb). */ + GHOSTTY_RENDER_STATE_DATA_COLOR_BACKGROUND = 5, + + /** Default/current foreground color (GhosttyColorRgb). */ + GHOSTTY_RENDER_STATE_DATA_COLOR_FOREGROUND = 6, + + /** Cursor color when explicitly set by terminal state (GhosttyColorRgb). + * Returns GHOSTTY_INVALID_VALUE if no explicit cursor color is set; + * use COLOR_CURSOR_HAS_VALUE to check first. */ + GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR = 7, + + /** Whether an explicit cursor color is set (bool). */ + GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR_HAS_VALUE = 8, + + /** The active 256-color palette (GhosttyColorRgb[256]). */ + GHOSTTY_RENDER_STATE_DATA_COLOR_PALETTE = 9, + + /** The visual style of the cursor (GhosttyRenderStateCursorVisualStyle). */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE = 10, + + /** Whether the cursor is visible based on terminal modes (bool). */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE = 11, + + /** Whether the cursor should blink based on terminal modes (bool). */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING = 12, + + /** Whether the cursor is at a password input field (bool). */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_PASSWORD_INPUT = 13, + + /** Whether the cursor is visible within the viewport (bool). + * If false, the cursor viewport position values are undefined. */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE = 14, + + /** Cursor viewport x position in cells (uint16_t). + * Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X = 15, + + /** Cursor viewport y position in cells (uint16_t). + * Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y = 16, + + /** Whether the cursor is on the tail of a wide character (bool). + * Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */ + GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL = 17, + GHOSTTY_RENDER_STATE_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyRenderStateData; + +/** + * Settable options for ghostty_render_state_set(). + * + * @ingroup render + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Set dirty state (GhosttyRenderStateDirty). */ + GHOSTTY_RENDER_STATE_OPTION_DIRTY = 0, + GHOSTTY_RENDER_STATE_OPTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyRenderStateOption; + +/** + * Queryable data kinds for ghostty_render_state_row_get(). + * + * @ingroup render + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Invalid / sentinel value. */ + GHOSTTY_RENDER_STATE_ROW_DATA_INVALID = 0, + + /** Whether the current row is dirty (bool). */ + GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY = 1, + + /** The raw row value (GhosttyRow). */ + GHOSTTY_RENDER_STATE_ROW_DATA_RAW = 2, + + /** Populate a pre-allocated GhosttyRenderStateRowCells with cell data for + * the current row (GhosttyRenderStateRowCells). Cell data is only + * valid as long as the underlying render state is not updated. + * It is unsafe to use cell data after updating the render state. */ + GHOSTTY_RENDER_STATE_ROW_DATA_CELLS = 3, + + /** Row-local selected cell range (GhosttyRenderStateRowSelection). */ + GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION = 4, + GHOSTTY_RENDER_STATE_ROW_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyRenderStateRowData; + +/** + * Settable options for ghostty_render_state_row_set(). + * + * @ingroup render + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Set dirty state for the current row (bool). */ + GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY = 0, + GHOSTTY_RENDER_STATE_ROW_OPTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyRenderStateRowOption; + +/** + * Row-local selection range. + * + * This struct uses the sized-struct ABI pattern. Initialize with + * GHOSTTY_INIT_SIZED(GhosttyRenderStateRowSelection) before querying + * GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION. + * + * Querying GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION returns GHOSTTY_NO_VALUE + * if the current row does not intersect the current selection. + * + * @ingroup render + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyRenderStateRowSelection). */ + size_t size; + + /** Start column of the row-local selection range, inclusive. */ + uint16_t start_x; + + /** End column of the row-local selection range, inclusive. */ + uint16_t end_x; +} GhosttyRenderStateRowSelection; + +/** + * Render-state color information. + * + * This struct uses the sized-struct ABI pattern. Initialize with + * GHOSTTY_INIT_SIZED(GhosttyRenderStateColors) before calling + * ghostty_render_state_colors_get(). + * + * Example: + * @code + * GhosttyRenderStateColors colors = GHOSTTY_INIT_SIZED(GhosttyRenderStateColors); + * GhosttyResult result = ghostty_render_state_colors_get(state, &colors); + * @endcode + * + * @ingroup render + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyRenderStateColors). */ + size_t size; + + /** The default/current background color for the render state. */ + GhosttyColorRgb background; + + /** The default/current foreground color for the render state. */ + GhosttyColorRgb foreground; + + /** The cursor color when explicitly set by terminal state. */ + GhosttyColorRgb cursor; + + /** + * True when cursor contains a valid explicit cursor color value. + * If this is false, the cursor color should be ignored; it will + * contain undefined data. + * */ + bool cursor_has_value; + + /** The active 256-color palette for this render state. */ + GhosttyColorRgb palette[256]; +} GhosttyRenderStateColors; + +/** + * Create a new render state instance. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param state Pointer to store the created render state handle + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation + * failure + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_new(const GhosttyAllocator* allocator, + GhosttyRenderState* state); + +/** + * Free a render state instance. + * + * Releases all resources associated with the render state. After this call, + * the render state handle becomes invalid. + * + * @param state The render state handle to free (may be NULL) + * + * @ingroup render + */ +GHOSTTY_API void ghostty_render_state_free(GhosttyRenderState state); + +/** + * Update a render state instance from a terminal. + * + * This consumes terminal/screen dirty state in the same way as the internal + * render state update path. + * + * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param terminal The terminal handle to read from (NULL returns GHOSTTY_INVALID_VALUE) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or + * `terminal` is NULL, GHOSTTY_OUT_OF_MEMORY if updating the state requires + * allocation and that allocation fails + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_update(GhosttyRenderState state, + GhosttyTerminal terminal); + +/** + * Get a value from a render state. + * + * The `out` pointer must point to a value of the type corresponding to the + * requested data kind (see GhosttyRenderStateData). + * + * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param data The data kind to query + * @param[out] out Pointer to receive the queried value + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` is + * NULL or `data` is not a recognized enum value + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_get(GhosttyRenderState state, + GhosttyRenderStateData data, + void* out); + +/** + * Get multiple data fields from a render state in a single call. + * + * Each element in the keys array specifies a data kind, and the + * corresponding element in the values array receives the result. + * + * Processing stops at the first error; on success out_written + * is set to count, on error it is set to the index of the + * failing key (i.e. the number of values successfully written). + * + * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param count Number of key/value pairs + * @param keys Array of data kinds to query + * @param values Array of output pointers (types must match each key's + * documented output type) + * @param[out] out_written On return, receives the number of values + * successfully written (may be NULL) + * @return GHOSTTY_SUCCESS if all queries succeed + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_get_multi( + GhosttyRenderState state, + size_t count, + const GhosttyRenderStateData* keys, + void** values, + size_t* out_written); + +/** + * Set an option on a render state. + * + * The `value` pointer must point to a value of the type corresponding to the + * requested option kind (see GhosttyRenderStateOption). + * + * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param option The option to set + * @param[in] value Pointer to the value to set (NULL returns + * GHOSTTY_INVALID_VALUE) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or + * `value` is NULL + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_set(GhosttyRenderState state, + GhosttyRenderStateOption option, + const void* value); + +/** + * Get the current color information from a render state. + * + * This writes as many fields as fit in the caller-provided sized struct. + * `out_colors->size` must be set by the caller (typically via + * GHOSTTY_INIT_SIZED(GhosttyRenderStateColors)). + * + * @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param[out] out_colors Sized output struct to receive render-state colors + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or + * `out_colors` is NULL, or if `out_colors->size` is smaller than + * `sizeof(size_t)` + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_colors_get(GhosttyRenderState state, + GhosttyRenderStateColors* out_colors); + +/** + * Create a new row iterator instance. + * + * All fields except the allocator are left undefined until populated + * via ghostty_render_state_get() with + * GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param[out] out_iterator On success, receives the created iterator handle + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation + * failure + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_row_iterator_new( + const GhosttyAllocator* allocator, + GhosttyRenderStateRowIterator* out_iterator); + +/** + * Free a render-state row iterator. + * + * @param iterator The iterator handle to free (may be NULL) + * + * @ingroup render + */ +GHOSTTY_API void ghostty_render_state_row_iterator_free(GhosttyRenderStateRowIterator iterator); + +/** + * Move a render-state row iterator to the next row. + * + * Returns true if the iterator moved successfully and row data is + * available to read at the new position. + * + * @param iterator The iterator handle to advance (may be NULL) + * @return true if advanced to the next row, false if `iterator` is + * NULL or if the iterator has reached the end + * + * @ingroup render + */ +GHOSTTY_API bool ghostty_render_state_row_iterator_next(GhosttyRenderStateRowIterator iterator); + +/** + * Get a value from the current row in a render-state row iterator. + * + * The `out` pointer must point to a value of the type corresponding to the + * requested data kind (see GhosttyRenderStateRowData). + * Call ghostty_render_state_row_iterator_next() at least once before + * calling this function. + * + * @param iterator The iterator handle to query (NULL returns GHOSTTY_INVALID_VALUE) + * @param data The data kind to query + * @param[out] out Pointer to receive the queried value + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if + * `iterator` is NULL or the iterator is not positioned on a row + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_row_get( + GhosttyRenderStateRowIterator iterator, + GhosttyRenderStateRowData data, + void* out); + +/** + * Get multiple data fields from the current row in a single call. + * + * Each element in the keys array specifies a data kind, and the + * corresponding element in the values array receives the result. + * + * Processing stops at the first error; on success out_written + * is set to count, on error it is set to the index of the + * failing key (i.e. the number of values successfully written). + * + * @param iterator The iterator handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param count Number of key/value pairs + * @param keys Array of data kinds to query + * @param values Array of output pointers (types must match each key's + * documented output type) + * @param[out] out_written On return, receives the number of values + * successfully written (may be NULL) + * @return GHOSTTY_SUCCESS if all queries succeed + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_row_get_multi( + GhosttyRenderStateRowIterator iterator, + size_t count, + const GhosttyRenderStateRowData* keys, + void** values, + size_t* out_written); + +/** + * Set an option on the current row in a render-state row iterator. + * + * The `value` pointer must point to a value of the type corresponding to the + * requested option kind (see GhosttyRenderStateRowOption). + * Call ghostty_render_state_row_iterator_next() at least once before + * calling this function. + * + * @param iterator The iterator handle to update (NULL returns GHOSTTY_INVALID_VALUE) + * @param option The option to set + * @param[in] value Pointer to the value to set (NULL returns + * GHOSTTY_INVALID_VALUE) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if + * `iterator` is NULL or the iterator is not positioned on a row + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_row_set( + GhosttyRenderStateRowIterator iterator, + GhosttyRenderStateRowOption option, + const void* value); + +/** + * Create a new row cells instance. + * + * All fields except the allocator are left undefined until populated + * via ghostty_render_state_row_get() with + * GHOSTTY_RENDER_STATE_ROW_DATA_CELLS. + * + * You can reuse this value repeatedly with ghostty_render_state_row_get() to + * avoid allocating a new cells container for every row. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param[out] out_cells On success, receives the created row cells handle + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation + * failure + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_new( + const GhosttyAllocator* allocator, + GhosttyRenderStateRowCells* out_cells); + +/** + * Queryable data kinds for ghostty_render_state_row_cells_get(). + * + * @ingroup render + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Invalid / sentinel value. */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_INVALID = 0, + + /** The raw cell value (GhosttyCell). */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW = 1, + + /** The style for the current cell (GhosttyStyle). */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE = 2, + + /** The total number of grapheme codepoints including the base codepoint + * (uint32_t). Returns 0 if the cell has no text. */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN = 3, + + /** Write grapheme codepoints into a caller-provided buffer (uint32_t*). + * The buffer must be at least graphemes_len elements. The base codepoint + * is written first, followed by any extra codepoints. */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF = 4, + + /** The resolved background color of the cell (GhosttyColorRgb). + * Flattens the three possible sources: content-tag bg_color_rgb, + * content-tag bg_color_palette (looked up in the palette), or the + * style's bg_color. Returns GHOSTTY_INVALID_VALUE if the cell has + * no background color, in which case the caller should use whatever + * default background color it wants (e.g. the terminal background). */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR = 5, + + /** The resolved foreground color of the cell (GhosttyColorRgb). + * Resolves palette indices through the palette. Bold color handling + * is not applied; the caller should handle bold styling separately. + * Returns GHOSTTY_INVALID_VALUE if the cell has no explicit foreground + * color, in which case the caller should use whatever default foreground + * color it wants (e.g. the terminal foreground). */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR = 6, + + /** Whether the cell is contained within the current selection (bool). + * This returns true when the cell's column is within the current row's + * row-local selection range, and false otherwise. Rendering policy for + * selected cells (colors, inversion, etc.) is left to the caller. + * + * Renderers that can draw cells in spans may be more efficient querying + * GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION once per row and applying that + * range directly, avoiding one C API call per cell for selection state. */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_SELECTED = 7, + + /** Whether the cell has any explicit styling (bool). + * This is equivalent to querying the raw cell's + * GHOSTTY_CELL_DATA_HAS_STYLING value, but avoids materializing the raw + * GhosttyCell for renderers that only need to know whether fetching the + * full style is necessary. */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_HAS_STYLING = 8, + + /** + * Encode the current cell's full grapheme cluster as UTF-8 into a + * caller-provided buffer (GhosttyBuffer). + * + * The base codepoint is encoded first, followed by any extra grapheme + * codepoints. Returns GHOSTTY_SUCCESS with len=0 when the cell has no text. + * + * If ptr is NULL or cap is too small for a non-empty cell, returns + * GHOSTTY_OUT_OF_SPACE without writing any bytes and sets len to the required + * buffer size in bytes. + */ + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8 = 9, + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyRenderStateRowCellsData; + +/** + * Move a render-state row cells iterator to the next cell. + * + * Returns true if the iterator moved successfully and cell data is + * available to read at the new position. + * + * @param cells The row cells handle to advance (may be NULL) + * @return true if advanced to the next cell, false if `cells` is + * NULL or if the iterator has reached the end + * + * @ingroup render + */ +GHOSTTY_API bool ghostty_render_state_row_cells_next(GhosttyRenderStateRowCells cells); + +/** + * Move a render-state row cells iterator to a specific column. + * + * Positions the iterator at the given x (column) index so that + * subsequent reads return data for that cell. + * + * @param cells The row cells handle to reposition (NULL returns + * GHOSTTY_INVALID_VALUE) + * @param x The zero-based column index to select + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `cells` + * is NULL or `x` is out of range + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_select( + GhosttyRenderStateRowCells cells, uint16_t x); + +/** + * Get a value from the current cell in a render-state row cells iterator. + * + * The `out` pointer must point to a value of the type corresponding to the + * requested data kind (see GhosttyRenderStateRowCellsData). + * Call ghostty_render_state_row_cells_next() or + * ghostty_render_state_row_cells_select() at least once before + * calling this function. + * + * @param cells The row cells handle to query (NULL returns GHOSTTY_INVALID_VALUE) + * @param data The data kind to query + * @param[out] out Pointer to receive the queried value + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if + * `cells` is NULL or the iterator is not positioned on a cell + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_get( + GhosttyRenderStateRowCells cells, + GhosttyRenderStateRowCellsData data, + void* out); + +/** + * Get multiple data fields from the current cell in a single call. + * + * Each element in the keys array specifies a data kind, and the + * corresponding element in the values array receives the result. + * + * Processing stops at the first error; on success out_written + * is set to count, on error it is set to the index of the + * failing key (i.e. the number of values successfully written). + * + * @param cells The row cells handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param count Number of key/value pairs + * @param keys Array of data kinds to query + * @param values Array of output pointers (types must match each key's + * documented output type) + * @param[out] out_written On return, receives the number of values + * successfully written (may be NULL) + * @return GHOSTTY_SUCCESS if all queries succeed + * + * @ingroup render + */ +GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_get_multi( + GhosttyRenderStateRowCells cells, + size_t count, + const GhosttyRenderStateRowCellsData* keys, + void** values, + size_t* out_written); + +/** + * Free a row cells instance. + * + * @param cells The row cells handle to free (may be NULL) + * + * @ingroup render + */ +GHOSTTY_API void ghostty_render_state_row_cells_free(GhosttyRenderStateRowCells cells); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_RENDER_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/screen.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/screen.h new file mode 100644 index 00000000000..9f639b58313 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/screen.h @@ -0,0 +1,400 @@ +/** + * @file screen.h + * + * Terminal screen cell and row types. + */ + +#ifndef GHOSTTY_VT_SCREEN_H +#define GHOSTTY_VT_SCREEN_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup screen Screen + * + * Terminal screen cell and row types. + * + * These types represent the contents of a terminal screen. A GhosttyCell + * is a single grid cell and a GhosttyRow is a single row. Both are opaque + * values whose fields are accessed via ghostty_cell_get() and + * ghostty_row_get() respectively. + * + * @{ + */ + +/** + * Opaque cell value. + * + * Represents a single terminal cell. The internal layout is opaque and + * must be queried via ghostty_cell_get(). Obtain cell values from + * terminal query APIs. + * + * @ingroup screen + */ +typedef uint64_t GhosttyCell; + +/** + * Opaque row value. + * + * Represents a single terminal row. The internal layout is opaque and + * must be queried via ghostty_row_get(). Obtain row values from + * terminal query APIs. + * + * @ingroup screen + */ +typedef uint64_t GhosttyRow; + +/** + * Cell content tag. + * + * Describes what kind of content a cell holds. + * + * @ingroup screen + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** A single codepoint (may be zero for empty). */ + GHOSTTY_CELL_CONTENT_CODEPOINT = 0, + + /** A codepoint that is part of a multi-codepoint grapheme cluster. */ + GHOSTTY_CELL_CONTENT_CODEPOINT_GRAPHEME = 1, + + /** No text; background color from palette. */ + GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE = 2, + + /** No text; background color as RGB. */ + GHOSTTY_CELL_CONTENT_BG_COLOR_RGB = 3, + GHOSTTY_CELL_CONTENT_TAG_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyCellContentTag; + +/** + * Cell wide property. + * + * Describes the width behavior of a cell. + * + * @ingroup screen + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Not a wide character, cell width 1. */ + GHOSTTY_CELL_WIDE_NARROW = 0, + + /** Wide character, cell width 2. */ + GHOSTTY_CELL_WIDE_WIDE = 1, + + /** Spacer after wide character. Do not render. */ + GHOSTTY_CELL_WIDE_SPACER_TAIL = 2, + + /** Spacer at end of soft-wrapped line for a wide character. */ + GHOSTTY_CELL_WIDE_SPACER_HEAD = 3, + GHOSTTY_CELL_WIDE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyCellWide; + +/** + * Semantic content type of a cell. + * + * Set by semantic prompt sequences (OSC 133) to distinguish between + * command output, user input, and shell prompt text. + * + * @ingroup screen + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Regular output content, such as command output. */ + GHOSTTY_CELL_SEMANTIC_OUTPUT = 0, + + /** Content that is part of user input. */ + GHOSTTY_CELL_SEMANTIC_INPUT = 1, + + /** Content that is part of a shell prompt. */ + GHOSTTY_CELL_SEMANTIC_PROMPT = 2, + GHOSTTY_CELL_SEMANTIC_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyCellSemanticContent; + +/** + * Cell data types. + * + * These values specify what type of data to extract from a cell + * using `ghostty_cell_get`. + * + * @ingroup screen + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Invalid data type. Never results in any data extraction. */ + GHOSTTY_CELL_DATA_INVALID = 0, + + /** + * The codepoint of the cell (0 if empty or bg-color-only). + * + * Output type: uint32_t * + */ + GHOSTTY_CELL_DATA_CODEPOINT = 1, + + /** + * The content tag describing what kind of content is in the cell. + * + * Output type: GhosttyCellContentTag * + */ + GHOSTTY_CELL_DATA_CONTENT_TAG = 2, + + /** + * The wide property of the cell. + * + * Output type: GhosttyCellWide * + */ + GHOSTTY_CELL_DATA_WIDE = 3, + + /** + * Whether the cell has text to render. + * + * Output type: bool * + */ + GHOSTTY_CELL_DATA_HAS_TEXT = 4, + + /** + * Whether the cell has non-default styling. + * + * Output type: bool * + */ + GHOSTTY_CELL_DATA_HAS_STYLING = 5, + + /** + * The style ID for the cell (for use with style lookups). + * + * Output type: uint16_t * + */ + GHOSTTY_CELL_DATA_STYLE_ID = 6, + + /** + * Whether the cell has a hyperlink. + * + * Output type: bool * + */ + GHOSTTY_CELL_DATA_HAS_HYPERLINK = 7, + + /** + * Whether the cell is protected. + * + * Output type: bool * + */ + GHOSTTY_CELL_DATA_PROTECTED = 8, + + /** + * The semantic content type of the cell (from OSC 133). + * + * Output type: GhosttyCellSemanticContent * + */ + GHOSTTY_CELL_DATA_SEMANTIC_CONTENT = 9, + + /** + * The palette index for the cell's background color. + * Only valid when content_tag is GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE. + * + * Output type: GhosttyColorPaletteIndex * + */ + GHOSTTY_CELL_DATA_COLOR_PALETTE = 10, + + /** + * The RGB value for the cell's background color. + * Only valid when content_tag is GHOSTTY_CELL_CONTENT_BG_COLOR_RGB. + * + * Output type: GhosttyColorRgb * + */ + GHOSTTY_CELL_DATA_COLOR_RGB = 11, + GHOSTTY_CELL_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyCellData; + +/** + * Row semantic prompt state. + * + * Indicates whether any cells in a row are part of a shell prompt, + * as reported by OSC 133 sequences. + * + * @ingroup screen + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** No prompt cells in this row. */ + GHOSTTY_ROW_SEMANTIC_NONE = 0, + + /** Prompt cells exist and this is a primary prompt line. */ + GHOSTTY_ROW_SEMANTIC_PROMPT = 1, + + /** Prompt cells exist and this is a continuation line. */ + GHOSTTY_ROW_SEMANTIC_PROMPT_CONTINUATION = 2, + GHOSTTY_ROW_SEMANTIC_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyRowSemanticPrompt; + +/** + * Row data types. + * + * These values specify what type of data to extract from a row + * using `ghostty_row_get`. + * + * @ingroup screen + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Invalid data type. Never results in any data extraction. */ + GHOSTTY_ROW_DATA_INVALID = 0, + + /** + * Whether this row is soft-wrapped. + * + * Output type: bool * + */ + GHOSTTY_ROW_DATA_WRAP = 1, + + /** + * Whether this row is a continuation of a soft-wrapped row. + * + * Output type: bool * + */ + GHOSTTY_ROW_DATA_WRAP_CONTINUATION = 2, + + /** + * Whether any cells in this row have grapheme clusters. + * + * Output type: bool * + */ + GHOSTTY_ROW_DATA_GRAPHEME = 3, + + /** + * Whether any cells in this row have styling (may have false positives). + * + * Output type: bool * + */ + GHOSTTY_ROW_DATA_STYLED = 4, + + /** + * Whether any cells in this row have hyperlinks (may have false positives). + * + * Output type: bool * + */ + GHOSTTY_ROW_DATA_HYPERLINK = 5, + + /** + * The semantic prompt state of this row. + * + * Output type: GhosttyRowSemanticPrompt * + */ + GHOSTTY_ROW_DATA_SEMANTIC_PROMPT = 6, + + /** + * Whether this row contains a Kitty virtual placeholder. + * + * Output type: bool * + */ + GHOSTTY_ROW_DATA_KITTY_VIRTUAL_PLACEHOLDER = 7, + + /** + * Whether this row is dirty and requires a redraw. + * + * Output type: bool * + */ + GHOSTTY_ROW_DATA_DIRTY = 8, + GHOSTTY_ROW_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyRowData; + +/** + * Get data from a cell. + * + * Extracts typed data from the given cell based on the specified + * data type. The output pointer must be of the appropriate type for the + * requested data kind. Valid data types and output types are documented + * in the `GhosttyCellData` enum. + * + * @param cell The cell value + * @param data The type of data to extract + * @param out Pointer to store the extracted data (type depends on data parameter) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the + * data type is invalid + * + * @ingroup screen + */ +GHOSTTY_API GhosttyResult ghostty_cell_get(GhosttyCell cell, + GhosttyCellData data, + void *out); + +/** + * Get multiple data fields from a cell in a single call. + * + * Each element in the keys array specifies a data kind, and the + * corresponding element in the values array receives the result. + * + * Processing stops at the first error; on success out_written + * is set to count, on error it is set to the index of the + * failing key (i.e. the number of values successfully written). + * + * @param cell The cell value + * @param count Number of key/value pairs + * @param keys Array of data kinds to query + * @param values Array of output pointers (types must match each key's + * documented output type) + * @param[out] out_written On return, receives the number of values + * successfully written (may be NULL) + * @return GHOSTTY_SUCCESS if all queries succeed + * + * @ingroup screen + */ +GHOSTTY_API GhosttyResult ghostty_cell_get_multi(GhosttyCell cell, + size_t count, + const GhosttyCellData* keys, + void** values, + size_t* out_written); + +/** + * Get data from a row. + * + * Extracts typed data from the given row based on the specified + * data type. The output pointer must be of the appropriate type for the + * requested data kind. Valid data types and output types are documented + * in the `GhosttyRowData` enum. + * + * @param row The row value + * @param data The type of data to extract + * @param out Pointer to store the extracted data (type depends on data parameter) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the + * data type is invalid + * + * @ingroup screen + */ +GHOSTTY_API GhosttyResult ghostty_row_get(GhosttyRow row, + GhosttyRowData data, + void *out); + +/** + * Get multiple data fields from a row in a single call. + * + * Each element in the keys array specifies a data kind, and the + * corresponding element in the values array receives the result. + * + * Processing stops at the first error; on success out_written + * is set to count, on error it is set to the index of the + * failing key (i.e. the number of values successfully written). + * + * @param row The row value + * @param count Number of key/value pairs + * @param keys Array of data kinds to query + * @param values Array of output pointers (types must match each key's + * documented output type) + * @param[out] out_written On return, receives the number of values + * successfully written (may be NULL) + * @return GHOSTTY_SUCCESS if all queries succeed + * + * @ingroup screen + */ +GHOSTTY_API GhosttyResult ghostty_row_get_multi(GhosttyRow row, + size_t count, + const GhosttyRowData* keys, + void** values, + size_t* out_written); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_SCREEN_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/selection.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/selection.h new file mode 100644 index 00000000000..3b926aab628 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/selection.h @@ -0,0 +1,1061 @@ +/** + * @file selection.h + * + * Selection range type for specifying a region of terminal content. + */ + +#ifndef GHOSTTY_VT_SELECTION_H +#define GHOSTTY_VT_SELECTION_H + +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup selection Selection + * + * A snapshot selection range defined by two grid references that identifies + * a contiguous or rectangular region of terminal content. + * + * The start and end values are GhosttyGridRef values. They are therefore + * untracked grid references and inherit the same lifetime rules: they are + * only safe to use until the next mutating operation on the terminal that + * produced them, including freeing the terminal. To keep a selection valid + * across terminal mutations, callers must maintain tracked grid references + * for the endpoints and reconstruct a GhosttySelection from fresh snapshots + * when needed. + * + * Selection gestures provide a reusable state machine for turning UI pointer + * interactions into selection snapshots. A caller creates one + * GhosttySelectionGesture per active gesture stream, reuses typed + * GhosttySelectionGestureEvent objects for synthetic press, drag, release, + * autoscroll tick, and deep-press events, and applies each event with + * ghostty_selection_gesture_event(). The returned GhosttySelection is a + * snapshot; the embedder decides whether to render it, format/copy it, or + * install it as the terminal's active selection. + * + * ## Examples + * + * @snippet c-vt-selection/src/main.c selection-main + * @snippet c-vt-selection-gesture/src/main.c selection-gesture-main + * + * @{ + */ + +/** + * Opaque handle to state for interpreting terminal selection gestures. + * + * The gesture owns only the state required to interpret pointer events. Calls + * that use a gesture are not concurrency-safe and must be serialized with + * terminal mutations. + * + * @ingroup selection + */ +typedef struct GhosttySelectionGestureImpl* GhosttySelectionGesture; + +/** + * Opaque handle to reusable input data for selection gesture operations. + * + * Event options are set with ghostty_selection_gesture_event_set(). Individual + * gesture operations document which options are required or optional. + * + * @ingroup selection + */ +typedef struct GhosttySelectionGestureEventImpl* GhosttySelectionGestureEvent; + +/** + * A snapshot selection range defined by two grid references. + * + * Both endpoints are inclusive. The endpoints preserve selection direction + * and may be reversed; callers must not assume that start is the top-left + * endpoint or that end is the bottom-right endpoint. + * + * When rectangle is false, the endpoints describe a linear selection. When + * rectangle is true, the same endpoints are interpreted as opposite corners + * of a rectangular/block selection. + * + * The start and end values are untracked GhosttyGridRef snapshots and are + * only valid until the next mutating operation on the terminal that produced + * them unless the selection is reconstructed from tracked references. + * + * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it. + * + * @ingroup selection + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttySelection). */ + size_t size; + + /** + * Start of the selection range (inclusive). + * + * This may be after end in terminal order. It is an untracked + * GhosttyGridRef snapshot and follows untracked grid-ref lifetime rules. + */ + GhosttyGridRef start; + + /** + * End of the selection range (inclusive). + * + * This may be before start in terminal order. It is an untracked + * GhosttyGridRef snapshot and follows untracked grid-ref lifetime rules. + */ + GhosttyGridRef end; + + /** + * Whether the endpoints are interpreted as a rectangular/block selection + * rather than a linear selection. + */ + bool rectangle; +} GhosttySelection; + +/** + * Options for deriving a word selection from a terminal grid reference. + * + * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it. + * If boundary_codepoints is NULL and boundary_codepoints_len is 0, Ghostty's + * default word-boundary codepoints are used. If boundary_codepoints_len is + * non-zero, boundary_codepoints must not be NULL. + * + * @ingroup selection + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectWordOptions). */ + size_t size; + + /** Grid reference under which to derive the word selection. */ + GhosttyGridRef ref; + + /** Optional word-boundary codepoints as uint32_t scalar values. */ + const uint32_t* boundary_codepoints; + + /** Number of entries in boundary_codepoints. */ + size_t boundary_codepoints_len; +} GhosttyTerminalSelectWordOptions; + +/** + * Options for deriving the nearest word selection between two grid references. + * + * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it. + * If boundary_codepoints is NULL and boundary_codepoints_len is 0, Ghostty's + * default word-boundary codepoints are used. If boundary_codepoints_len is + * non-zero, boundary_codepoints must not be NULL. + * + * @ingroup selection + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectWordBetweenOptions). */ + size_t size; + + /** Starting grid reference for the inclusive search range. */ + GhosttyGridRef start; + + /** Ending grid reference for the inclusive search range. */ + GhosttyGridRef end; + + /** Optional word-boundary codepoints as uint32_t scalar values. */ + const uint32_t* boundary_codepoints; + + /** Number of entries in boundary_codepoints. */ + size_t boundary_codepoints_len; +} GhosttyTerminalSelectWordBetweenOptions; + +/** + * Options for deriving a line selection from a terminal grid reference. + * + * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it. + * If whitespace is NULL and whitespace_len is 0, Ghostty's default line-trim + * whitespace codepoints are used. If whitespace_len is non-zero, whitespace + * must not be NULL. + * + * @ingroup selection + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectLineOptions). */ + size_t size; + + /** Grid reference under which to derive the line selection. */ + GhosttyGridRef ref; + + /** Optional codepoints to trim from the start and end of the line. */ + const uint32_t* whitespace; + + /** Number of entries in whitespace. */ + size_t whitespace_len; + + /** Whether semantic prompt state changes should bound the line selection. */ + bool semantic_prompt_boundary; +} GhosttyTerminalSelectLineOptions; + +/** + * Options for one-shot formatting of a terminal selection. + * + * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it. + * + * If selection is NULL, the terminal's current active selection is used. + * If selection is non-NULL, that caller-provided snapshot selection is used. + * + * The selection is formatted from the terminal's active screen using the same + * formatting semantics as GhosttyFormatter. For copy/clipboard behavior + * matching Ghostty's Screen.selectionString(), use plain output with unwrap + * and trim both set to true. + * + * @ingroup selection + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectionFormatOptions). */ + size_t size; + + /** Output format to emit. */ + GhosttyFormatterFormat emit; + + /** Whether to unwrap soft-wrapped lines. */ + bool unwrap; + + /** Whether to trim trailing whitespace on non-blank lines. */ + bool trim; + + /** + * Optional selection to format. + * + * If NULL, the terminal's current active selection is used. If the terminal + * has no active selection, formatting returns GHOSTTY_NO_VALUE. + * + * If non-NULL, the pointed-to selection must be a valid snapshot selection + * for this terminal and must obey GhosttySelection lifetime rules. + */ + const GhosttySelection *selection; +} GhosttyTerminalSelectionFormatOptions; + +/** + * Ordering of a selection's endpoints in terminal coordinates. + * + * Mirrored orders are only produced by rectangular selections whose start + * and end endpoints are on opposite diagonal corners that are not simple + * top-left-to-bottom-right or bottom-right-to-top-left orderings. + * + * @ingroup selection + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Start is before end in top-left to bottom-right order. */ + GHOSTTY_SELECTION_ORDER_FORWARD = 0, + + /** End is before start in top-left to bottom-right order. */ + GHOSTTY_SELECTION_ORDER_REVERSE = 1, + + /** Rectangular selection from top-right to bottom-left. */ + GHOSTTY_SELECTION_ORDER_MIRRORED_FORWARD = 2, + + /** Rectangular selection from bottom-left to top-right. */ + GHOSTTY_SELECTION_ORDER_MIRRORED_REVERSE = 3, + + GHOSTTY_SELECTION_ORDER_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySelectionOrder; + +/** + * Operation used to adjust a selection endpoint. + * + * Adjustment mutates the selection's logical end endpoint, not whichever + * endpoint is visually bottom/right. This preserves keyboard and drag + * behavior for both forward and reversed selections. + * + * @ingroup selection + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Move left to the previous non-empty cell, wrapping upward. */ + GHOSTTY_SELECTION_ADJUST_LEFT = 0, + + /** Move right to the next non-empty cell, wrapping downward. */ + GHOSTTY_SELECTION_ADJUST_RIGHT = 1, + + /** + * Move up one row at the current column, or to the beginning of the + * line if already at the top. + */ + GHOSTTY_SELECTION_ADJUST_UP = 2, + + /** + * Move down to the next non-blank row at the current column, or to the + * end of the line if none exists. + */ + GHOSTTY_SELECTION_ADJUST_DOWN = 3, + + /** Move to the top-left cell of the screen. */ + GHOSTTY_SELECTION_ADJUST_HOME = 4, + + /** Move to the right edge of the last non-blank row on the screen. */ + GHOSTTY_SELECTION_ADJUST_END = 5, + + /** + * Move up by one terminal page height, or to home if that would move + * past the top. + */ + GHOSTTY_SELECTION_ADJUST_PAGE_UP = 6, + + /** + * Move down by one terminal page height, or to end if that would move + * past the bottom. + */ + GHOSTTY_SELECTION_ADJUST_PAGE_DOWN = 7, + + /** Move to the left edge of the current line. */ + GHOSTTY_SELECTION_ADJUST_BEGINNING_OF_LINE = 8, + + /** Move to the right edge of the current line. */ + GHOSTTY_SELECTION_ADJUST_END_OF_LINE = 9, + + GHOSTTY_SELECTION_ADJUST_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySelectionAdjust; + +/** + * Selection behavior chosen for a gesture's click sequence. + * + * @ingroup selection + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Cell-granular drag selection. */ + GHOSTTY_SELECTION_GESTURE_BEHAVIOR_CELL = 0, + + /** Word selection on press and word-granular drag selection. */ + GHOSTTY_SELECTION_GESTURE_BEHAVIOR_WORD = 1, + + /** Line selection on press and line-granular drag selection. */ + GHOSTTY_SELECTION_GESTURE_BEHAVIOR_LINE = 2, + + /** Semantic command output selection on press and drag. */ + GHOSTTY_SELECTION_GESTURE_BEHAVIOR_OUTPUT = 3, + + GHOSTTY_SELECTION_GESTURE_BEHAVIOR_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySelectionGestureBehavior; + +/** + * Selection behaviors for single-, double-, and triple-click gestures. + * + * @ingroup selection + */ +typedef struct { + /** Behavior for single-click selection gestures. */ + GhosttySelectionGestureBehavior single_click; + + /** Behavior for double-click selection gestures. */ + GhosttySelectionGestureBehavior double_click; + + /** Behavior for triple-click selection gestures. */ + GhosttySelectionGestureBehavior triple_click; +} GhosttySelectionGestureBehaviors; + +/** + * Display geometry used to interpret selection gesture drag events. + * + * @ingroup selection + */ +typedef struct { + /** Number of columns in the rendered terminal grid. Must be non-zero. */ + uint32_t columns; + + /** Width of one terminal cell in surface pixels. Must be non-zero. */ + uint32_t cell_width; + + /** Left padding before the terminal grid begins in surface pixels. */ + uint32_t padding_left; + + /** Height of the rendered terminal surface in surface pixels. Must be non-zero. */ + uint32_t screen_height; +} GhosttySelectionGestureGeometry; + +/** + * Current autoscroll direction for an active selection drag gesture. + * + * @ingroup selection + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** No selection autoscroll is requested. */ + GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_NONE = 0, + + /** Selection dragging should autoscroll the viewport upward. */ + GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_UP = 1, + + /** Selection dragging should autoscroll the viewport downward. */ + GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_DOWN = 2, + + GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySelectionGestureAutoscroll; + +/** + * Data fields readable from a selection gesture with + * ghostty_selection_gesture_get(). + * + * @ingroup selection + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Current click count: uint8_t*. 0 means inactive. */ + GHOSTTY_SELECTION_GESTURE_DATA_CLICK_COUNT = 0, + + /** Whether the current/last left-click gesture has dragged: bool*. */ + GHOSTTY_SELECTION_GESTURE_DATA_DRAGGED = 1, + + /** Current autoscroll request: GhosttySelectionGestureAutoscroll*. */ + GHOSTTY_SELECTION_GESTURE_DATA_AUTOSCROLL = 2, + + /** Current gesture behavior: GhosttySelectionGestureBehavior*. */ + GHOSTTY_SELECTION_GESTURE_DATA_BEHAVIOR = 3, + + /** + * Current left-click anchor: GhosttyGridRef*. + * + * Returns GHOSTTY_NO_VALUE if there is no valid active anchor. On success, + * writes an untracked GhosttyGridRef snapshot with normal GhosttyGridRef + * lifetime rules. + */ + GHOSTTY_SELECTION_GESTURE_DATA_ANCHOR = 4, + + GHOSTTY_SELECTION_GESTURE_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySelectionGestureData; + +/** + * Selection gesture event type. + * + * The event type is fixed when the event is created. Each event type documents + * which options are valid and which options are required by gesture operations. + * + * @ingroup selection + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Press event for ghostty_selection_gesture_event(). */ + GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_PRESS = 0, + + /** Release event for ghostty_selection_gesture_event(). */ + GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_RELEASE = 1, + + /** Drag event for ghostty_selection_gesture_event(). */ + GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DRAG = 2, + + /** Autoscroll tick event for ghostty_selection_gesture_event(). */ + GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_AUTOSCROLL_TICK = 3, + + /** Deep press event for ghostty_selection_gesture_event(). */ + GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DEEP_PRESS = 4, + + GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySelectionGestureEventType; + +/** + * Options stored on a reusable selection gesture event. + * + * Passing NULL as the value to ghostty_selection_gesture_event_set() clears the + * corresponding option. + * + * @ingroup selection + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** + * Grid reference under the pointer: GhosttyGridRef*. + * + * Required for PRESS and DRAG events. Optional for RELEASE events; when unset + * or cleared, release records that the pointer did not map to a valid cell. + */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF = 0, + + /** + * Surface-space pointer position: GhosttySurfacePosition*. + * + * Valid for PRESS, DRAG, and AUTOSCROLL_TICK. + */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_POSITION = 1, + + /** Maximum repeat-click distance in pixels: double*. */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REPEAT_DISTANCE = 2, + + /** + * Optional monotonic event time in nanoseconds: uint64_t*. + * + * If unset, press treats the event as untimed and only single-click behavior + * is available. + */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_TIME_NS = 3, + + /** Maximum interval between repeat clicks in nanoseconds: uint64_t*. */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REPEAT_INTERVAL_NS = 4, + + /** + * Word-boundary codepoints: GhosttyCodepoints*. + * + * The codepoints are copied into event-owned storage when set. If unset, + * operations that need word boundaries use Ghostty's defaults. + * + * Valid for PRESS, DRAG, AUTOSCROLL_TICK, and DEEP_PRESS. + */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_WORD_BOUNDARY_CODEPOINTS = 5, + + /** + * Selection behavior table: GhosttySelectionGestureBehaviors*. + * + * If unset, press uses the default behavior table: cell, word, line. + */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_BEHAVIORS = 6, + + /** Whether a drag or autoscroll tick should produce a rectangular selection: bool*. */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_RECTANGLE = 7, + + /** Drag display geometry: GhosttySelectionGestureGeometry*. Required for DRAG and AUTOSCROLL_TICK. */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_GEOMETRY = 8, + + /** Viewport coordinate for an autoscroll tick: GhosttyPointCoordinate*. Required for AUTOSCROLL_TICK. */ + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_VIEWPORT = 9, + + GHOSTTY_SELECTION_GESTURE_EVENT_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySelectionGestureEventOption; + +/** + * Create a reusable selection gesture event object. + * + * @param allocator Allocator, or NULL for the default allocator + * @param out_event Receives the created event handle + * @param type Event type. This is fixed for the lifetime of the event. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if out_event is + * NULL or type is invalid, or GHOSTTY_OUT_OF_MEMORY if allocation fails + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_selection_gesture_event_new( + const GhosttyAllocator* allocator, + GhosttySelectionGestureEvent* out_event, + GhosttySelectionGestureEventType type); + +/** + * Free a selection gesture event object. + * + * Passing NULL is allowed and is a no-op. + * + * @param event Selection gesture event handle to free + * + * @ingroup selection + */ +GHOSTTY_API void ghostty_selection_gesture_event_free( + GhosttySelectionGestureEvent event); + +/** + * Set or clear an option on a selection gesture event. + * + * The value type depends on option and is documented by + * GhosttySelectionGestureEventOption. Passing NULL for value clears the option. + * + * @param event Selection gesture event handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param option Event option to set or clear + * @param value Pointer to the input value for option, or NULL to clear + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if copying + * event-owned data fails, or GHOSTTY_INVALID_VALUE if event, option, or + * value is invalid + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_selection_gesture_event_set( + GhosttySelectionGestureEvent event, + GhosttySelectionGestureEventOption option, + const void* value); + +/** + * Apply a selection gesture event and return the resulting selection snapshot. + * + * This dispatches to the gesture operation matching the event's fixed type. + * For GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_PRESS, the event must have + * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF set before calling this function. + * All other press options use their initialized defaults when unset or cleared. + * + * For GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_RELEASE, only + * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF is valid. It is optional; if unset or + * cleared, release records that the pointer did not map to a valid cell. Release + * events update gesture state but do not produce a selection, so this function + * returns GHOSTTY_NO_VALUE after applying them. + * + * For GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DRAG, + * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF and + * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_GEOMETRY are required. Position, + * rectangle, and word-boundary codepoints are optional and use initialized + * defaults when unset or cleared. + * + * For GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_AUTOSCROLL_TICK, + * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_VIEWPORT and + * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_GEOMETRY are required. Position, + * rectangle, and word-boundary codepoints are optional and use initialized + * defaults when unset or cleared. + * + * For GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DEEP_PRESS, only + * GHOSTTY_SELECTION_GESTURE_EVENT_OPT_WORD_BOUNDARY_CODEPOINTS is valid. It is + * optional and uses initialized defaults when unset or cleared. + * + * The returned selection is not installed as the terminal's current selection. + * It is a snapshot with the same lifetime rules as GhosttySelection. + * + * @param gesture Selection gesture handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param terminal Terminal used to interpret and update gesture state + * @param event Selection gesture event handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param[out] out_selection On success, receives the resulting selection. May + * be NULL to apply the event and discard the selection result. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the event does not + * currently produce a selection, GHOSTTY_OUT_OF_MEMORY if tracking + * gesture state fails, or GHOSTTY_INVALID_VALUE if gesture, terminal, + * event, or required event data is invalid + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_selection_gesture_event( + GhosttySelectionGesture gesture, + GhosttyTerminal terminal, + GhosttySelectionGestureEvent event, + GhosttySelection* out_selection); + +/** + * Create a selection gesture object. + * + * The gesture stores mutable state for terminal text selection gestures. The + * gesture is not bound to a terminal at creation time; terminal-dependent APIs + * take the terminal explicitly. + * + * @param allocator Allocator, or NULL for the default allocator + * @param out_gesture Receives the created gesture handle + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if out_gesture is + * NULL, or GHOSTTY_OUT_OF_MEMORY if allocation fails + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_selection_gesture_new( + const GhosttyAllocator* allocator, + GhosttySelectionGesture* out_gesture); + +/** + * Free a selection gesture object. + * + * This releases any tracked terminal references owned by the gesture using the + * provided terminal, then frees the gesture object. Passing NULL for gesture is + * allowed and is a no-op. + * + * If the terminal is still alive, pass the terminal most recently used with the + * gesture so any tracked terminal references can be released correctly. If the + * terminal has already been freed, pass NULL for terminal; the terminal's page + * storage has already released the underlying tracked references, so the + * gesture wrapper can be safely discarded without touching the stale terminal + * state. + * + * @param gesture Selection gesture handle to free + * @param terminal Terminal used to release tracked gesture state, or NULL if + * the terminal has already been freed + * + * @ingroup selection + */ +GHOSTTY_API void ghostty_selection_gesture_free( + GhosttySelectionGesture gesture, + GhosttyTerminal terminal); + +/** + * Reset any active selection gesture state. + * + * This cancels the active click sequence and releases any tracked terminal + * references owned by the gesture without freeing the gesture object. + * Passing NULL is allowed and is a no-op. + * + * @param gesture Selection gesture handle to reset + * @param terminal Terminal used to release tracked gesture state + * + * @ingroup selection + */ +GHOSTTY_API void ghostty_selection_gesture_reset( + GhosttySelectionGesture gesture, + GhosttyTerminal terminal); + +/** + * Read data from a selection gesture. + * + * The type of value depends on data and is documented by + * GhosttySelectionGestureData. For GHOSTTY_SELECTION_GESTURE_DATA_ANCHOR, + * the returned GhosttyGridRef is an untracked snapshot with normal grid-ref + * lifetime rules. + * + * @param gesture Selection gesture handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param terminal Terminal used to validate terminal-backed gesture state + * @param data Data field to read + * @param value Output pointer whose type depends on data + * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the requested data + * has no value, or GHOSTTY_INVALID_VALUE if gesture, terminal, data, or + * value is invalid + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_selection_gesture_get( + GhosttySelectionGesture gesture, + GhosttyTerminal terminal, + GhosttySelectionGestureData data, + void* value); + +/** + * Read multiple data fields from a selection gesture in a single call. + * + * This is an optimization over calling ghostty_selection_gesture_get() multiple + * times. Each entry in values must point to storage of the type documented by + * the corresponding GhosttySelectionGestureData key. + * + * If any individual read fails, the function returns that error and writes the + * index of the failing key to out_written when out_written is non-NULL. On + * success, out_written receives count when non-NULL. + * + * @param gesture Selection gesture handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param terminal Terminal used to validate terminal-backed gesture state + * @param count Number of data fields to read + * @param keys Data fields to read (must not be NULL) + * @param values Output pointers corresponding to keys (must not be NULL) + * @param out_written Optional number of fields read, or failing index on error + * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if a requested data + * field has no value, or GHOSTTY_INVALID_VALUE if gesture, terminal, + * keys, values, or a value pointer is invalid + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_selection_gesture_get_multi( + GhosttySelectionGesture gesture, + GhosttyTerminal terminal, + size_t count, + const GhosttySelectionGestureData* keys, + void** values, + size_t* out_written); + +/** + * Derive a word selection snapshot from a terminal grid reference. + * + * The returned selection is not installed as the terminal's current + * selection. It is a snapshot with the same lifetime rules as GhosttySelection. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param options Word-selection options + * @param[out] out_selection On success, receives the derived selection + * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the valid ref has + * no selectable word content, or GHOSTTY_INVALID_VALUE if the + * terminal, options, ref, codepoint pointer, or output pointer are + * invalid. + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_terminal_select_word( + GhosttyTerminal terminal, + const GhosttyTerminalSelectWordOptions* options, + GhosttySelection* out_selection); + +/** + * Derive the nearest word selection snapshot between two terminal grid refs. + * + * Starting at options->start, this searches toward options->end (inclusive) + * and returns the first selectable word found using Ghostty's word-selection + * rules. + * + * This is useful for implementing double-click-and-drag selection in a UI. If + * a user double-clicks one word and drags across spaces or punctuation toward + * another word, selecting only the word directly under the current pointer can + * flicker or collapse when the pointer is between words. Instead, ask for the + * nearest word between the original click and the drag point, ask again in the + * reverse direction, and combine the two word bounds into the drag selection. + * + * @snippet c-vt-selection/src/main.c selection-word-between + * + * The returned selection is not installed as the terminal's current + * selection. It is a snapshot with the same lifetime rules as GhosttySelection. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param options Word-between-selection options + * @param[out] out_selection On success, receives the derived selection + * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if there is no + * selectable word content between the valid refs, or + * GHOSTTY_INVALID_VALUE if the terminal, options, refs, codepoint + * pointer, or output pointer are invalid. + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_terminal_select_word_between( + GhosttyTerminal terminal, + const GhosttyTerminalSelectWordBetweenOptions* options, + GhosttySelection* out_selection); + +/** + * Derive a line selection snapshot from a terminal grid reference. + * + * The returned selection is not installed as the terminal's current + * selection. It is a snapshot with the same lifetime rules as GhosttySelection. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param options Line-selection options + * @param[out] out_selection On success, receives the derived selection + * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the valid ref has + * no selectable line content, or GHOSTTY_INVALID_VALUE if the + * terminal, options, ref, codepoint pointer, or output pointer are + * invalid. + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_terminal_select_line( + GhosttyTerminal terminal, + const GhosttyTerminalSelectLineOptions* options, + GhosttySelection* out_selection); + +/** + * Derive a selection snapshot covering all selectable terminal content. + * + * The returned selection is not installed as the terminal's current + * selection. It is a snapshot with the same lifetime rules as GhosttySelection. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param[out] out_selection On success, receives the derived selection + * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if there is no + * selectable content, or GHOSTTY_INVALID_VALUE if the terminal or + * output pointer is invalid. + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_terminal_select_all( + GhosttyTerminal terminal, + GhosttySelection* out_selection); + +/** + * Derive a command-output selection snapshot from a terminal grid reference. + * + * The returned selection is not installed as the terminal's current + * selection. It is a snapshot with the same lifetime rules as GhosttySelection. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param ref Grid reference within command output to select + * @param[out] out_selection On success, receives the derived selection + * @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the valid ref is + * not selectable command output, or GHOSTTY_INVALID_VALUE if the + * terminal, ref, or output pointer is invalid. + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_terminal_select_output( + GhosttyTerminal terminal, + GhosttyGridRef ref, + GhosttySelection* out_selection); + +/** + * Format a terminal selection into a caller-provided buffer. + * + * This is a one-shot convenience API for formatting either the terminal's + * active selection or a caller-provided GhosttySelection without explicitly + * creating a GhosttyFormatter. + * + * Pass NULL for buf to query the required output size. In that case, + * out_written receives the required size and the function returns + * GHOSTTY_OUT_OF_SPACE. + * + * If buf is too small, the function returns GHOSTTY_OUT_OF_SPACE and writes + * the required size to out_written. The caller can then retry with a larger + * buffer. + * + * If options.selection is NULL and the terminal has no active selection, the + * function returns GHOSTTY_NO_VALUE. + * + * @param terminal The terminal to read from (must not be NULL) + * @param options Selection formatting options + * @param buf Output buffer, or NULL to query required size + * @param buf_len Length of buf in bytes + * @param out_written Number of bytes written, or required size on + * GHOSTTY_OUT_OF_SPACE (must not be NULL) + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_terminal_selection_format_buf( + GhosttyTerminal terminal, + GhosttyTerminalSelectionFormatOptions options, + uint8_t* buf, + size_t buf_len, + size_t* out_written); + +/** + * Format a terminal selection into an allocated buffer. + * + * This is a one-shot convenience API for formatting either the terminal's + * active selection or a caller-provided GhosttySelection without explicitly + * creating a GhosttyFormatter. + * + * The returned buffer is allocated using allocator, or the default allocator + * if NULL is passed. The caller owns the returned buffer and must free it with + * ghostty_free(), passing the same allocator and returned length. + * + * The returned bytes are not NUL-terminated. This supports plain text, VT, and + * HTML uniformly as byte output. + * + * If options.selection is NULL and the terminal has no active selection, the + * function returns GHOSTTY_NO_VALUE and leaves out_ptr as NULL and out_len as 0. + * + * @param terminal The terminal to read from (must not be NULL) + * @param allocator Allocator used for the returned buffer, or NULL for the default allocator + * @param options Selection formatting options + * @param out_ptr Receives the allocated output buffer (must not be NULL) + * @param out_len Receives the output length in bytes (must not be NULL) + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_terminal_selection_format_alloc( + GhosttyTerminal terminal, + const GhosttyAllocator* allocator, + GhosttyTerminalSelectionFormatOptions options, + uint8_t** out_ptr, + size_t* out_len); + +/** + * Adjust a selection snapshot using terminal selection semantics. + * + * This mutates the caller-provided GhosttySelection in place. The logical end + * endpoint is always moved, regardless of whether the selection is forward or + * reversed visually. The input selection remains a snapshot: after adjustment, + * call ghostty_terminal_set() with GHOSTTY_TERMINAL_OPT_SELECTION to install it + * as the terminal-owned selection if desired. + * + * The selection's start and end grid refs must both be valid untracked + * snapshots for the given terminal's currently active screen. In practice, + * they must come from that terminal and screen, and no mutating terminal call + * may have occurred since the refs were produced or reconstructed from + * tracked refs. Passing refs from another terminal, another screen, or stale + * refs violates this precondition. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param selection Selection snapshot to adjust in place + * @param adjustment The adjustment operation to apply + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal, + * selection, or adjustment are invalid. Selection reference validity + * is a precondition and is not checked. + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_terminal_selection_adjust( + GhosttyTerminal terminal, + GhosttySelection* selection, + GhosttySelectionAdjust adjustment); + +/** + * Get the current endpoint ordering of a selection snapshot. + * + * The selection's start and end grid refs must both be valid untracked + * snapshots for the given terminal's currently active screen. In practice, + * they must come from that terminal and screen, and no mutating terminal call + * may have occurred since the refs were produced or reconstructed from + * tracked refs. Passing refs from another terminal, another screen, or stale + * refs violates this precondition. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param selection Selection snapshot to inspect + * @param[out] out_order On success, receives the selection order + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal, + * selection, or output pointer are invalid. Selection reference + * validity is a precondition and is not checked. + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_terminal_selection_order( + GhosttyTerminal terminal, + const GhosttySelection* selection, + GhosttySelectionOrder* out_order); + +/** + * Return a selection snapshot with endpoints ordered as requested. + * + * Use GHOSTTY_SELECTION_ORDER_FORWARD to get top-left to bottom-right bounds, + * and GHOSTTY_SELECTION_ORDER_REVERSE to get bottom-right to top-left bounds. + * Mirrored desired orders are accepted but normalized the same as forward. + * The output selection is a fresh untracked snapshot and is not installed as + * the terminal's current selection. + * + * The selection's start and end grid refs must both be valid untracked + * snapshots for the given terminal's currently active screen. In practice, + * they must come from that terminal and screen, and no mutating terminal call + * may have occurred since the refs were produced or reconstructed from + * tracked refs. Passing refs from another terminal, another screen, or stale + * refs violates this precondition. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param selection Selection snapshot to order + * @param desired Desired endpoint order + * @param[out] out_selection On success, receives the ordered selection + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal, + * selection, desired order, or output pointer are invalid. Selection + * reference validity is a precondition and is not checked. + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_terminal_selection_ordered( + GhosttyTerminal terminal, + const GhosttySelection* selection, + GhosttySelectionOrder desired, + GhosttySelection* out_selection); + +/** + * Test whether a terminal point is inside a selection snapshot. + * + * This uses the same selection semantics as the terminal, including + * rectangular/block selections and linear selections spanning multiple rows. + * + * The selection's start and end grid refs must both be valid untracked + * snapshots for the given terminal's currently active screen. In practice, + * they must come from that terminal and screen, and no mutating terminal call + * may have occurred since the refs were produced or reconstructed from + * tracked refs. Passing refs from another terminal, another screen, or stale + * refs violates this precondition. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param selection Selection snapshot to inspect + * @param point Point to test for containment + * @param[out] out_contains On success, receives whether point is inside selection + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal, + * selection, point, or output pointer are invalid. Selection reference + * validity is a precondition and is not checked. + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_terminal_selection_contains( + GhosttyTerminal terminal, + const GhosttySelection* selection, + GhosttyPoint point, + bool* out_contains); + +/** + * Test whether two selection snapshots are equal. + * + * Equality uses the terminal's internal selection semantics: both endpoint + * pins must match and both selections must have the same rectangular/block + * state. This avoids requiring callers to compare raw GhosttyGridRef internals. + * + * Both selections' start and end grid refs must be valid untracked snapshots + * for the given terminal's currently active screen. In practice, they must + * come from that terminal and screen, and no mutating terminal call may have + * occurred since the refs were produced or reconstructed from tracked refs. + * Passing refs from another terminal, another screen, or stale refs violates + * this precondition. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param a First selection snapshot to compare + * @param b Second selection snapshot to compare + * @param[out] out_equal On success, receives whether the selections are equal + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal, + * selections, or output pointer are invalid. Selection reference + * validity is a precondition and is not checked. + * + * @ingroup selection + */ +GHOSTTY_API GhosttyResult ghostty_terminal_selection_equal( + GhosttyTerminal terminal, + const GhosttySelection* a, + const GhosttySelection* b, + bool* out_equal); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_SELECTION_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/sgr.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/sgr.h new file mode 100644 index 00000000000..8eec11dc970 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/sgr.h @@ -0,0 +1,350 @@ +/** + * @file sgr.h + * + * SGR (Select Graphic Rendition) attribute parsing and handling. + */ + +#ifndef GHOSTTY_VT_SGR_H +#define GHOSTTY_VT_SGR_H + +/** @defgroup sgr SGR Parser + * + * SGR (Select Graphic Rendition) attribute parser. + * + * SGR sequences are the syntax used to set styling attributes such as + * bold, italic, underline, and colors for text in terminal emulators. + * For example, you may be familiar with sequences like `ESC[1;31m`. The + * `1;31` is the SGR attribute list. + * + * The parser processes SGR parameters from CSI sequences (e.g., `ESC[1;31m`) + * and returns individual text attributes like bold, italic, colors, etc. + * It supports both semicolon (`;`) and colon (`:`) separators, possibly mixed, + * and handles various color formats including 8-color, 16-color, 256-color, + * X11 named colors, and RGB in multiple formats. + * + * ## Basic Usage + * + * 1. Create a parser instance with ghostty_sgr_new() + * 2. Set SGR parameters with ghostty_sgr_set_params() + * 3. Iterate through attributes using ghostty_sgr_next() + * 4. Free the parser with ghostty_sgr_free() when done + * + * ## Example + * + * @snippet c-vt-sgr/src/main.c sgr-basic + * + * @{ + */ + +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * SGR attribute tags. + * + * These values identify the type of an SGR attribute in a tagged union. + * Use the tag to determine which field in the attribute value union to access. + * + * @ingroup sgr + */ +typedef enum GHOSTTY_ENUM_TYPED { + GHOSTTY_SGR_ATTR_UNSET = 0, + GHOSTTY_SGR_ATTR_UNKNOWN = 1, + GHOSTTY_SGR_ATTR_BOLD = 2, + GHOSTTY_SGR_ATTR_RESET_BOLD = 3, + GHOSTTY_SGR_ATTR_ITALIC = 4, + GHOSTTY_SGR_ATTR_RESET_ITALIC = 5, + GHOSTTY_SGR_ATTR_FAINT = 6, + GHOSTTY_SGR_ATTR_UNDERLINE = 7, + GHOSTTY_SGR_ATTR_UNDERLINE_COLOR = 8, + GHOSTTY_SGR_ATTR_UNDERLINE_COLOR_256 = 9, + GHOSTTY_SGR_ATTR_RESET_UNDERLINE_COLOR = 10, + GHOSTTY_SGR_ATTR_OVERLINE = 11, + GHOSTTY_SGR_ATTR_RESET_OVERLINE = 12, + GHOSTTY_SGR_ATTR_BLINK = 13, + GHOSTTY_SGR_ATTR_RESET_BLINK = 14, + GHOSTTY_SGR_ATTR_INVERSE = 15, + GHOSTTY_SGR_ATTR_RESET_INVERSE = 16, + GHOSTTY_SGR_ATTR_INVISIBLE = 17, + GHOSTTY_SGR_ATTR_RESET_INVISIBLE = 18, + GHOSTTY_SGR_ATTR_STRIKETHROUGH = 19, + GHOSTTY_SGR_ATTR_RESET_STRIKETHROUGH = 20, + GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG = 21, + GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG = 22, + GHOSTTY_SGR_ATTR_BG_8 = 23, + GHOSTTY_SGR_ATTR_FG_8 = 24, + GHOSTTY_SGR_ATTR_RESET_FG = 25, + GHOSTTY_SGR_ATTR_RESET_BG = 26, + GHOSTTY_SGR_ATTR_BRIGHT_BG_8 = 27, + GHOSTTY_SGR_ATTR_BRIGHT_FG_8 = 28, + GHOSTTY_SGR_ATTR_BG_256 = 29, + GHOSTTY_SGR_ATTR_FG_256 = 30, + GHOSTTY_SGR_ATTR_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySgrAttributeTag; + +/** + * Underline style types. + * + * @ingroup sgr + */ +typedef enum GHOSTTY_ENUM_TYPED { + GHOSTTY_SGR_UNDERLINE_NONE = 0, + GHOSTTY_SGR_UNDERLINE_SINGLE = 1, + GHOSTTY_SGR_UNDERLINE_DOUBLE = 2, + GHOSTTY_SGR_UNDERLINE_CURLY = 3, + GHOSTTY_SGR_UNDERLINE_DOTTED = 4, + GHOSTTY_SGR_UNDERLINE_DASHED = 5, + GHOSTTY_SGR_UNDERLINE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySgrUnderline; + +/** + * Unknown SGR attribute data. + * + * Contains the full parameter list and the partial list where parsing + * encountered an unknown or invalid sequence. + * + * @ingroup sgr + */ +typedef struct { + const uint16_t* full_ptr; + size_t full_len; + const uint16_t* partial_ptr; + size_t partial_len; +} GhosttySgrUnknown; + +/** + * SGR attribute value union. + * + * This union contains all possible attribute values. Use the tag field + * to determine which union member is active. Attributes without associated + * data (like bold, italic) don't use the union value. + * + * @ingroup sgr + */ +typedef union { + GhosttySgrUnknown unknown; + GhosttySgrUnderline underline; + GhosttyColorRgb underline_color; + GhosttyColorPaletteIndex underline_color_256; + GhosttyColorRgb direct_color_fg; + GhosttyColorRgb direct_color_bg; + GhosttyColorPaletteIndex bg_8; + GhosttyColorPaletteIndex fg_8; + GhosttyColorPaletteIndex bright_bg_8; + GhosttyColorPaletteIndex bright_fg_8; + GhosttyColorPaletteIndex bg_256; + GhosttyColorPaletteIndex fg_256; + uint64_t _padding[8]; +} GhosttySgrAttributeValue; + +/** + * SGR attribute (tagged union). + * + * A complete SGR attribute with both its type tag and associated value. + * Always check the tag field to determine which value union member is valid. + * + * Attributes without associated data (e.g., GHOSTTY_SGR_ATTR_BOLD) can be + * identified by tag alone; the value union is not used for these and + * the memory in the value field is undefined. + * + * @ingroup sgr + */ +typedef struct { + GhosttySgrAttributeTag tag; + GhosttySgrAttributeValue value; +} GhosttySgrAttribute; + +/** + * Create a new SGR parser instance. + * + * Creates a new SGR (Select Graphic Rendition) parser using the provided + * allocator. The parser must be freed using ghostty_sgr_free() when + * no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or + * NULL to use the default allocator + * @param parser Pointer to store the created parser handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup sgr + */ +GHOSTTY_API GhosttyResult ghostty_sgr_new(const GhosttyAllocator* allocator, + GhosttySgrParser* parser); + +/** + * Free an SGR parser instance. + * + * Releases all resources associated with the SGR parser. After this call, + * the parser handle becomes invalid and must not be used. This includes + * any attributes previously returned by ghostty_sgr_next(). + * + * @param parser The parser handle to free (may be NULL) + * + * @ingroup sgr + */ +GHOSTTY_API void ghostty_sgr_free(GhosttySgrParser parser); + +/** + * Reset an SGR parser instance to the beginning of the parameter list. + * + * Resets the parser's iteration state without clearing the parameters. + * After calling this, ghostty_sgr_next() will start from the beginning + * of the parameter list again. + * + * @param parser The parser handle to reset, must not be NULL + * + * @ingroup sgr + */ +GHOSTTY_API void ghostty_sgr_reset(GhosttySgrParser parser); + +/** + * Set SGR parameters for parsing. + * + * Sets the SGR parameter list to parse. Parameters are the numeric values + * from a CSI SGR sequence (e.g., for `ESC[1;31m`, params would be {1, 31}). + * + * The separators array optionally specifies the separator type for each + * parameter position. Each byte should be either ';' for semicolon or ':' + * for colon. This is needed for certain color formats that use colon + * separators (e.g., `ESC[4:3m` for curly underline). Any invalid separator + * values are treated as semicolons. The separators array must have the same + * length as the params array, if it is not NULL. + * + * If separators is NULL, all parameters are assumed to be semicolon-separated. + * + * This function makes an internal copy of the parameter and separator data, + * so the caller can safely free or modify the input arrays after this call. + * + * After calling this function, the parser is automatically reset and ready + * to iterate from the beginning. + * + * @param parser The parser handle, must not be NULL + * @param params Array of SGR parameter values + * @param separators Optional array of separator characters (';' or ':'), or + * NULL + * @param len Number of parameters (and separators if provided) + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup sgr + */ +GHOSTTY_API GhosttyResult ghostty_sgr_set_params(GhosttySgrParser parser, + const uint16_t* params, + const char* separators, + size_t len); + +/** + * Get the next SGR attribute. + * + * Parses and returns the next attribute from the parameter list. + * Call this function repeatedly until it returns false to process + * all attributes in the sequence. + * + * @param parser The parser handle, must not be NULL + * @param attr Pointer to store the next attribute + * @return true if an attribute was returned, false if no more attributes + * + * @ingroup sgr + */ +GHOSTTY_API bool ghostty_sgr_next(GhosttySgrParser parser, GhosttySgrAttribute* attr); + +/** + * Get the full parameter list from an unknown SGR attribute. + * + * This function retrieves the full parameter list that was provided to the + * parser when an unknown attribute was encountered. Primarily useful in + * WebAssembly environments where accessing struct fields directly is difficult. + * + * @param unknown The unknown attribute data + * @param ptr Pointer to store the pointer to the parameter array (may be NULL) + * @return The length of the full parameter array + * + * @ingroup sgr + */ +GHOSTTY_API size_t ghostty_sgr_unknown_full(GhosttySgrUnknown unknown, + const uint16_t** ptr); + +/** + * Get the partial parameter list from an unknown SGR attribute. + * + * This function retrieves the partial parameter list where parsing stopped + * when an unknown attribute was encountered. Primarily useful in WebAssembly + * environments where accessing struct fields directly is difficult. + * + * @param unknown The unknown attribute data + * @param ptr Pointer to store the pointer to the parameter array (may be NULL) + * @return The length of the partial parameter array + * + * @ingroup sgr + */ +GHOSTTY_API size_t ghostty_sgr_unknown_partial(GhosttySgrUnknown unknown, + const uint16_t** ptr); + +/** + * Get the tag from an SGR attribute. + * + * This function extracts the tag that identifies which type of attribute + * this is. Primarily useful in WebAssembly environments where accessing + * struct fields directly is difficult. + * + * @param attr The SGR attribute + * @return The attribute tag + * + * @ingroup sgr + */ +GHOSTTY_API GhosttySgrAttributeTag ghostty_sgr_attribute_tag(GhosttySgrAttribute attr); + +/** + * Get the value from an SGR attribute. + * + * This function returns a pointer to the value union from an SGR attribute. Use + * the tag to determine which field of the union is valid. Primarily useful in + * WebAssembly environments where accessing struct fields directly is difficult. + * + * @param attr Pointer to the SGR attribute + * @return Pointer to the attribute value union + * + * @ingroup sgr + */ +GHOSTTY_API GhosttySgrAttributeValue* ghostty_sgr_attribute_value( + GhosttySgrAttribute* attr); + +#ifdef __wasm__ +/** + * Allocate memory for an SGR attribute (WebAssembly only). + * + * This is a convenience function for WebAssembly environments to allocate + * memory for an SGR attribute structure that can be passed to ghostty_sgr_next. + * + * @return Pointer to the allocated attribute structure + * + * @ingroup wasm + */ +GHOSTTY_API GhosttySgrAttribute* ghostty_wasm_alloc_sgr_attribute(void); + +/** + * Free memory for an SGR attribute (WebAssembly only). + * + * Frees memory allocated by ghostty_wasm_alloc_sgr_attribute. + * + * @param attr Pointer to the attribute structure to free + * + * @ingroup wasm + */ +GHOSTTY_API void ghostty_wasm_free_sgr_attribute(GhosttySgrAttribute* attr); +#endif + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_SGR_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/size_report.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/size_report.h new file mode 100644 index 00000000000..da33e5e5593 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/size_report.h @@ -0,0 +1,101 @@ +/** + * @file size_report.h + * + * Size report encoding - encode terminal size reports into escape sequences. + */ + +#ifndef GHOSTTY_VT_SIZE_REPORT_H +#define GHOSTTY_VT_SIZE_REPORT_H + +/** @defgroup size_report Size Report Encoding + * + * Utilities for encoding terminal size reports into escape sequences, + * supporting in-band size reports (mode 2048) and XTWINOPS responses + * (CSI 14 t, CSI 16 t, CSI 18 t). + * + * ## Basic Usage + * + * Use ghostty_size_report_encode() to encode a size report into a + * caller-provided buffer. If the buffer is too small, the function + * returns GHOSTTY_OUT_OF_SPACE and sets the required size in the + * output parameter. + * + * ## Example + * + * @snippet c-vt-size-report/src/main.c size-report-encode + * + * @{ + */ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Size report style. + * + * Determines the output format for the terminal size report. + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** In-band size report (mode 2048): ESC [ 48 ; rows ; cols ; height ; width t */ + GHOSTTY_SIZE_REPORT_MODE_2048 = 0, + /** XTWINOPS text area size in pixels: ESC [ 4 ; height ; width t */ + GHOSTTY_SIZE_REPORT_CSI_14_T = 1, + /** XTWINOPS cell size in pixels: ESC [ 6 ; height ; width t */ + GHOSTTY_SIZE_REPORT_CSI_16_T = 2, + /** XTWINOPS text area size in characters: ESC [ 8 ; rows ; cols t */ + GHOSTTY_SIZE_REPORT_CSI_18_T = 3, + GHOSTTY_SIZE_REPORT_STYLE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySizeReportStyle; + +/** + * Terminal size information for encoding size reports. + */ +typedef struct { + /** Terminal row count in cells. */ + uint16_t rows; + /** Terminal column count in cells. */ + uint16_t columns; + /** Width of a single terminal cell in pixels. */ + uint32_t cell_width; + /** Height of a single terminal cell in pixels. */ + uint32_t cell_height; +} GhosttySizeReportSize; + +/** + * Encode a terminal size report into an escape sequence. + * + * Encodes a size report in the format specified by @p style into the + * provided buffer. + * + * If the buffer is too small, the function returns GHOSTTY_OUT_OF_SPACE + * and writes the required buffer size to @p out_written. The caller can + * then retry with a sufficiently sized buffer. + * + * @param style The size report format to encode + * @param size Terminal size information + * @param buf Output buffer to write the encoded sequence into (may be NULL) + * @param buf_len Size of the output buffer in bytes + * @param[out] out_written On success, the number of bytes written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_SPACE if the buffer + * is too small + */ +GHOSTTY_API GhosttyResult ghostty_size_report_encode( + GhosttySizeReportStyle style, + GhosttySizeReportSize size, + char* buf, + size_t buf_len, + size_t* out_written); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_SIZE_REPORT_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/style.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/style.h new file mode 100644 index 00000000000..b6bf860ebe7 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/style.h @@ -0,0 +1,139 @@ +/** + * @file style.h + * + * Terminal cell style types. + */ + +#ifndef GHOSTTY_VT_STYLE_H +#define GHOSTTY_VT_STYLE_H + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup style Style + * + * Terminal cell style attributes. + * + * A style describes the visual attributes of a terminal cell, including + * foreground, background, and underline colors, as well as flags for + * bold, italic, underline, and other text decorations. + * + * @{ + */ + +/** + * Style identifier type. + * + * Used to look up the full style from a grid reference. + * Obtain this from a cell via GHOSTTY_CELL_DATA_STYLE_ID. + * + * @ingroup style + */ +typedef uint16_t GhosttyStyleId; + +/** + * Style color tags. + * + * These values identify the type of color in a style color. + * Use the tag to determine which field in the color value union to access. + * + * @ingroup style + */ +typedef enum GHOSTTY_ENUM_TYPED { + GHOSTTY_STYLE_COLOR_NONE = 0, + GHOSTTY_STYLE_COLOR_PALETTE = 1, + GHOSTTY_STYLE_COLOR_RGB = 2, + GHOSTTY_STYLE_COLOR_TAG_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, + } GhosttyStyleColorTag; + +/** + * Style color value union. + * + * Use the tag to determine which field is active. + * + * @ingroup style + */ +typedef union { + GhosttyColorPaletteIndex palette; + GhosttyColorRgb rgb; + uint64_t _padding; +} GhosttyStyleColorValue; + +/** + * Style color (tagged union). + * + * A color used in a style attribute. Can be unset (none), a palette + * index, or a direct RGB value. + * + * @ingroup style + */ +typedef struct { + GhosttyStyleColorTag tag; + GhosttyStyleColorValue value; +} GhosttyStyleColor; + +/** + * Terminal cell style. + * + * Describes the complete visual style for a terminal cell, including + * foreground, background, and underline colors, as well as text + * decoration flags. The underline field uses the same values as + * GhosttySgrUnderline. + * + * This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it. + * + * @ingroup style + */ +typedef struct { + size_t size; + GhosttyStyleColor fg_color; + GhosttyStyleColor bg_color; + GhosttyStyleColor underline_color; + bool bold; + bool italic; + bool faint; + bool blink; + bool inverse; + bool invisible; + bool strikethrough; + bool overline; + int underline; /**< One of GHOSTTY_SGR_UNDERLINE_* values */ +} GhosttyStyle; + +/** + * Get the default style. + * + * Initializes the style to the default values (no colors, no flags). + * + * @param style Pointer to the style to initialize + * + * @ingroup style + */ +GHOSTTY_API void ghostty_style_default(GhosttyStyle* style); + +/** + * Check if a style is the default style. + * + * Returns true if all colors are unset and all flags are off. + * + * @param style Pointer to the style to check + * @return true if the style is the default style + * + * @ingroup style + */ +GHOSTTY_API bool ghostty_style_is_default(const GhosttyStyle* style); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_STYLE_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/sys.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/sys.h new file mode 100644 index 00000000000..ae90596927d --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/sys.h @@ -0,0 +1,210 @@ +/** + * @file sys.h + * + * System interface - runtime-swappable implementations for external dependencies. + */ + +#ifndef GHOSTTY_VT_SYS_H +#define GHOSTTY_VT_SYS_H + +#include +#include +#include +#include +#include + +/** @defgroup sys System Interface + * + * Runtime-swappable function pointers for operations that depend on + * external implementations (e.g. image decoding). + * + * These are process-global settings that must be configured at startup + * before any terminal functionality that depends on them is used. + * Setting these enables various optional features of the terminal. For + * example, setting a PNG decoder enables PNG image support in the Kitty + * Graphics Protocol. + * + * Use ghostty_sys_set() with a `GhosttySysOption` to install or clear + * an implementation. Passing NULL as the value clears the implementation + * and disables the corresponding feature. + * + * ## Example + * + * ### Defining a PNG decode callback + * @snippet c-vt-kitty-graphics/src/main.c kitty-graphics-decode-png + * + * ### Installing the callback and sending a PNG image + * @snippet c-vt-kitty-graphics/src/main.c kitty-graphics-main + * + * @{ + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Result of decoding an image. + * + * The `data` buffer must be allocated through the allocator provided to + * the decode callback. The library takes ownership and will free it + * with the same allocator. + */ +typedef struct { + /** Image width in pixels. */ + uint32_t width; + + /** Image height in pixels. */ + uint32_t height; + + /** Pointer to the decoded RGBA pixel data. */ + uint8_t* data; + + /** Length of the pixel data in bytes. */ + size_t data_len; +} GhosttySysImage; + +/** + * Log severity levels for the log callback. + */ +typedef enum GHOSTTY_ENUM_TYPED { + GHOSTTY_SYS_LOG_LEVEL_ERROR = 0, + GHOSTTY_SYS_LOG_LEVEL_WARNING = 1, + GHOSTTY_SYS_LOG_LEVEL_INFO = 2, + GHOSTTY_SYS_LOG_LEVEL_DEBUG = 3, + GHOSTTY_SYS_LOG_LEVEL_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySysLogLevel; + +/** + * Callback type for logging. + * + * When installed, internal library log messages are delivered through + * this callback instead of being discarded. The embedder is responsible + * for formatting and routing log output. + * + * @p scope is the log scope name as UTF-8 bytes (e.g. "osc", "kitty"). + * When the log is unscoped (default scope), @p scope_len is 0. + * + * All pointer arguments are only valid for the duration of the callback. + * The callback must be safe to call from any thread. + * + * @param userdata The userdata pointer set via GHOSTTY_SYS_OPT_USERDATA + * @param level The severity level of the log message + * @param scope Pointer to the scope name bytes + * @param scope_len Length of the scope name in bytes + * @param message Pointer to the log message bytes + * @param message_len Length of the log message in bytes + */ +typedef void (*GhosttySysLogFn)( + void* userdata, + GhosttySysLogLevel level, + const uint8_t* scope, + size_t scope_len, + const uint8_t* message, + size_t message_len); + +/** + * Callback type for PNG decoding. + * + * Decodes raw PNG data into RGBA pixels. The output pixel data must be + * allocated through the provided allocator. The library takes ownership + * of the buffer and will free it with the same allocator. + * + * @param userdata The userdata pointer set via GHOSTTY_SYS_OPT_USERDATA + * @param allocator The allocator to use for the output pixel buffer + * @param data Pointer to the raw PNG data + * @param data_len Length of the raw PNG data in bytes + * @param[out] out On success, filled with the decoded image + * @return true on success, false on failure + */ +typedef bool (*GhosttySysDecodePngFn)( + void* userdata, + const GhosttyAllocator* allocator, + const uint8_t* data, + size_t data_len, + GhosttySysImage* out); + +/** + * System option identifiers for ghostty_sys_set(). + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** + * Set the userdata pointer passed to all sys callbacks. + * + * Input type: void* (or NULL) + */ + GHOSTTY_SYS_OPT_USERDATA = 0, + + /** + * Set the PNG decode function. + * + * When set, the terminal can accept PNG images via the Kitty + * Graphics Protocol. When cleared (NULL value), PNG decoding is + * unsupported and PNG image data will be rejected. + * + * Input type: GhosttySysDecodePngFn (function pointer, or NULL) + */ + GHOSTTY_SYS_OPT_DECODE_PNG = 1, + + /** + * Set the log callback. + * + * When set, internal library log messages are delivered to this + * callback. When cleared (NULL value), log messages are silently + * discarded. + * + * Use ghostty_sys_log_stderr as a convenience callback that + * writes formatted messages to stderr. + * + * Which log levels are emitted depends on the build mode of the + * library and is not configurable at runtime. Debug builds emit + * all levels (debug and above). Release builds emit info and + * above; debug-level messages are compiled out entirely and will + * never reach the callback. + * + * Input type: GhosttySysLogFn (function pointer, or NULL) + */ + GHOSTTY_SYS_OPT_LOG = 2, + GHOSTTY_SYS_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySysOption; + +/** + * Set a system-level option. + * + * Configures a process-global implementation function. These should be + * set once at startup before using any terminal functionality that + * depends on them. + * + * @param option The option to set + * @param value Pointer to the value (type depends on the option), + * or NULL to clear it + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the + * option is not recognized + */ +GHOSTTY_API GhosttyResult ghostty_sys_set(GhosttySysOption option, + const void* value); + +/** + * Built-in log callback that writes to stderr. + * + * Formats each message as "[level](scope): message\n". + * Can be passed directly to ghostty_sys_set(): + * + * @code + * ghostty_sys_set(GHOSTTY_SYS_OPT_LOG, &ghostty_sys_log_stderr); + * @endcode + */ +GHOSTTY_API void ghostty_sys_log_stderr(void* userdata, + GhosttySysLogLevel level, + const uint8_t* scope, + size_t scope_len, + const uint8_t* message, + size_t message_len); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_SYS_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/terminal.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/terminal.h new file mode 100644 index 00000000000..b22e8aedc89 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/terminal.h @@ -0,0 +1,1322 @@ +/** + * @file terminal.h + * + * Complete terminal emulator state and rendering. + */ + +#ifndef GHOSTTY_VT_TERMINAL_H +#define GHOSTTY_VT_TERMINAL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup terminal Terminal + * + * Complete terminal emulator state and rendering. + * + * A terminal instance manages the full emulator state including the screen, + * scrollback, cursor, styles, modes, and VT stream processing. + * + * Once a terminal session is up and running, you can configure a key encoder + * to write keyboard input via ghostty_key_encoder_setopt_from_terminal(). + * + * ### Example: VT stream processing + * @snippet c-vt-stream/src/main.c vt-stream-init + * @snippet c-vt-stream/src/main.c vt-stream-write + * + * ## Effects + * + * By default, the terminal sequence processing with ghostty_terminal_vt_write() + * only process sequences that directly affect terminal state and + * ignores sequences that have side effect behavior or require responses. + * These sequences include things like bell characters, title changes, device + * attributes queries, and more. To handle these sequences, the embedder + * must configure "effects." + * + * Effects are callbacks that the terminal invokes in response to VT + * sequences processed during ghostty_terminal_vt_write(). They let the + * embedding application react to terminal-initiated events such as bell + * characters, title changes, device status report responses, and more. + * + * Each effect is registered with ghostty_terminal_set() using the + * corresponding `GhosttyTerminalOption` identifier. A `NULL` value + * pointer clears the callback and disables the effect. + * + * A userdata pointer can be attached via `GHOSTTY_TERMINAL_OPT_USERDATA` + * and is passed to every callback, allowing callers to route events + * back to their own application state without global variables. + * You cannot specify different userdata for different callbacks. + * + * All callbacks are invoked synchronously during + * ghostty_terminal_vt_write(). Callbacks **must not** call + * ghostty_terminal_vt_write() on the same terminal (no reentrancy). + * And callbacks must be very careful to not block for too long or perform + * expensive operations, since they are blocking further IO processing. + * + * The available effects are: + * + * | Option | Callback Type | Trigger | + * |-----------------------------------------|-----------------------------------|-------------------------------------------| + * | `GHOSTTY_TERMINAL_OPT_WRITE_PTY` | `GhosttyTerminalWritePtyFn` | Query responses written back to the pty | + * | `GHOSTTY_TERMINAL_OPT_BELL` | `GhosttyTerminalBellFn` | BEL character (0x07) | + * | `GHOSTTY_TERMINAL_OPT_TITLE_CHANGED` | `GhosttyTerminalTitleChangedFn` | Title change via OSC 0 / OSC 2 | + * | `GHOSTTY_TERMINAL_OPT_PWD_CHANGED` | `GhosttyTerminalPwdChangedFn` | Pwd change via OSC 7 / OSC 9 / OSC 1337 | + * | `GHOSTTY_TERMINAL_OPT_ENQUIRY` | `GhosttyTerminalEnquiryFn` | ENQ character (0x05) | + * | `GHOSTTY_TERMINAL_OPT_XTVERSION` | `GhosttyTerminalXtversionFn` | XTVERSION query (CSI > q) | + * | `GHOSTTY_TERMINAL_OPT_SIZE` | `GhosttyTerminalSizeFn` | XTWINOPS size query (CSI 14/16/18 t) | + * | `GHOSTTY_TERMINAL_OPT_COLOR_SCHEME` | `GhosttyTerminalColorSchemeFn` | Color scheme query (CSI ? 996 n) | + * | `GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES`| `GhosttyTerminalDeviceAttributesFn`| Device attributes query (CSI c / > c / = c)| + * + * ### Defining a write_pty callback + * @snippet c-vt-effects/src/main.c effects-write-pty + * + * ### Defining a bell callback + * @snippet c-vt-effects/src/main.c effects-bell + * + * ### Defining a title_changed callback + * @snippet c-vt-effects/src/main.c effects-title-changed + * + * ### Registering effects and processing VT data + * @snippet c-vt-effects/src/main.c effects-register + * + * ## Color Theme + * + * The terminal maintains a set of colors used for rendering: a foreground + * color, a background color, a cursor color, and a 256-color palette. Each + * of these has two layers: a **default** value set by the embedder, and an + * **override** value that programs running in the terminal can set via OSC + * escape sequences (e.g. OSC 10/11/12 for foreground/background/cursor, + * OSC 4 for individual palette entries). + * + * ### Default Colors + * + * Use ghostty_terminal_set() with the color options to configure the + * default colors. These represent the theme or configuration chosen by + * the embedder. Passing `NULL` clears the default, leaving the color + * unset. + * + * | Option | Input Type | Description | + * |-----------------------------------------|-------------------------|--------------------------------------| + * | `GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND` | `GhosttyColorRgb*` | Default foreground color | + * | `GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND` | `GhosttyColorRgb*` | Default background color | + * | `GHOSTTY_TERMINAL_OPT_COLOR_CURSOR` | `GhosttyColorRgb*` | Default cursor color | + * | `GHOSTTY_TERMINAL_OPT_COLOR_PALETTE` | `GhosttyColorRgb[256]*` | Default 256-color palette | + * + * For the palette, passing `NULL` resets to the built-in default palette. + * The palette set operation preserves any per-index OSC overrides that + * programs have applied; only unmodified indices are updated. + * + * ### Reading colors + * + * Use ghostty_terminal_get() to read colors. There are two variants for + * each color: the **effective** value (which returns the OSC override if + * one is active, otherwise the default) and the **default** value (which + * ignores any OSC overrides). + * + * | Data | Output Type | Description | + * |---------------------------------------------------|-------------------------|------------------------------------------------| + * | `GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND` | `GhosttyColorRgb*` | Effective foreground (override or default) | + * | `GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND` | `GhosttyColorRgb*` | Effective background (override or default) | + * | `GHOSTTY_TERMINAL_DATA_COLOR_CURSOR` | `GhosttyColorRgb*` | Effective cursor (override or default) | + * | `GHOSTTY_TERMINAL_DATA_COLOR_PALETTE` | `GhosttyColorRgb[256]*` | Current palette (with any OSC overrides) | + * | `GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT` | `GhosttyColorRgb*` | Default foreground only (ignores OSC override) | + * | `GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT` | `GhosttyColorRgb*` | Default background only (ignores OSC override) | + * | `GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT` | `GhosttyColorRgb*` | Default cursor only (ignores OSC override) | + * | `GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT` | `GhosttyColorRgb[256]*` | Default palette only (ignores OSC overrides) | + * + * For foreground, background, and cursor colors, the getters return + * `GHOSTTY_NO_VALUE` if no color is configured (neither a default nor an + * OSC override). The palette getters always succeed since the palette + * always has a value (the built-in default if nothing else is set). + * + * ### Setting a color theme + * @snippet c-vt-colors/src/main.c colors-set-defaults + * + * ### Reading effective and default colors + * @snippet c-vt-colors/src/main.c colors-read + * + * ### Full example with OSC overrides + * @snippet c-vt-colors/src/main.c colors-main + * + * @{ + */ + +/** + * Terminal initialization options. + * + * @ingroup terminal + */ +typedef struct { + /** Terminal width in cells. Must be greater than zero. */ + uint16_t cols; + + /** Terminal height in cells. Must be greater than zero. */ + uint16_t rows; + + /** Maximum number of lines to keep in scrollback history. */ + size_t max_scrollback; + + // TODO: Consider ABI compatibility implications of this struct. + // We may want to artificially pad it significantly to support + // future options. +} GhosttyTerminalOptions; + +/** + * Scroll viewport behavior tag. + * + * @ingroup terminal + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Scroll to the top of the scrollback. */ + GHOSTTY_SCROLL_VIEWPORT_TOP, + + /** Scroll to the bottom (active area). */ + GHOSTTY_SCROLL_VIEWPORT_BOTTOM, + + /** Scroll by a delta amount (up is negative). */ + GHOSTTY_SCROLL_VIEWPORT_DELTA, + GHOSTTY_SCROLL_VIEWPORT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyTerminalScrollViewportTag; + +/** + * Scroll viewport value. + * + * @ingroup terminal + */ +typedef union { + /** Scroll delta (only used with GHOSTTY_SCROLL_VIEWPORT_DELTA). Up is negative. */ + intptr_t delta; + + /** Padding for ABI compatibility. Do not use. */ + uint64_t _padding[2]; +} GhosttyTerminalScrollViewportValue; + +/** + * Tagged union for scroll viewport behavior. + * + * @ingroup terminal + */ +typedef struct { + GhosttyTerminalScrollViewportTag tag; + GhosttyTerminalScrollViewportValue value; +} GhosttyTerminalScrollViewport; + +/** + * Terminal screen identifier. + * + * Identifies which screen buffer is active in the terminal. + * + * @ingroup terminal + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** The primary (normal) screen. */ + GHOSTTY_TERMINAL_SCREEN_PRIMARY = 0, + + /** The alternate screen. */ + GHOSTTY_TERMINAL_SCREEN_ALTERNATE = 1, + GHOSTTY_TERMINAL_SCREEN_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyTerminalScreen; + +/** + * Visual style of the terminal cursor. + * + * @ingroup terminal + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Bar cursor (DECSCUSR 5, 6). */ + GHOSTTY_TERMINAL_CURSOR_STYLE_BAR = 0, + + /** Block cursor (DECSCUSR 1, 2). */ + GHOSTTY_TERMINAL_CURSOR_STYLE_BLOCK = 1, + + /** Underline cursor (DECSCUSR 3, 4). */ + GHOSTTY_TERMINAL_CURSOR_STYLE_UNDERLINE = 2, + + /** Hollow block cursor. */ + GHOSTTY_TERMINAL_CURSOR_STYLE_BLOCK_HOLLOW = 3, + GHOSTTY_TERMINAL_CURSOR_STYLE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyTerminalCursorStyle; + +/** + * Scrollbar state for the terminal viewport. + * + * Represents the scrollable area dimensions needed to render a scrollbar. + * + * @ingroup terminal + */ +typedef struct { + /** Total size of the scrollable area in rows. */ + uint64_t total; + + /** Offset into the total area that the viewport is at. */ + uint64_t offset; + + /** Length of the visible area in rows. */ + uint64_t len; +} GhosttyTerminalScrollbar; + +/** + * Callback function type for bell. + * + * Called when the terminal receives a BEL character (0x07). + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * + * @ingroup terminal + */ +typedef void (*GhosttyTerminalBellFn)(GhosttyTerminal terminal, + void* userdata); + +/** + * Callback function type for color scheme queries (CSI ? 996 n). + * + * Called when the terminal receives a color scheme device status report + * query. Return true and fill *out_scheme with the current color scheme, + * or return false to silently ignore the query. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * @param[out] out_scheme Pointer to store the current color scheme + * @return true if the color scheme was filled, false to ignore the query + * + * @ingroup terminal + */ +typedef bool (*GhosttyTerminalColorSchemeFn)(GhosttyTerminal terminal, + void* userdata, + GhosttyColorScheme* out_scheme); + +/** + * Callback function type for device attributes queries (DA1/DA2/DA3). + * + * Called when the terminal receives a device attributes query (CSI c, + * CSI > c, or CSI = c). Return true and fill *out_attrs with the + * response data, or return false to silently ignore the query. + * + * The terminal uses whichever sub-struct (primary, secondary, tertiary) + * matches the request type, but all three should be filled for simplicity. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * @param[out] out_attrs Pointer to store the device attributes response + * @return true if attributes were filled, false to ignore the query + * + * @ingroup terminal + */ +typedef bool (*GhosttyTerminalDeviceAttributesFn)(GhosttyTerminal terminal, + void* userdata, + GhosttyDeviceAttributes* out_attrs); + +/** + * Callback function type for enquiry (ENQ, 0x05). + * + * Called when the terminal receives an ENQ character. Return the + * response bytes as a GhosttyString. The memory must remain valid + * until the callback returns. Return a zero-length string to send + * no response. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * @return The response bytes to write back to the pty + * + * @ingroup terminal + */ +typedef GhosttyString (*GhosttyTerminalEnquiryFn)(GhosttyTerminal terminal, + void* userdata); + +/** + * Callback function type for size queries (XTWINOPS). + * + * Called in response to XTWINOPS size queries (CSI 14/16/18 t). + * Return true and fill *out_size with the current terminal geometry, + * or return false to silently ignore the query. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * @param[out] out_size Pointer to store the terminal size information + * @return true if size was filled, false to ignore the query + * + * @ingroup terminal + */ +typedef bool (*GhosttyTerminalSizeFn)(GhosttyTerminal terminal, + void* userdata, + GhosttySizeReportSize* out_size); + +/** + * Callback function type for title_changed. + * + * Called when the terminal title changes via escape sequences + * (e.g. OSC 0 or OSC 2). The new title can be queried from the + * terminal after the callback returns. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * + * @ingroup terminal + */ +typedef void (*GhosttyTerminalTitleChangedFn)(GhosttyTerminal terminal, + void* userdata); + +/** + * Callback function type for pwd_changed. + * + * Called when the terminal pwd (current working directory) changes via + * escape sequences: OSC 7 (file:// URI), OSC 9 (ConEmu CurrentDir), or + * OSC 1337 CurrentDir (iTerm2). Use ghostty_terminal_get() with + * GHOSTTY_TERMINAL_DATA_PWD inside the callback to read the new value. + * + * The terminal stores whatever bytes the shell emitted, without parsing. + * That means for OSC 7 the value is the raw URI (typically file://...); + * for OSC 9/OSC 1337 it is typically a bare path. The embedder is + * responsible for decoding any URI scheme or host if it cares about them. + * + * The callback also fires when the shell clears the pwd (e.g. an empty + * OSC 7). In that case GHOSTTY_TERMINAL_DATA_PWD returns a zero-length + * string. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * + * @ingroup terminal + */ +typedef void (*GhosttyTerminalPwdChangedFn)(GhosttyTerminal terminal, + void* userdata); + +/** + * Callback function type for write_pty. + * + * Called when the terminal needs to write data back to the pty, for + * example in response to a device status report or mode query. The + * data is only valid for the duration of the call; callers must copy + * it if it needs to persist. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * @param data Pointer to the response bytes + * @param len Length of the response in bytes + * + * @ingroup terminal + */ +typedef void (*GhosttyTerminalWritePtyFn)(GhosttyTerminal terminal, + void* userdata, + const uint8_t* data, + size_t len); + +/** + * Callback function type for XTVERSION. + * + * Called when the terminal receives an XTVERSION query (CSI > q). + * Return the version string (e.g. "myterm 1.0") as a GhosttyString. + * The memory must remain valid until the callback returns. Return a + * zero-length string to report the default "libghostty" version. + * + * @param terminal The terminal handle + * @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA + * @return The version string to report + * + * @ingroup terminal + */ +typedef GhosttyString (*GhosttyTerminalXtversionFn)(GhosttyTerminal terminal, + void* userdata); + +/** + * Terminal option identifiers. + * + * These values are used with ghostty_terminal_set() to configure + * terminal callbacks and associated state. + * + * @ingroup terminal + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** + * Opaque userdata pointer passed to all callbacks. + * + * Input type: void* + */ + GHOSTTY_TERMINAL_OPT_USERDATA = 0, + + /** + * Callback invoked when the terminal needs to write data back + * to the pty (e.g. in response to a DECRQM query or device + * status report). Set to NULL to ignore such sequences. + * + * Input type: GhosttyTerminalWritePtyFn + */ + GHOSTTY_TERMINAL_OPT_WRITE_PTY = 1, + + /** + * Callback invoked when the terminal receives a BEL character + * (0x07). Set to NULL to ignore bell events. + * + * Input type: GhosttyTerminalBellFn + */ + GHOSTTY_TERMINAL_OPT_BELL = 2, + + /** + * Callback invoked when the terminal receives an ENQ character + * (0x05). Set to NULL to send no response. + * + * Input type: GhosttyTerminalEnquiryFn + */ + GHOSTTY_TERMINAL_OPT_ENQUIRY = 3, + + /** + * Callback invoked when the terminal receives an XTVERSION query + * (CSI > q). Set to NULL to report the default "libghostty" string. + * + * Input type: GhosttyTerminalXtversionFn + */ + GHOSTTY_TERMINAL_OPT_XTVERSION = 4, + + /** + * Callback invoked when the terminal title changes via escape + * sequences (e.g. OSC 0 or OSC 2). Set to NULL to ignore title + * change events. + * + * Input type: GhosttyTerminalTitleChangedFn + */ + GHOSTTY_TERMINAL_OPT_TITLE_CHANGED = 5, + + /** + * Callback invoked in response to XTWINOPS size queries + * (CSI 14/16/18 t). Set to NULL to silently ignore size queries. + * + * Input type: GhosttyTerminalSizeFn + */ + GHOSTTY_TERMINAL_OPT_SIZE = 6, + + /** + * Callback invoked in response to a color scheme device status + * report query (CSI ? 996 n). Return true and fill the out pointer + * to report the current scheme, or return false to silently ignore. + * Set to NULL to ignore color scheme queries. + * + * Input type: GhosttyTerminalColorSchemeFn + */ + GHOSTTY_TERMINAL_OPT_COLOR_SCHEME = 7, + + /** + * Callback invoked in response to a device attributes query + * (CSI c, CSI > c, or CSI = c). Return true and fill the out + * pointer with response data, or return false to silently ignore. + * Set to NULL to ignore device attributes queries. + * + * Input type: GhosttyTerminalDeviceAttributesFn + */ + GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES = 8, + + /** + * Set the terminal title manually. + * + * The string data is copied into the terminal. A NULL value pointer + * clears the title (equivalent to setting an empty string). + * + * Input type: GhosttyString* + */ + GHOSTTY_TERMINAL_OPT_TITLE = 9, + + /** + * Set the terminal working directory manually. + * + * The string data is copied into the terminal. A NULL value pointer + * clears the pwd (equivalent to setting an empty string). + * + * Input type: GhosttyString* + */ + GHOSTTY_TERMINAL_OPT_PWD = 10, + + /** + * Set the default foreground color. + * + * A NULL value pointer clears the default (unset). + * + * Input type: GhosttyColorRgb* + */ + GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND = 11, + + /** + * Set the default background color. + * + * A NULL value pointer clears the default (unset). + * + * Input type: GhosttyColorRgb* + */ + GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND = 12, + + /** + * Set the default cursor color. + * + * A NULL value pointer clears the default (unset). + * + * Input type: GhosttyColorRgb* + */ + GHOSTTY_TERMINAL_OPT_COLOR_CURSOR = 13, + + /** + * Set the default 256-color palette. + * + * The value must point to an array of exactly 256 GhosttyColorRgb values. + * A NULL value pointer resets to the built-in default palette. + * + * Input type: GhosttyColorRgb[256]* + */ + GHOSTTY_TERMINAL_OPT_COLOR_PALETTE = 14, + + /** + * Set the Kitty image storage limit in bytes. + * + * Applied to all initialized screens (primary and alternate). + * A value of zero disables the Kitty graphics protocol entirely, + * deleting all stored images and placements. A NULL value pointer + * is equivalent to zero (disables). Has no effect when Kitty graphics + * are disabled at build time. + * + * Input type: uint64_t* + */ + GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT = 15, + + /** + * Enable or disable Kitty image loading via the file medium. + * + * A NULL value pointer is a no-op. Has no effect when Kitty graphics + * are disabled at build time. + * + * Input type: bool* + */ + GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE = 16, + + /** + * Enable or disable Kitty image loading via the temporary file medium. + * + * A NULL value pointer is a no-op. Has no effect when Kitty graphics + * are disabled at build time. + * + * Input type: bool* + */ + GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE = 17, + + /** + * Enable or disable Kitty image loading via the shared memory medium. + * + * A NULL value pointer is a no-op. Has no effect when Kitty graphics + * are disabled at build time. + * + * Input type: bool* + */ + GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM = 18, + + /** + * Set the maximum bytes the APC handler will buffer for all protocols. + * This prevents malicious input from causing unbounded memory allocation. + * A NULL value pointer removes all overrides, reverting to the built-in + * defaults. + * + * Input type: size_t* + */ + GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES = 19, + + /** + * Set the maximum bytes the APC handler will buffer for Kitty graphics + * protocol data. A NULL value pointer removes the override, reverting + * to the built-in default. + * + * Input type: size_t* + */ + GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES_KITTY = 20, + + /** + * Set the active screen selection. + * + * The value must point to a GhosttySelection whose grid references are + * valid for this terminal's active screen at the time of the call. The + * terminal copies the selection immediately and converts it to + * terminal-owned tracked state, so the GhosttySelection struct and its + * untracked grid references do not need to outlive this call. + * + * Passing NULL clears the active screen selection. + * + * Input type: GhosttySelection* + */ + GHOSTTY_TERMINAL_OPT_SELECTION = 21, + + /** + * Set the default cursor style used by DECSCUSR reset (CSI 0 q). + * + * A NULL value pointer resets to the built-in default block cursor. + * + * Input type: GhosttyTerminalCursorStyle* + */ + GHOSTTY_TERMINAL_OPT_DEFAULT_CURSOR_STYLE = 22, + + /** + * Set whether the default cursor should blink when reset by DECSCUSR + * (CSI 0 q). + * + * A NULL value pointer resets to the built-in default of not blinking. + * + * Input type: bool* + */ + GHOSTTY_TERMINAL_OPT_DEFAULT_CURSOR_BLINK = 23, + + /** + * Enable or disable Glyph Protocol APC handling. + * + * When disabled, Glyph Protocol APC sequences are ignored and no + * support/query/register/clear responses are emitted. Disabling also clears + * the terminal session's glyph glossary. A NULL value pointer is a no-op. + * + * Input type: bool* + */ + GHOSTTY_TERMINAL_OPT_GLYPH_PROTOCOL = 24, + + /** + * Callback invoked when the terminal pwd changes via escape + * sequences (OSC 7, OSC 9, or OSC 1337 CurrentDir). Set to NULL + * to ignore pwd change events. + * + * Input type: GhosttyTerminalPwdChangedFn + */ + GHOSTTY_TERMINAL_OPT_PWD_CHANGED = 25, + GHOSTTY_TERMINAL_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyTerminalOption; + +/** + * Terminal data types. + * + * These values specify what type of data to extract from a terminal + * using `ghostty_terminal_get`. + * + * @ingroup terminal + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Invalid data type. Never results in any data extraction. */ + GHOSTTY_TERMINAL_DATA_INVALID = 0, + + /** + * Terminal width in cells. + * + * Output type: uint16_t * + */ + GHOSTTY_TERMINAL_DATA_COLS = 1, + + /** + * Terminal height in cells. + * + * Output type: uint16_t * + */ + GHOSTTY_TERMINAL_DATA_ROWS = 2, + + /** + * Cursor column position (0-indexed). + * + * Output type: uint16_t * + */ + GHOSTTY_TERMINAL_DATA_CURSOR_X = 3, + + /** + * Cursor row position within the active area (0-indexed). + * + * Output type: uint16_t * + */ + GHOSTTY_TERMINAL_DATA_CURSOR_Y = 4, + + /** + * Whether the cursor has a pending wrap (next print will soft-wrap). + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_CURSOR_PENDING_WRAP = 5, + + /** + * The currently active screen. + * + * Output type: GhosttyTerminalScreen * + */ + GHOSTTY_TERMINAL_DATA_ACTIVE_SCREEN = 6, + + /** + * Whether the cursor is visible (DEC mode 25). + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_CURSOR_VISIBLE = 7, + + /** + * Current Kitty keyboard protocol flags. + * + * Output type: GhosttyKittyKeyFlags * (uint8_t *) + */ + GHOSTTY_TERMINAL_DATA_KITTY_KEYBOARD_FLAGS = 8, + + /** + * Scrollbar state for the terminal viewport. + * + * This may be expensive to calculate depending on where the viewport + * is (arbitrary pins are expensive). The caller should take care to only + * call this as needed and not too frequently. + * + * Output type: GhosttyTerminalScrollbar * + */ + GHOSTTY_TERMINAL_DATA_SCROLLBAR = 9, + + /** + * The current SGR style of the cursor. + * + * This is the style that will be applied to newly printed characters. + * + * Output type: GhosttyStyle * + */ + GHOSTTY_TERMINAL_DATA_CURSOR_STYLE = 10, + + /** + * Whether any mouse tracking mode is active. + * + * Returns true if any of the mouse tracking modes (X10, normal, button, + * or any-event) are enabled. + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING = 11, + + /** + * The terminal title as set by escape sequences (e.g. OSC 0/2). + * + * Returns a borrowed string. The pointer is valid until the next call + * to ghostty_terminal_vt_write() or ghostty_terminal_reset(). An empty + * string (len=0) is returned when no title has been set. + * + * Output type: GhosttyString * + */ + GHOSTTY_TERMINAL_DATA_TITLE = 12, + + /** + * The terminal's current working directory as set by escape sequences + * (e.g. OSC 7). + * + * Returns a borrowed string. The pointer is valid until the next call + * to ghostty_terminal_vt_write() or ghostty_terminal_reset(). An empty + * string (len=0) is returned when no pwd has been set. + * + * Output type: GhosttyString * + */ + GHOSTTY_TERMINAL_DATA_PWD = 13, + + /** + * The total number of rows in the active screen including scrollback. + * + * Output type: size_t * + */ + GHOSTTY_TERMINAL_DATA_TOTAL_ROWS = 14, + + /** + * The number of scrollback rows (total rows minus viewport rows). + * + * Output type: size_t * + */ + GHOSTTY_TERMINAL_DATA_SCROLLBACK_ROWS = 15, + + /** + * The total width of the terminal in pixels. + * + * This is cols * cell_width_px as set by ghostty_terminal_resize(). + * + * Output type: uint32_t * + */ + GHOSTTY_TERMINAL_DATA_WIDTH_PX = 16, + + /** + * The total height of the terminal in pixels. + * + * This is rows * cell_height_px as set by ghostty_terminal_resize(). + * + * Output type: uint32_t * + */ + GHOSTTY_TERMINAL_DATA_HEIGHT_PX = 17, + + /** + * The effective foreground color (override or default). + * + * Returns GHOSTTY_NO_VALUE if no foreground color is set. + * + * Output type: GhosttyColorRgb * + */ + GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND = 18, + + /** + * The effective background color (override or default). + * + * Returns GHOSTTY_NO_VALUE if no background color is set. + * + * Output type: GhosttyColorRgb * + */ + GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND = 19, + + /** + * The effective cursor color (override or default). + * + * Returns GHOSTTY_NO_VALUE if no cursor color is set. + * + * Output type: GhosttyColorRgb * + */ + GHOSTTY_TERMINAL_DATA_COLOR_CURSOR = 20, + + /** + * The current 256-color palette. + * + * Output type: GhosttyColorRgb[256] * + */ + GHOSTTY_TERMINAL_DATA_COLOR_PALETTE = 21, + + /** + * The default foreground color (ignoring any OSC override). + * + * Returns GHOSTTY_NO_VALUE if no default foreground color is set. + * + * Output type: GhosttyColorRgb * + */ + GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT = 22, + + /** + * The default background color (ignoring any OSC override). + * + * Returns GHOSTTY_NO_VALUE if no default background color is set. + * + * Output type: GhosttyColorRgb * + */ + GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT = 23, + + /** + * The default cursor color (ignoring any OSC override). + * + * Returns GHOSTTY_NO_VALUE if no default cursor color is set. + * + * Output type: GhosttyColorRgb * + */ + GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT = 24, + + /** + * The default 256-color palette (ignoring any OSC overrides). + * + * Output type: GhosttyColorRgb[256] * + */ + GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT = 25, + + /** + * The Kitty image storage limit in bytes for the active screen. + * + * A value of zero means the Kitty graphics protocol is disabled. + * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time. + * + * Output type: uint64_t * + */ + GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_STORAGE_LIMIT = 26, + + /** + * Whether the file medium is enabled for Kitty image loading on the + * active screen. + * + * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time. + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_FILE = 27, + + /** + * Whether the temporary file medium is enabled for Kitty image loading + * on the active screen. + * + * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time. + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_TEMP_FILE = 28, + + /** + * Whether the shared memory medium is enabled for Kitty image loading + * on the active screen. + * + * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time. + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_SHARED_MEM = 29, + + /** + * The Kitty graphics image storage for the active screen. + * + * Returns a borrowed pointer to the image storage. The pointer is valid + * until the next mutating terminal call (e.g. ghostty_terminal_vt_write() + * or ghostty_terminal_reset()). + * + * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time. + * + * Output type: GhosttyKittyGraphics * + */ + GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS = 30, + + /** + * The active screen's current selection. + * + * On success, writes an untracked snapshot of the terminal-owned selection + * to the caller-provided GhosttySelection. The GhosttySelection struct is + * caller-owned and may be kept, but the grid references inside it are + * untracked borrowed references into the active screen. They are only valid + * until the next mutating terminal call, such as ghostty_terminal_set(), + * ghostty_terminal_vt_write(), ghostty_terminal_resize(), or + * ghostty_terminal_reset(). + * + * Returns GHOSTTY_NO_VALUE when there is no active selection. + * + * Output type: GhosttySelection * + */ + GHOSTTY_TERMINAL_DATA_SELECTION = 31, + + /** + * Whether the viewport is currently pinned to the active area. + * + * This is true when the viewport is following the active terminal area, + * and false when the user has scrolled into history. + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_VIEWPORT_ACTIVE = 32, + GHOSTTY_TERMINAL_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyTerminalData; + +/** + * Create a new terminal instance. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param terminal Pointer to store the created terminal handle + * @param options Terminal initialization options + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_new(const GhosttyAllocator* allocator, + GhosttyTerminal* terminal, + GhosttyTerminalOptions options); + +/** + * Free a terminal instance. + * + * Releases all resources associated with the terminal. After this call, + * the terminal handle becomes invalid and must not be used. + * + * @param terminal The terminal handle to free (may be NULL) + * + * @ingroup terminal + */ +GHOSTTY_API void ghostty_terminal_free(GhosttyTerminal terminal); + +/** + * Perform a full reset of the terminal (RIS). + * + * Resets all terminal state back to its initial configuration, including + * modes, scrollback, scrolling region, and screen contents. The terminal + * dimensions are preserved. + * + * @param terminal The terminal handle (may be NULL, in which case this is a no-op) + * + * @ingroup terminal + */ +GHOSTTY_API void ghostty_terminal_reset(GhosttyTerminal terminal); + +/** + * Resize the terminal to the given dimensions. + * + * Changes the number of columns and rows in the terminal. The primary + * screen will reflow content if wraparound mode is enabled; the alternate + * screen does not reflow. If the dimensions are unchanged, this is a no-op. + * + * This also updates the terminal's pixel dimensions (used for image + * protocols and size reports), disables synchronized output mode (allowed + * by the spec so that resize results are shown immediately), and sends an + * in-band size report if mode 2048 is enabled. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param cols New width in cells (must be greater than zero) + * @param rows New height in cells (must be greater than zero) + * @param cell_width_px Width of a single cell in pixels + * @param cell_height_px Height of a single cell in pixels + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_resize(GhosttyTerminal terminal, + uint16_t cols, + uint16_t rows, + uint32_t cell_width_px, + uint32_t cell_height_px); + +/** + * Set an option on the terminal. + * + * Configures terminal callbacks and associated state such as the + * write_pty callback and userdata pointer. The value is passed + * directly for pointer types (callbacks, userdata) or as a pointer + * to the value for non-pointer types (e.g. GhosttyString*). + * NULL clears the option to its default. + * + * Callbacks are invoked synchronously during ghostty_terminal_vt_write(). + * Callbacks must not call ghostty_terminal_vt_write() on the same + * terminal (no reentrancy). + * + * @param terminal The terminal handle (may be NULL, in which case this is a no-op) + * @param option The option to set + * @param value Pointer to the value to set (type depends on the option), + * or NULL to clear the option + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_set(GhosttyTerminal terminal, + GhosttyTerminalOption option, + const void* value); + +/** + * Write VT-encoded data to the terminal for processing. + * + * Feeds raw bytes through the terminal's VT stream parser, updating + * terminal state accordingly. By default, sequences that require output + * (queries, device status reports) are silently ignored. Use + * ghostty_terminal_set() with GHOSTTY_TERMINAL_OPT_WRITE_PTY to install + * a callback that receives response data. + * + * This never fails. Any erroneous input or errors in processing the + * input are logged internally but do not cause this function to fail + * because this input is assumed to be untrusted and from an external + * source; so the primary goal is to keep the terminal state consistent and + * not allow malformed input to corrupt or crash. + * + * @param terminal The terminal handle + * @param data Pointer to the data to write + * @param len Length of the data in bytes + * + * @ingroup terminal + */ +GHOSTTY_API void ghostty_terminal_vt_write(GhosttyTerminal terminal, + const uint8_t* data, + size_t len); + +/** + * Scroll the terminal viewport. + * + * Scrolls the terminal's viewport according to the given behavior. + * When using GHOSTTY_SCROLL_VIEWPORT_DELTA, set the delta field in + * the value union to specify the number of rows to scroll (negative + * for up, positive for down). For other behaviors, the value is ignored. + * + * @param terminal The terminal handle (may be NULL, in which case this is a no-op) + * @param behavior The scroll behavior as a tagged union + * + * @ingroup terminal + */ +GHOSTTY_API void ghostty_terminal_scroll_viewport(GhosttyTerminal terminal, + GhosttyTerminalScrollViewport behavior); + +/** + * Get the current value of a terminal mode. + * + * Returns the value of the mode identified by the given mode. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param mode The mode identifying the mode to query + * @param[out] out_value On success, set to true if the mode is set, false + * if it is reset + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal + * is NULL or the mode does not correspond to a known mode + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_mode_get(GhosttyTerminal terminal, + GhosttyMode mode, + bool* out_value); + +/** + * Set the value of a terminal mode. + * + * Sets the mode identified by the given mode to the specified value. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param mode The mode identifying the mode to set + * @param value true to set the mode, false to reset it + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal + * is NULL or the mode does not correspond to a known mode + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_mode_set(GhosttyTerminal terminal, + GhosttyMode mode, + bool value); + +/** + * Get data from a terminal instance. + * + * Extracts typed data from the given terminal based on the specified + * data type. The output pointer must be of the appropriate type for the + * requested data kind. Valid data types and output types are documented + * in the `GhosttyTerminalData` enum. + * + * @param terminal The terminal handle (may be NULL) + * @param data The type of data to extract + * @param out Pointer to store the extracted data (type depends on data parameter) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal + * is NULL or the data type is invalid + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_get(GhosttyTerminal terminal, + GhosttyTerminalData data, + void *out); + +/** + * Get multiple data fields from a terminal in a single call. + * + * This is an optimization over calling ghostty_terminal_get() + * repeatedly, particularly useful in environments with high per-call + * overhead such as FFI or Cgo. + * + * Each element in the keys array specifies a data kind, and the + * corresponding element in the values array receives the result. + * The type of each values[i] pointer must match the output type + * documented for keys[i]. + * + * Processing stops at the first error; on success out_written + * is set to count, on error it is set to the index of the + * failing key (i.e. the number of values successfully written). + * + * @param terminal The terminal handle (may be NULL) + * @param count Number of key/value pairs + * @param keys Array of data kinds to query + * @param values Array of output pointers (types must match each key's + * documented output type) + * @param[out] out_written On return, receives the number of values + * successfully written (may be NULL) + * @return GHOSTTY_SUCCESS if all queries succeed + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_get_multi(GhosttyTerminal terminal, + size_t count, + const GhosttyTerminalData* keys, + void** values, + size_t* out_written); + +/** + * Resolve a point in the terminal grid to a grid reference. + * + * Resolves the given point (which can be in active, viewport, screen, + * or history coordinates) to a grid reference for that location. Use + * ghostty_grid_ref_cell() and ghostty_grid_ref_row() to extract the cell + * and row. + * + * Lookups using the `active` and `viewport` tags are fast. The `screen` + * and `history` tags may require traversing the full scrollback page list + * to resolve the y coordinate, so they can be expensive for large + * scrollback buffers. + * + * This function isn't meant to be used as the core of render loop. It + * isn't built to sustain the framerates needed for rendering large screens. + * Use the render state API for that. This API is instead meant for less + * strictly performance-sensitive use cases. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param point The point specifying which cell to look up + * @param[out] out_ref On success, set to the grid reference at the given point (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal + * is NULL or the point is out of bounds + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_grid_ref(GhosttyTerminal terminal, + GhosttyPoint point, + GhosttyGridRef *out_ref); + +/** + * Create an owned tracked grid reference for a terminal point. + * + * This is the tracked variant of ghostty_terminal_grid_ref(). The returned + * handle follows the referenced cell as the terminal's page list is modified: + * scrolling, pruning, resize/reflow, and other page-list operations update the + * tracked reference automatically. + * + * The reference is attached to the terminal screen/page-list that is active at + * creation time. + * + * If the point is outside the requested coordinate space, this returns + * GHOSTTY_INVALID_VALUE and writes NULL to out_ref. + * + * The returned handle must be freed with ghostty_tracked_grid_ref_free(). If + * the terminal is freed first, the handle remains valid only for + * tracked-grid-ref APIs: it reports no value and can still be freed. + * + * @param terminal Terminal instance. + * @param point Point to track. + * @param[out] out_ref On success, receives the tracked reference handle. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if terminal, + * point, or out_ref is invalid, or GHOSTTY_OUT_OF_MEMORY if allocation + * fails. + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_grid_ref_track( + GhosttyTerminal terminal, + GhosttyPoint point, + GhosttyTrackedGridRef *out_ref); + +/** + * Convert a grid reference back to a point in the given coordinate system. + * + * This is the inverse of ghostty_terminal_grid_ref(): given a grid reference, + * it returns the x/y coordinates in the requested coordinate system (active, + * viewport, screen, or history). + * + * The grid reference must have been obtained from the same terminal instance. + * Like all grid references, it is only valid until the next mutating terminal + * call. + * + * Not every grid reference is representable in every coordinate system. For + * example, a cell in scrollback history cannot be expressed in active + * coordinates, and a cell that has scrolled off the visible area cannot be + * expressed in viewport coordinates. In these cases, the function returns + * GHOSTTY_NO_VALUE. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param ref Pointer to the grid reference to convert + * @param tag The target coordinate system + * @param[out] out On success, set to the coordinate in the requested system (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal + * or ref is NULL/invalid, GHOSTTY_NO_VALUE if the ref falls outside + * the requested coordinate system + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_point_from_grid_ref( + GhosttyTerminal terminal, + const GhosttyGridRef *ref, + GhosttyPointTag tag, + GhosttyPointCoordinate *out); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_TERMINAL_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/types.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/types.h new file mode 100644 index 00000000000..214d282296c --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/types.h @@ -0,0 +1,335 @@ +/** + * @file types.h + * + * Common types, macros, and utilities for libghostty-vt. + */ + +#ifndef GHOSTTY_VT_TYPES_H +#define GHOSTTY_VT_TYPES_H + +#include +#include +#include + +// Symbol visibility for shared library builds. On Windows, functions +// are exported from the DLL when building and imported when consuming. +// On other platforms with GCC/Clang, functions are marked with default +// visibility so they remain accessible when the library is built with +// -fvisibility=hidden. For static library builds, define GHOSTTY_STATIC +// before including this header to make this a no-op. +#ifndef GHOSTTY_API +#if defined(GHOSTTY_STATIC) + #define GHOSTTY_API +#elif defined(_WIN32) || defined(_WIN64) + #ifdef GHOSTTY_BUILD_SHARED + #define GHOSTTY_API __declspec(dllexport) + #else + #define GHOSTTY_API __declspec(dllimport) + #endif +#elif defined(__GNUC__) && __GNUC__ >= 4 + #define GHOSTTY_API __attribute__((visibility("default"))) +#else + #define GHOSTTY_API +#endif +#endif + +/** + * Enum int-sizing helpers. + * + * The Zig side backs all C enums with c_int, so the C declarations + * must use int as their underlying type to maintain ABI compatibility. + * + * C23 (detected via __STDC_VERSION__ >= 202311L) supports explicit + * enum underlying types with `enum : int { ... }`. For pre-C23 + * compilers, which are free to choose any type that can represent + * all values (C11 §6.7.2.2), we add an INT_MAX sentinel as the last + * entry to force the compiler to use int. + * + * INT_MAX is used rather than a fixed constant like 0xFFFFFFFF + * because enum constants must have type int (which is signed). + * Values above INT_MAX overflow signed int and are a constraint + * violation in standard C; compilers that accept them interpret them + * as negative values via two's complement, which can collide with + * legitimate negative enum values. + * + * Usage: + * @code + * typedef enum GHOSTTY_ENUM_TYPED { + * FOO_A = 0, + * FOO_B = 1, + * FOO_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, + * } Foo; + * @endcode + */ +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L +#define GHOSTTY_ENUM_TYPED : int +#else +#define GHOSTTY_ENUM_TYPED +#endif +#define GHOSTTY_ENUM_MAX_VALUE INT_MAX + +/** + * Result codes for libghostty-vt operations. + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Operation completed successfully */ + GHOSTTY_SUCCESS = 0, + /** Operation failed due to failed allocation */ + GHOSTTY_OUT_OF_MEMORY = -1, + /** Operation failed due to invalid value */ + GHOSTTY_INVALID_VALUE = -2, + /** Operation failed because the provided buffer was too small */ + GHOSTTY_OUT_OF_SPACE = -3, + /** The requested value has no value */ + GHOSTTY_NO_VALUE = -4, + GHOSTTY_RESULT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyResult; + +/* ---- Opaque handles ---- */ + +/** + * Opaque handle to a terminal instance. + * + * @ingroup terminal + */ +typedef struct GhosttyTerminalImpl* GhosttyTerminal; + +/** + * Opaque handle to a tracked grid reference. + * + * A tracked grid reference is owned by the caller and must be freed with + * ghostty_tracked_grid_ref_free(). If the terminal that created it is freed + * first, the handle remains valid only for tracked-grid-ref APIs: it reports no + * value and can still be freed. + * + * @ingroup grid_ref + */ +typedef struct GhosttyTrackedGridRefImpl* GhosttyTrackedGridRef; + +/** + * Opaque handle to a Kitty graphics image storage. + * + * Obtained via ghostty_terminal_get() with + * GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS. The pointer is borrowed from + * the terminal and remains valid until the next mutating terminal call + * (e.g. ghostty_terminal_vt_write() or ghostty_terminal_reset()). + * + * @ingroup kitty_graphics + */ +typedef struct GhosttyKittyGraphicsImpl* GhosttyKittyGraphics; + +/** + * Opaque handle to a Kitty graphics image. + * + * Obtained via ghostty_kitty_graphics_image() with an image ID. The + * pointer is borrowed from the storage and remains valid until the next + * mutating terminal call. + * + * @ingroup kitty_graphics + */ +typedef const struct GhosttyKittyGraphicsImageImpl* GhosttyKittyGraphicsImage; + +/** + * Opaque handle to a Kitty graphics placement iterator. + * + * @ingroup kitty_graphics + */ +typedef struct GhosttyKittyGraphicsPlacementIteratorImpl* GhosttyKittyGraphicsPlacementIterator; + +/** + * Opaque handle to a render state instance. + * + * @ingroup render + */ +typedef struct GhosttyRenderStateImpl* GhosttyRenderState; + +/** + * Opaque handle to a render-state row iterator. + * + * @ingroup render + */ +typedef struct GhosttyRenderStateRowIteratorImpl* GhosttyRenderStateRowIterator; + +/** + * Opaque handle to render-state row cells. + * + * @ingroup render + */ +typedef struct GhosttyRenderStateRowCellsImpl* GhosttyRenderStateRowCells; + +/** + * Opaque handle to an SGR parser instance. + * + * This handle represents an SGR (Select Graphic Rendition) parser that can + * be used to parse SGR sequences and extract individual text attributes. + * + * @ingroup sgr + */ +typedef struct GhosttySgrParserImpl* GhosttySgrParser; + +/** + * Opaque handle to a formatter instance. + * + * @ingroup formatter + */ +typedef struct GhosttyFormatterImpl* GhosttyFormatter; + +/** + * Opaque handle to an OSC parser instance. + * + * This handle represents an OSC (Operating System Command) parser that can + * be used to parse the contents of OSC sequences. + * + * @ingroup osc + */ +typedef struct GhosttyOscParserImpl* GhosttyOscParser; + +/** + * Opaque handle to a single OSC command. + * + * This handle represents a parsed OSC (Operating System Command) command. + * The command can be queried for its type and associated data. + * + * @ingroup osc + */ +typedef struct GhosttyOscCommandImpl* GhosttyOscCommand; + +/* ---- Common value types ---- */ + +/** + * Terminal content output format. + * + * @ingroup formatter + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Plain text (no escape sequences). */ + GHOSTTY_FORMATTER_FORMAT_PLAIN, + + /** VT sequences preserving colors, styles, URLs, etc. */ + GHOSTTY_FORMATTER_FORMAT_VT, + + /** HTML with inline styles. */ + GHOSTTY_FORMATTER_FORMAT_HTML, + GHOSTTY_FORMATTER_FORMAT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyFormatterFormat; + +/** + * A borrowed byte string (pointer + length). + * + * The memory is not owned by this struct. The pointer is only valid + * for the lifetime documented by the API that produces or consumes it. + */ +typedef struct { + /** Pointer to the string bytes. */ + const uint8_t* ptr; + + /** Length of the string in bytes. */ + size_t len; +} GhosttyString; + +/** + * A caller-provided byte buffer. + * + * APIs that write to this type use `len` for the number of bytes written on + * GHOSTTY_SUCCESS and the required byte capacity on GHOSTTY_OUT_OF_SPACE. + */ +typedef struct { + /** Destination buffer for bytes. May be NULL when cap is 0 to query required size. */ + uint8_t* ptr; + + /** Capacity of ptr in bytes. */ + size_t cap; + + /** Bytes written on success, or required byte capacity on GHOSTTY_OUT_OF_SPACE. */ + size_t len; +} GhosttyBuffer; + +/** + * A surface-space position in pixels. + * + * This is not a terminal grid coordinate. It represents an x/y position in the + * rendered surface coordinate space, with (0, 0) at the top-left of the + * surface. + */ +typedef struct { + /** X position in surface pixels. */ + double x; + + /** Y position in surface pixels. */ + double y; +} GhosttySurfacePosition; + +/** + * A borrowed list of Unicode scalar values. + * + * Values are encoded as uint32_t scalar values. The memory is not owned by this + * struct. The pointer is only valid for the lifetime documented by the API that + * consumes or produces it. + * + * APIs may document special handling for NULL + len 0, such as “use defaults”. + */ +typedef struct { + /** Pointer to Unicode scalar values. */ + const uint32_t* ptr; + + /** Number of entries in ptr. */ + size_t len; +} GhosttyCodepoints; + +/** + * Initialize a sized struct to zero and set its size field. + * + * Sized structs use a `size` field as the first member for ABI + * compatibility. This macro zero-initializes the struct and sets the + * size field to `sizeof(type)`, which allows the library to detect + * which version of the struct the caller was compiled against. + * + * @param type The struct type to initialize + * @return A zero-initialized struct with the size field set + * + * Example: + * @code + * GhosttyFormatterTerminalOptions opts = GHOSTTY_INIT_SIZED(GhosttyFormatterTerminalOptions); + * opts.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN; + * opts.trim = true; + * @endcode + */ +#define GHOSTTY_INIT_SIZED(type) \ + ((type){ .size = sizeof(type) }) + +/** + * Return a pointer to a null-terminated JSON string describing the + * layout of every C API struct for the current target. + * + * This is primarily useful for language bindings that can't easily + * set C struct fields and need to do so via byte offsets. For example, + * WebAssembly modules can't share struct definitions with the host. + * + * Example (abbreviated): + * @code{.json} + * { + * "GhosttyMouseEncoderSize": { + * "size": 40, + * "align": 8, + * "fields": { + * "size": { "offset": 0, "size": 8, "type": "u64" }, + * "screen_width": { "offset": 8, "size": 4, "type": "u32" }, + * "screen_height": { "offset": 12, "size": 4, "type": "u32" }, + * "cell_width": { "offset": 16, "size": 4, "type": "u32" }, + * "cell_height": { "offset": 20, "size": 4, "type": "u32" }, + * "padding_top": { "offset": 24, "size": 4, "type": "u32" }, + * "padding_bottom": { "offset": 28, "size": 4, "type": "u32" }, + * "padding_right": { "offset": 32, "size": 4, "type": "u32" }, + * "padding_left": { "offset": 36, "size": 4, "type": "u32" } + * } + * } + * } + * @endcode + * + * The returned pointer is valid for the lifetime of the process. + * + * @return Pointer to the null-terminated JSON string. + */ +GHOSTTY_API const char *ghostty_type_json(void); + +#endif /* GHOSTTY_VT_TYPES_H */ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/wasm.h b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/wasm.h new file mode 100644 index 00000000000..e2b63e2c601 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/wasm.h @@ -0,0 +1,160 @@ +/** + * @file wasm.h + * + * WebAssembly utility functions for libghostty-vt. + */ + +#ifndef GHOSTTY_VT_WASM_H +#define GHOSTTY_VT_WASM_H + +#ifdef __wasm__ + +#include +#include +#include + +/** @defgroup wasm WebAssembly Utilities + * + * Convenience functions for allocating various types in WebAssembly builds. + * **These are only available the libghostty-vt wasm module.** + * + * Ghostty relies on pointers to various types for ABI compatibility, and + * creating those pointers in Wasm can be tedious. These functions provide + * a purely additive set of utilities that simplify memory management in + * Wasm environments without changing the core C library API. + * + * @note These functions always use the default allocator. If you need + * custom allocation strategies, you should allocate types manually using + * your custom allocator. This is a very rare use case in the WebAssembly + * world so these are optimized for simplicity. + * + * ## Example Usage + * + * Here's a simple example of using the Wasm utilities with the key encoder: + * + * @code + * const { exports } = wasmInstance; + * const view = new DataView(wasmMemory.buffer); + * + * // Create key encoder + * const encoderPtr = exports.ghostty_wasm_alloc_opaque(); + * exports.ghostty_key_encoder_new(null, encoderPtr); + * const encoder = view.getUint32(encoder, true); + * + * // Configure encoder with Kitty protocol flags + * const flagsPtr = exports.ghostty_wasm_alloc_u8(); + * view.setUint8(flagsPtr, 0x1F); + * exports.ghostty_key_encoder_setopt(encoder, 5, flagsPtr); + * + * // Allocate output buffer and size pointer + * const bufferSize = 32; + * const bufPtr = exports.ghostty_wasm_alloc_u8_array(bufferSize); + * const writtenPtr = exports.ghostty_wasm_alloc_usize(); + * + * // Encode the key event + * exports.ghostty_key_encoder_encode( + * encoder, eventPtr, bufPtr, bufferSize, writtenPtr + * ); + * + * // Read encoded output + * const bytesWritten = view.getUint32(writtenPtr, true); + * const encoded = new Uint8Array(wasmMemory.buffer, bufPtr, bytesWritten); + * @endcode + * + * @remark The code above is pretty ugly! This is the lowest level interface + * to the libghostty-vt Wasm module. In practice, this should be wrapped + * in a higher-level API that abstracts away all this. + * + * @{ + */ + +/** + * Allocate an opaque pointer. This can be used for any opaque pointer + * types such as GhosttyKeyEncoder, GhosttyKeyEvent, etc. + * + * @return Pointer to allocated opaque pointer, or NULL if allocation failed + * @ingroup wasm + */ +GHOSTTY_API void** ghostty_wasm_alloc_opaque(void); + +/** + * Free an opaque pointer allocated by ghostty_wasm_alloc_opaque(). + * + * @param ptr Pointer to free, or NULL (NULL is safely ignored) + * @ingroup wasm + */ +GHOSTTY_API void ghostty_wasm_free_opaque(void **ptr); + +/** + * Allocate an array of uint8_t values. + * + * @param len Number of uint8_t elements to allocate + * @return Pointer to allocated array, or NULL if allocation failed + * @ingroup wasm + */ +GHOSTTY_API uint8_t* ghostty_wasm_alloc_u8_array(size_t len); + +/** + * Free an array allocated by ghostty_wasm_alloc_u8_array(). + * + * @param ptr Pointer to the array to free, or NULL (NULL is safely ignored) + * @param len Length of the array (must match the length passed to alloc) + * @ingroup wasm + */ +GHOSTTY_API void ghostty_wasm_free_u8_array(uint8_t *ptr, size_t len); + +/** + * Allocate an array of uint16_t values. + * + * @param len Number of uint16_t elements to allocate + * @return Pointer to allocated array, or NULL if allocation failed + * @ingroup wasm + */ +GHOSTTY_API uint16_t* ghostty_wasm_alloc_u16_array(size_t len); + +/** + * Free an array allocated by ghostty_wasm_alloc_u16_array(). + * + * @param ptr Pointer to the array to free, or NULL (NULL is safely ignored) + * @param len Length of the array (must match the length passed to alloc) + * @ingroup wasm + */ +GHOSTTY_API void ghostty_wasm_free_u16_array(uint16_t *ptr, size_t len); + +/** + * Allocate a single uint8_t value. + * + * @return Pointer to allocated uint8_t, or NULL if allocation failed + * @ingroup wasm + */ +GHOSTTY_API uint8_t* ghostty_wasm_alloc_u8(void); + +/** + * Free a uint8_t allocated by ghostty_wasm_alloc_u8(). + * + * @param ptr Pointer to free, or NULL (NULL is safely ignored) + * @ingroup wasm + */ +GHOSTTY_API void ghostty_wasm_free_u8(uint8_t *ptr); + +/** + * Allocate a single size_t value. + * + * @return Pointer to allocated size_t, or NULL if allocation failed + * @ingroup wasm + */ +GHOSTTY_API size_t* ghostty_wasm_alloc_usize(void); + +/** + * Free a size_t allocated by ghostty_wasm_alloc_usize(). + * + * @param ptr Pointer to free, or NULL (NULL is safely ignored) + * @ingroup wasm + */ +GHOSTTY_API void ghostty_wasm_free_usize(size_t *ptr); + +/** @} */ + +#endif /* __wasm__ */ + +#endif /* GHOSTTY_VT_WASM_H */ diff --git a/apps/mobile/modules/t3-terminal/android/.gitignore b/apps/mobile/modules/t3-terminal/android/.gitignore new file mode 100644 index 00000000000..3166b90b042 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/android/.gitignore @@ -0,0 +1 @@ +.cxx/ diff --git a/apps/mobile/modules/t3-terminal/android/build.gradle b/apps/mobile/modules/t3-terminal/android/build.gradle index 90c0d4fc21e..0da0777cf4f 100644 --- a/apps/mobile/modules/t3-terminal/android/build.gradle +++ b/apps/mobile/modules/t3-terminal/android/build.gradle @@ -6,11 +6,25 @@ version = '0.0.0' android { namespace 'expo.modules.t3terminal' + ndkVersion rootProject.ext.ndkVersion compileSdk rootProject.ext.compileSdkVersion defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion + + externalNativeBuild { + cmake { + cppFlags '-std=c++17 -Wall -Wextra -Werror' + } + } + } + + externalNativeBuild { + cmake { + path 'src/main/cpp/CMakeLists.txt' + version '3.22.1' + } } } diff --git a/apps/mobile/modules/t3-terminal/android/src/main/cpp/CMakeLists.txt b/apps/mobile/modules/t3-terminal/android/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000000..89f7d8c5e5d --- /dev/null +++ b/apps/mobile/modules/t3-terminal/android/src/main/cpp/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.22.1) + +project(t3terminal LANGUAGES CXX) + +add_library(ghostty-vt SHARED IMPORTED) +set_target_properties( + ghostty-vt + PROPERTIES IMPORTED_LOCATION + "${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libghostty-vt.so" +) + +add_library(t3terminal SHARED t3_terminal_jni.cpp) + +target_include_directories( + t3terminal + PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../../../../Vendor/libghostty-vt/include" +) + +target_link_libraries(t3terminal PRIVATE ghostty-vt log) diff --git a/apps/mobile/modules/t3-terminal/android/src/main/cpp/t3_terminal_jni.cpp b/apps/mobile/modules/t3-terminal/android/src/main/cpp/t3_terminal_jni.cpp new file mode 100644 index 00000000000..3faf5e230fb --- /dev/null +++ b/apps/mobile/modules/t3-terminal/android/src/main/cpp/t3_terminal_jni.cpp @@ -0,0 +1,416 @@ +#include + +#include +#include +#include +#include +#include + +#include + +namespace { + +constexpr uint32_t kSnapshotMagic = 0x54563354; // "T3VT" in little endian. +constexpr uint16_t kSnapshotVersion = 1; +constexpr size_t kMaxScrollbackRows = 10000; + +enum CellFlag : uint16_t { + kBold = 1 << 0, + kItalic = 1 << 1, + kFaint = 1 << 2, + kInverse = 1 << 3, + kInvisible = 1 << 4, + kStrikethrough = 1 << 5, + kOverline = 1 << 6, + kUnderline = 1 << 7, + kSelected = 1 << 8, +}; + +struct Session { + GhosttyTerminal terminal = nullptr; + GhosttyRenderState render_state = nullptr; + GhosttyRenderStateRowIterator row_iterator = nullptr; + GhosttyRenderStateRowCells row_cells = nullptr; + std::vector responses; + std::mutex mutex; +}; + +class ByteWriter { + public: + explicit ByteWriter(size_t capacity) { bytes_.reserve(capacity); } + + void U8(uint8_t value) { bytes_.push_back(value); } + + void U16(uint16_t value) { + U8(static_cast(value)); + U8(static_cast(value >> 8)); + } + + void U32(uint32_t value) { + U16(static_cast(value)); + U16(static_cast(value >> 16)); + } + + void Bytes(const std::vector& value) { + bytes_.insert(bytes_.end(), value.begin(), value.end()); + } + + std::vector Take() { return std::move(bytes_); } + + private: + std::vector bytes_; +}; + +Session* FromHandle(jlong handle) { + return reinterpret_cast(static_cast(handle)); +} + +jbyteArray ToJavaBytes(JNIEnv* env, const std::vector& bytes) { + auto result = env->NewByteArray(static_cast(bytes.size())); + if (result != nullptr && !bytes.empty()) { + env->SetByteArrayRegion(result, 0, static_cast(bytes.size()), + reinterpret_cast(bytes.data())); + } + return result; +} + +GhosttyColorRgb RgbFromArgb(jint color) { + const auto value = static_cast(color); + return { + .r = static_cast(value >> 16), + .g = static_cast(value >> 8), + .b = static_cast(value), + }; +} + +uint32_t ArgbFromRgb(GhosttyColorRgb color) { + return 0xFF000000U | (static_cast(color.r) << 16U) | + (static_cast(color.g) << 8U) | color.b; +} + +GhosttyColorRgb Blend(GhosttyColorRgb foreground, GhosttyColorRgb background, + uint8_t foreground_weight) { + const auto blend = [foreground_weight](uint8_t front, uint8_t back) { + const uint16_t back_weight = 255 - foreground_weight; + return static_cast((front * foreground_weight + back * back_weight) / 255); + }; + return { + .r = blend(foreground.r, background.r), + .g = blend(foreground.g, background.g), + .b = blend(foreground.b, background.b), + }; +} + +void AppendUtf8(std::vector* output, uint32_t codepoint) { + if (codepoint <= 0x7F) { + output->push_back(static_cast(codepoint)); + } else if (codepoint <= 0x7FF) { + output->push_back(static_cast(0xC0 | (codepoint >> 6))); + output->push_back(static_cast(0x80 | (codepoint & 0x3F))); + } else if (codepoint <= 0xFFFF && !(codepoint >= 0xD800 && codepoint <= 0xDFFF)) { + output->push_back(static_cast(0xE0 | (codepoint >> 12))); + output->push_back(static_cast(0x80 | ((codepoint >> 6) & 0x3F))); + output->push_back(static_cast(0x80 | (codepoint & 0x3F))); + } else if (codepoint <= 0x10FFFF) { + output->push_back(static_cast(0xF0 | (codepoint >> 18))); + output->push_back(static_cast(0x80 | ((codepoint >> 12) & 0x3F))); + output->push_back(static_cast(0x80 | ((codepoint >> 6) & 0x3F))); + output->push_back(static_cast(0x80 | (codepoint & 0x3F))); + } +} + +void OnWritePty(GhosttyTerminal, void* userdata, const uint8_t* data, size_t len) { + auto* session = static_cast(userdata); + if (session == nullptr || data == nullptr || len == 0) return; + session->responses.insert(session->responses.end(), data, data + len); +} + +void ApplyTheme(Session* session, jint foreground, jint background, jint cursor, + JNIEnv* env, jintArray palette_array) { + auto foreground_rgb = RgbFromArgb(foreground); + auto background_rgb = RgbFromArgb(background); + auto cursor_rgb = RgbFromArgb(cursor); + ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND, + &foreground_rgb); + ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND, + &background_rgb); + ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_COLOR_CURSOR, &cursor_rgb); + + if (palette_array == nullptr) return; + const auto palette_length = env->GetArrayLength(palette_array); + if (palette_length <= 0) return; + + GhosttyColorRgb palette[256]; + if (ghostty_terminal_get(session->terminal, + GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT, + palette) != GHOSTTY_SUCCESS) { + return; + } + + const auto copied_length = std::min(palette_length, 256); + std::vector colors(static_cast(copied_length)); + env->GetIntArrayRegion(palette_array, 0, copied_length, colors.data()); + for (jsize index = 0; index < copied_length; ++index) { + palette[index] = RgbFromArgb(colors[index]); + } + ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_COLOR_PALETTE, palette); +} + +void FreeSession(Session* session) { + if (session == nullptr) return; + ghostty_render_state_row_cells_free(session->row_cells); + ghostty_render_state_row_iterator_free(session->row_iterator); + ghostty_render_state_free(session->render_state); + ghostty_terminal_free(session->terminal); + delete session; +} + +std::vector DrainResponses(Session* session) { + std::vector responses; + responses.swap(session->responses); + return responses; +} + +uint16_t StyleFlags(const GhosttyStyle& style, bool selected) { + uint16_t flags = 0; + if (style.bold) flags |= kBold; + if (style.italic) flags |= kItalic; + if (style.faint) flags |= kFaint; + if (style.inverse) flags |= kInverse; + if (style.invisible) flags |= kInvisible; + if (style.strikethrough) flags |= kStrikethrough; + if (style.overline) flags |= kOverline; + if (style.underline != 0) flags |= kUnderline; + if (selected) flags |= kSelected; + return flags; +} + +} // namespace + +extern "C" JNIEXPORT jlong JNICALL +Java_expo_modules_t3terminal_GhosttyBridge_nativeCreate( + JNIEnv* env, jclass, jint cols, jint rows, jint cell_width, jint cell_height, + jint foreground, jint background, jint cursor, jintArray palette) { + auto* session = new Session(); + GhosttyTerminalOptions options = { + .cols = static_cast(std::clamp(cols, 1, 65535)), + .rows = static_cast(std::clamp(rows, 1, 65535)), + .max_scrollback = kMaxScrollbackRows, + }; + if (ghostty_terminal_new(nullptr, &session->terminal, options) != GHOSTTY_SUCCESS || + ghostty_render_state_new(nullptr, &session->render_state) != GHOSTTY_SUCCESS || + ghostty_render_state_row_iterator_new(nullptr, &session->row_iterator) != GHOSTTY_SUCCESS || + ghostty_render_state_row_cells_new(nullptr, &session->row_cells) != GHOSTTY_SUCCESS) { + FreeSession(session); + return 0; + } + + ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_USERDATA, session); + ghostty_terminal_set(session->terminal, GHOSTTY_TERMINAL_OPT_WRITE_PTY, + reinterpret_cast(OnWritePty)); + ApplyTheme(session, foreground, background, cursor, env, palette); + ghostty_terminal_resize(session->terminal, options.cols, options.rows, + static_cast(std::max(cell_width, 1)), + static_cast(std::max(cell_height, 1))); + return static_cast(reinterpret_cast(session)); +} + +extern "C" JNIEXPORT void JNICALL +Java_expo_modules_t3terminal_GhosttyBridge_nativeDestroy(JNIEnv*, jclass, jlong handle) { + FreeSession(FromHandle(handle)); +} + +extern "C" JNIEXPORT jbyteArray JNICALL +Java_expo_modules_t3terminal_GhosttyBridge_nativeFeed(JNIEnv* env, jclass, jlong handle, + jbyteArray data) { + auto* session = FromHandle(handle); + if (session == nullptr || data == nullptr) return env->NewByteArray(0); + std::lock_guard lock(session->mutex); + const auto length = env->GetArrayLength(data); + std::vector bytes(static_cast(length)); + if (length > 0) { + env->GetByteArrayRegion(data, 0, length, reinterpret_cast(bytes.data())); + ghostty_terminal_vt_write(session->terminal, bytes.data(), bytes.size()); + } + return ToJavaBytes(env, DrainResponses(session)); +} + +extern "C" JNIEXPORT jbyteArray JNICALL +Java_expo_modules_t3terminal_GhosttyBridge_nativeResize( + JNIEnv* env, jclass, jlong handle, jint cols, jint rows, jint cell_width, jint cell_height) { + auto* session = FromHandle(handle); + if (session == nullptr) return env->NewByteArray(0); + std::lock_guard lock(session->mutex); + ghostty_terminal_resize(session->terminal, + static_cast(std::clamp(cols, 1, 65535)), + static_cast(std::clamp(rows, 1, 65535)), + static_cast(std::max(cell_width, 1)), + static_cast(std::max(cell_height, 1))); + return ToJavaBytes(env, DrainResponses(session)); +} + +extern "C" JNIEXPORT void JNICALL +Java_expo_modules_t3terminal_GhosttyBridge_nativeScroll(JNIEnv*, jclass, jlong handle, + jint rows) { + auto* session = FromHandle(handle); + if (session == nullptr || rows == 0) return; + std::lock_guard lock(session->mutex); + GhosttyTerminalScrollViewport scroll = { + .tag = GHOSTTY_SCROLL_VIEWPORT_DELTA, + .value = {.delta = rows}, + }; + ghostty_terminal_scroll_viewport(session->terminal, scroll); +} + +extern "C" JNIEXPORT void JNICALL +Java_expo_modules_t3terminal_GhosttyBridge_nativeSetTheme( + JNIEnv* env, jclass, jlong handle, jint foreground, jint background, jint cursor, + jintArray palette) { + auto* session = FromHandle(handle); + if (session == nullptr) return; + std::lock_guard lock(session->mutex); + ApplyTheme(session, foreground, background, cursor, env, palette); +} + +extern "C" JNIEXPORT jbyteArray JNICALL +Java_expo_modules_t3terminal_GhosttyBridge_nativeSnapshot(JNIEnv* env, jclass, + jlong handle) { + auto* session = FromHandle(handle); + if (session == nullptr) return env->NewByteArray(0); + std::lock_guard lock(session->mutex); + + if (ghostty_render_state_update(session->render_state, session->terminal) != GHOSTTY_SUCCESS) { + return env->NewByteArray(0); + } + + uint16_t cols = 0; + uint16_t rows = 0; + bool cursor_visible = false; + bool cursor_in_viewport = false; + bool cursor_blinking = false; + uint16_t cursor_x = 0xFFFF; + uint16_t cursor_y = 0xFFFF; + GhosttyRenderStateCursorVisualStyle cursor_style = + GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK; + ghostty_render_state_get(session->render_state, GHOSTTY_RENDER_STATE_DATA_COLS, &cols); + ghostty_render_state_get(session->render_state, GHOSTTY_RENDER_STATE_DATA_ROWS, &rows); + ghostty_render_state_get(session->render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE, + &cursor_visible); + ghostty_render_state_get(session->render_state, + GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE, + &cursor_in_viewport); + ghostty_render_state_get(session->render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING, + &cursor_blinking); + ghostty_render_state_get(session->render_state, + GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE, &cursor_style); + if (cursor_in_viewport) { + ghostty_render_state_get(session->render_state, + GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X, &cursor_x); + ghostty_render_state_get(session->render_state, + GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y, &cursor_y); + } + + GhosttyRenderStateColors colors{}; + colors.size = sizeof(colors); + if (ghostty_render_state_colors_get(session->render_state, &colors) != GHOSTTY_SUCCESS) { + return env->NewByteArray(0); + } + const auto cursor_color = colors.cursor_has_value ? colors.cursor : colors.foreground; + + ByteWriter writer(32 + static_cast(cols) * rows * 14); + writer.U32(kSnapshotMagic); + writer.U16(kSnapshotVersion); + writer.U16(cols); + writer.U16(rows); + writer.U16(cursor_x); + writer.U16(cursor_y); + writer.U8(cursor_visible && cursor_in_viewport ? 1 : 0); + writer.U8(static_cast(cursor_style)); + writer.U8(cursor_blinking ? 1 : 0); + writer.U8(0); + writer.U32(ArgbFromRgb(colors.foreground)); + writer.U32(ArgbFromRgb(colors.background)); + writer.U32(ArgbFromRgb(cursor_color)); + + if (ghostty_render_state_get(session->render_state, + GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, + &session->row_iterator) != GHOSTTY_SUCCESS) { + return env->NewByteArray(0); + } + + uint16_t written_rows = 0; + while (written_rows < rows && + ghostty_render_state_row_iterator_next(session->row_iterator)) { + if (ghostty_render_state_row_get(session->row_iterator, + GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, + &session->row_cells) != GHOSTTY_SUCCESS) { + break; + } + + uint16_t written_cols = 0; + while (written_cols < cols && ghostty_render_state_row_cells_next(session->row_cells)) { + GhosttyStyle style{}; + style.size = sizeof(style); + bool selected = false; + GhosttyColorRgb foreground = colors.foreground; + GhosttyColorRgb background = colors.background; + ghostty_render_state_row_cells_get(session->row_cells, + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE, + &style); + ghostty_render_state_row_cells_get(session->row_cells, + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_SELECTED, + &selected); + ghostty_render_state_row_cells_get(session->row_cells, + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR, + &foreground); + ghostty_render_state_row_cells_get(session->row_cells, + GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR, + &background); + if (style.inverse) std::swap(foreground, background); + if (style.faint) foreground = Blend(foreground, background, 155); + + uint32_t grapheme_count = 0; + ghostty_render_state_row_cells_get( + session->row_cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN, + &grapheme_count); + std::vector utf8; + if (grapheme_count > 0) { + std::vector codepoints(grapheme_count); + if (ghostty_render_state_row_cells_get( + session->row_cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, + codepoints.data()) == GHOSTTY_SUCCESS) { + utf8.reserve(grapheme_count * 4); + for (const auto codepoint : codepoints) AppendUtf8(&utf8, codepoint); + } + } + + const auto text_length = static_cast(std::min(utf8.size(), 65535)); + writer.U32(ArgbFromRgb(foreground)); + writer.U32(ArgbFromRgb(background)); + writer.U16(StyleFlags(style, selected)); + writer.U16(text_length); + if (text_length != utf8.size()) utf8.resize(text_length); + writer.Bytes(utf8); + ++written_cols; + } + + while (written_cols++ < cols) { + writer.U32(ArgbFromRgb(colors.foreground)); + writer.U32(ArgbFromRgb(colors.background)); + writer.U16(0); + writer.U16(0); + } + ++written_rows; + } + + while (written_rows++ < rows) { + for (uint16_t column = 0; column < cols; ++column) { + writer.U32(ArgbFromRgb(colors.foreground)); + writer.U32(ArgbFromRgb(colors.background)); + writer.U16(0); + writer.U16(0); + } + } + + return ToJavaBytes(env, writer.Take()); +} diff --git a/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/GhosttyBridge.kt b/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/GhosttyBridge.kt new file mode 100644 index 00000000000..6746337e279 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/GhosttyBridge.kt @@ -0,0 +1,45 @@ +package expo.modules.t3terminal + +internal object GhosttyBridge { + init { + System.loadLibrary("ghostty-vt") + System.loadLibrary("t3terminal") + } + + @JvmStatic + external fun nativeCreate( + cols: Int, + rows: Int, + cellWidth: Int, + cellHeight: Int, + foreground: Int, + background: Int, + cursor: Int, + palette: IntArray, + ): Long + + @JvmStatic external fun nativeDestroy(handle: Long) + @JvmStatic external fun nativeFeed(handle: Long, data: ByteArray): ByteArray + + @JvmStatic + external fun nativeResize( + handle: Long, + cols: Int, + rows: Int, + cellWidth: Int, + cellHeight: Int, + ): ByteArray + + @JvmStatic external fun nativeScroll(handle: Long, rows: Int) + + @JvmStatic + external fun nativeSetTheme( + handle: Long, + foreground: Int, + background: Int, + cursor: Int, + palette: IntArray, + ) + + @JvmStatic external fun nativeSnapshot(handle: Long): ByteArray +} diff --git a/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt b/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt index abb3982be1e..d95ecc39d38 100644 --- a/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt +++ b/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt @@ -41,6 +41,10 @@ class T3TerminalModule : Module() { } Events("onInput", "onResize") + + OnViewDestroys { view: T3TerminalView -> + view.cleanup() + } } } } diff --git a/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt b/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt index ec85d0ba070..1dd79276670 100644 --- a/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt +++ b/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt @@ -3,65 +3,70 @@ package expo.modules.t3terminal import android.content.Context import android.graphics.Color import android.graphics.Typeface -import android.view.View +import android.text.Editable +import android.text.InputType +import android.text.TextWatcher +import android.view.KeyEvent import android.view.ViewGroup -import android.view.inputmethod.InputMethodManager import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager import android.widget.EditText -import android.widget.LinearLayout -import android.widget.ScrollView -import android.widget.TextView -import androidx.core.widget.doAfterTextChanged +import android.widget.FrameLayout import expo.modules.kotlin.AppContext -import expo.modules.kotlin.views.ExpoView import expo.modules.kotlin.viewevent.EventDispatcher +import expo.modules.kotlin.views.ExpoView import kotlin.math.max -import kotlin.math.min class T3TerminalView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { - private val container = LinearLayout(context) - private val scrollView = ScrollView(context) - private val textView = TextView(context) + private val container = FrameLayout(context) + private val terminalCanvas = TerminalCanvasView(context) private val inputView = EditText(context) private val onInput by EventDispatcher() private val onResize by EventDispatcher() - private var lastWidth = 0 - private var lastHeight = 0 + private var terminalHandle = 0L + private var fedBuffer = "" + private var cols = 0 + private var rows = 0 private var clearingInput = false + private var isCleanedUp = false private var backgroundColorValue = Color.parseColor("#24292E") private var foregroundColorValue = Color.parseColor("#D1D5DA") private var mutedForegroundColorValue = Color.parseColor("#959DA5") + private var cursorColorValue = Color.parseColor("#009FFF") + private var paletteColors = IntArray(0) var terminalKey: String = "" set(value) { + if (field == value) return field = value contentDescription = "t3-terminal-$value" + recreateTerminal() } var initialBuffer: String = "" set(value) { + if (field == value) return field = value - textView.text = value.ifEmpty { "$ " } - scrollView.post { - scrollView.fullScroll(View.FOCUS_DOWN) - } + feedPendingBuffer() } var fontSize: Float = 10f set(value) { field = value - textView.textSize = value + terminalCanvas.fontSizeSp = value inputView.textSize = max(value, 13f) emitResize() } var appearanceScheme: String = "dark" + + var themeConfig: String = "" set(value) { field = value + parseThemeConfig(value) + applyTheme() } - var themeConfig: String = "" - var backgroundColorHex: String = "#24292E" set(value) { field = value @@ -80,121 +85,259 @@ class T3TerminalView(context: Context, appContext: AppContext) : ExpoView(contex set(value) { field = value mutedForegroundColorValue = parseColor(value, mutedForegroundColorValue) - applyTheme() } init { + terminalCanvas.fontSizeSp = fontSize + terminalCanvas.onRequestKeyboard = { requestKeyboardFocus() } + terminalCanvas.onScrollRows = { delta -> + if (terminalHandle != 0L) { + GhosttyBridge.nativeScroll(terminalHandle, delta) + renderSnapshot() + } + } + terminalCanvas.onCellMetricsChanged = { emitResize() } + + configureInputView() + container.addView( + terminalCanvas, + FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ), + ) + container.addView(inputView, FrameLayout.LayoutParams(1, 1)) + addView( + container, + LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT), + ) applyTheme() - container.orientation = LinearLayout.VERTICAL - textView.typeface = Typeface.MONOSPACE - textView.textSize = fontSize - textView.setPadding(8, 8, 8, 8) - textView.text = "$ " + } + + override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) { + super.onSizeChanged(width, height, oldWidth, oldHeight) + if (width != oldWidth || height != oldHeight) emitResize() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val childWidthSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY) + val childHeightSpec = MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY) + container.measure(childWidthSpec, childHeightSpec) + } + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + container.layout(0, 0, right - left, bottom - top) + if (changed) emitResize() + } + + fun cleanup() { + if (isCleanedUp) return + isCleanedUp = true + inputView.setOnEditorActionListener(null) + terminalCanvas.onScrollRows = null + terminalCanvas.onRequestKeyboard = null + terminalCanvas.onCellMetricsChanged = null + destroyTerminal() + } + + private fun configureInputView() { inputView.setSingleLine(true) inputView.setTextColor(Color.TRANSPARENT) inputView.setHintTextColor(Color.TRANSPARENT) inputView.setBackgroundColor(Color.TRANSPARENT) inputView.typeface = Typeface.MONOSPACE inputView.textSize = max(fontSize, 13f) - inputView.hint = "" - inputView.alpha = 0.02f - inputView.imeOptions = EditorInfo.IME_ACTION_SEND + inputView.alpha = 0.01f + inputView.isFocusableInTouchMode = true + inputView.imeOptions = EditorInfo.IME_ACTION_SEND or + EditorInfo.IME_FLAG_NO_EXTRACT_UI or + EditorInfo.IME_FLAG_NO_FULLSCREEN or + EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + inputView.inputType = InputType.TYPE_CLASS_TEXT or + InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD or + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS inputView.setPadding(0, 0, 0, 0) - inputView.setOnFocusChangeListener { _, hasFocus -> - if (hasFocus) { - showKeyboard() + inputView.setOnEditorActionListener { _, actionId, event -> + val isKeyUp = event?.action == KeyEvent.ACTION_UP + val isImeSend = actionId == EditorInfo.IME_ACTION_SEND && !isKeyUp + val isHardwareEnter = event?.keyCode == KeyEvent.KEYCODE_ENTER && + event.action == KeyEvent.ACTION_DOWN + val isEnter = isImeSend || isHardwareEnter + if (isEnter) { + onInput(mapOf("data" to "\n")) + true + } else { + false } } - inputView.setOnEditorActionListener { view, actionId, _ -> - if (actionId != EditorInfo.IME_ACTION_SEND) return@setOnEditorActionListener false - onInput(mapOf("data" to "\n")) - true - } inputView.setOnKeyListener { _, keyCode, event -> - if (event.action != android.view.KeyEvent.ACTION_DOWN) return@setOnKeyListener false - when (keyCode) { - android.view.KeyEvent.KEYCODE_DEL -> { - onInput(mapOf("data" to "\u007F")) - true - } - else -> false + if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_DEL) { + onInput(mapOf("data" to "\u007F")) + true + } else { + false } } - inputView.doAfterTextChanged { editable -> - if (clearingInput) return@doAfterTextChanged - val text = editable?.toString().orEmpty() - if (text.isEmpty()) return@doAfterTextChanged - onInput(mapOf("data" to text)) - clearingInput = true - inputView.text?.clear() - clearingInput = false - } - - textView.setOnClickListener { requestKeyboardFocus() } - scrollView.setOnClickListener { requestKeyboardFocus() } - container.setOnClickListener { requestKeyboardFocus() } - isClickable = true - setOnClickListener { requestKeyboardFocus() } - - scrollView.addView( - textView, - LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT), - ) - container.addView( - scrollView, - LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - 0, - 1f, - ), - ) - container.addView( - inputView, - LinearLayout.LayoutParams(1, 1), + inputView.addTextChangedListener( + object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (clearingInput || s == null || count <= 0) return + val end = (start + count).coerceAtMost(s.length) + if (start >= end) return + val insertedText = s.subSequence(start, end).toString() + if (insertedText.isNotEmpty()) { + onInput(mapOf("data" to insertedText)) + } + } + + override fun afterTextChanged(editable: Editable?) { + if (clearingInput || editable.isNullOrEmpty()) return + clearingInput = true + editable.clear() + clearingInput = false + } + }, ) - addView( - container, - LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT), + } + + private fun emitResize() { + if ( + width <= 0 || + height <= 0 || + terminalCanvas.width <= 0 || + terminalCanvas.height <= 0 || + isCleanedUp + ) return + val nextCols = (terminalCanvas.usableWidth() / terminalCanvas.cellWidthPx) + .toInt() + .coerceIn(2, 400) + val nextRows = (terminalCanvas.usableHeight() / terminalCanvas.cellHeightPx) + .toInt() + .coerceIn(2, 200) + if (nextCols == cols && nextRows == rows && terminalHandle != 0L) return + cols = nextCols + rows = nextRows + val response = if (terminalHandle == 0L) { + createTerminal() + ByteArray(0) + } else { + GhosttyBridge.nativeResize( + terminalHandle, + cols, + rows, + terminalCanvas.cellWidthPx.toInt(), + terminalCanvas.cellHeightPx.toInt(), + ) + } + emitResponse(response) + onResize(mapOf("cols" to cols, "rows" to rows)) + feedPendingBuffer() + renderSnapshot() + } + + private fun createTerminal() { + if (terminalHandle != 0L || cols <= 0 || rows <= 0 || isCleanedUp) return + terminalHandle = GhosttyBridge.nativeCreate( + cols, + rows, + terminalCanvas.cellWidthPx.toInt(), + terminalCanvas.cellHeightPx.toInt(), + foregroundColorValue, + backgroundColorValue, + cursorColorValue, + paletteColors, ) + fedBuffer = "" + } - post { - requestKeyboardFocus() + private fun recreateTerminal() { + if (terminalHandle == 0L) return + destroyTerminal() + createTerminal() + feedPendingBuffer() + renderSnapshot() + } + + private fun destroyTerminal() { + if (terminalHandle == 0L) return + GhosttyBridge.nativeDestroy(terminalHandle) + terminalHandle = 0L + fedBuffer = "" + } + + private fun feedPendingBuffer() { + if (terminalHandle == 0L || initialBuffer == fedBuffer) return + if (!initialBuffer.startsWith(fedBuffer)) { + recreateTerminal() + if (terminalHandle == 0L) return + } + val suffix = initialBuffer.substring(fedBuffer.length) + if (suffix.isNotEmpty()) { + emitResponse(GhosttyBridge.nativeFeed(terminalHandle, suffix.toByteArray(Charsets.UTF_8))) } + fedBuffer = initialBuffer + renderSnapshot() } - override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) { - super.onSizeChanged(width, height, oldWidth, oldHeight) - if (width == lastWidth && height == lastHeight) return - lastWidth = width - lastHeight = height - emitResize() + private fun renderSnapshot() { + if (terminalHandle == 0L) return + TerminalFrame.decode(GhosttyBridge.nativeSnapshot(terminalHandle))?.let(terminalCanvas::setFrame) } - private fun emitResize() { - if (width <= 0 || height <= 0) return - val density = resources.displayMetrics.scaledDensity - val fontPx = max(fontSize * density, 1f) - val cols = max(20, min(400, (width / (fontPx * 0.62f)).toInt())) - val terminalHeight = max(height - inputView.height, 0) - val rows = max(5, min(200, (terminalHeight / (fontPx * 1.35f)).toInt())) - onResize(mapOf("cols" to cols, "rows" to rows)) + private fun emitResponse(response: ByteArray) { + if (response.isNotEmpty()) { + onInput(mapOf("data" to String(response, Charsets.UTF_8))) + } } private fun requestKeyboardFocus() { inputView.requestFocus() - showKeyboard() + val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + inputMethodManager?.showSoftInput(inputView, InputMethodManager.SHOW_IMPLICIT) } private fun applyTheme() { setBackgroundColor(backgroundColorValue) container.setBackgroundColor(backgroundColorValue) - scrollView.setBackgroundColor(backgroundColorValue) - textView.setTextColor(foregroundColorValue) - textView.setBackgroundColor(backgroundColorValue) - inputView.setTextColor(Color.TRANSPARENT) - inputView.setHintTextColor(mutedForegroundColorValue) - inputView.setBackgroundColor(Color.TRANSPARENT) + terminalCanvas.setBackgroundColor(backgroundColorValue) + if (terminalHandle != 0L) { + GhosttyBridge.nativeSetTheme( + terminalHandle, + foregroundColorValue, + backgroundColorValue, + cursorColorValue, + paletteColors, + ) + renderSnapshot() + } + } + + private fun parseThemeConfig(config: String) { + val palette = sortedMapOf() + for (line in config.lineSequence()) { + val parts = line.split('=', limit = 2) + if (parts.size != 2) continue + val key = parts[0].trim() + val value = parts[1].trim() + when (key) { + "cursor-color" -> cursorColorValue = parseColor(value, cursorColorValue) + "palette" -> { + val paletteParts = value.split('=', limit = 2) + val index = paletteParts.firstOrNull()?.trim()?.toIntOrNull() ?: continue + val color = paletteParts.getOrNull(1)?.trim() ?: continue + if (index in 0..255) palette[index] = parseColor(color, foregroundColorValue) + } + } + } + if (palette.isNotEmpty()) { + val lastIndex = palette.lastKey() + paletteColors = IntArray(lastIndex + 1) { index -> + palette[index] ?: foregroundColorValue + } + } } private fun parseColor(value: String, fallback: Int): Int = @@ -203,9 +346,4 @@ class T3TerminalView(context: Context, appContext: AppContext) : ExpoView(contex } catch (_: IllegalArgumentException) { fallback } - - private fun showKeyboard() { - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.showSoftInput(inputView, InputMethodManager.SHOW_IMPLICIT) - } } diff --git a/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/TerminalCanvasView.kt b/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/TerminalCanvasView.kt new file mode 100644 index 00000000000..e465a11d550 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/TerminalCanvasView.kt @@ -0,0 +1,246 @@ +package expo.modules.t3terminal + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Typeface +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import kotlin.math.ceil +import kotlin.math.max + +internal class TerminalCanvasView(context: Context) : View(context) { + companion object { + const val FLAG_BOLD = 1 shl 0 + const val FLAG_ITALIC = 1 shl 1 + const val FLAG_INVISIBLE = 1 shl 4 + const val FLAG_STRIKETHROUGH = 1 shl 5 + const val FLAG_OVERLINE = 1 shl 6 + const val FLAG_UNDERLINE = 1 shl 7 + const val FLAG_SELECTED = 1 shl 8 + } + + private val density = resources.displayMetrics.density + private val scaledDensity = density * resources.configuration.fontScale + private val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.SUBPIXEL_TEXT_FLAG) + private val regularTypeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL) + private val boldTypeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD) + private val italicTypeface = Typeface.create(Typeface.MONOSPACE, Typeface.ITALIC) + private val boldItalicTypeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD_ITALIC) + private val gestureDetector = GestureDetector(context, TerminalGestureListener()) + private val contentPadding = 8f * density + private var frame: TerminalFrame? = null + private var scrollRemainder = 0f + private var cursorOn = true + private val cursorBlink = object : Runnable { + override fun run() { + val currentFrame = frame ?: return + if (!currentFrame.cursorBlinking || !currentFrame.cursorVisible) return + cursorOn = !cursorOn + invalidate() + postDelayed(this, 500) + } + } + + var onScrollRows: ((Int) -> Unit)? = null + var onRequestKeyboard: (() -> Unit)? = null + var onCellMetricsChanged: (() -> Unit)? = null + + var fontSizeSp: Float = 10f + set(value) { + if (field == value) return + field = value + updateCellMetrics() + } + + var cellWidthPx: Float = 1f + private set + var cellHeightPx: Float = 1f + private set + private var baselineOffsetPx: Float = 1f + + init { + isClickable = true + isFocusable = true + isFocusableInTouchMode = true + paint.typeface = regularTypeface + updateCellMetrics() + } + + fun setFrame(value: TerminalFrame) { + frame = value + cursorOn = true + removeCallbacks(cursorBlink) + if (value.cursorBlinking && value.cursorVisible) postDelayed(cursorBlink, 500) + invalidate() + } + + fun usableWidth(): Float = max(width - contentPadding * 2f, 1f) + fun usableHeight(): Float = max(height - contentPadding * 2f, 1f) + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val currentFrame = frame + if (currentFrame == null) { + canvas.drawColor(Color.TRANSPARENT) + return + } + canvas.drawColor(currentFrame.background) + canvas.save() + canvas.clipRect( + contentPadding, + contentPadding, + width - contentPadding, + height - contentPadding, + ) + + for (row in 0 until currentFrame.rows) { + val top = contentPadding + row * cellHeightPx + val bottom = top + cellHeightPx + for (column in 0 until currentFrame.cols) { + val index = row * currentFrame.cols + column + val left = contentPadding + column * cellWidthPx + val right = left + cellWidthPx + val background = currentFrame.cellBackgrounds[index] + val flags = currentFrame.cellFlags[index] + paint.style = Paint.Style.FILL + paint.color = if (flags and FLAG_SELECTED != 0) { + blend(currentFrame.cursorColor, background, 0.32f) + } else { + background + } + if (paint.color != currentFrame.background || flags and FLAG_SELECTED != 0) { + canvas.drawRect(left, top, right + 0.5f, bottom + 0.5f, paint) + } + + val text = currentFrame.cellText[index] + if (text.isNotEmpty() && flags and FLAG_INVISIBLE == 0) { + configureTextPaint(flags, currentFrame.cellForegrounds[index]) + canvas.drawText(text, left, top + baselineOffsetPx, paint) + if (flags and FLAG_OVERLINE != 0) { + canvas.drawRect(left, top + 1f, right, top + max(2f, density), paint) + } + } + } + } + + if (currentFrame.cursorVisible && cursorOn && + currentFrame.cursorX in 0 until currentFrame.cols && + currentFrame.cursorY in 0 until currentFrame.rows + ) { + drawCursor(canvas, currentFrame) + } + canvas.restore() + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + parent?.requestDisallowInterceptTouchEvent(true) + } else if (event.actionMasked == MotionEvent.ACTION_UP || + event.actionMasked == MotionEvent.ACTION_CANCEL + ) { + parent?.requestDisallowInterceptTouchEvent(false) + } + return gestureDetector.onTouchEvent(event) || super.onTouchEvent(event) + } + + override fun onDetachedFromWindow() { + removeCallbacks(cursorBlink) + super.onDetachedFromWindow() + } + + private fun updateCellMetrics() { + paint.textSize = fontSizeSp * scaledDensity + paint.typeface = regularTypeface + cellWidthPx = ceil(paint.measureText("M").toDouble()).toFloat().coerceAtLeast(1f) + val metrics = paint.fontMetrics + val glyphHeight = metrics.descent - metrics.ascent + cellHeightPx = ceil((glyphHeight * 1.12f).toDouble()).toFloat().coerceAtLeast(1f) + baselineOffsetPx = (cellHeightPx - glyphHeight) / 2f - metrics.ascent + onCellMetricsChanged?.invoke() + invalidate() + } + + private fun configureTextPaint(flags: Int, color: Int) { + val bold = flags and FLAG_BOLD != 0 + val italic = flags and FLAG_ITALIC != 0 + paint.typeface = when { + bold && italic -> boldItalicTypeface + bold -> boldTypeface + italic -> italicTypeface + else -> regularTypeface + } + paint.textSize = fontSizeSp * scaledDensity + paint.color = color + paint.style = Paint.Style.FILL + paint.isUnderlineText = flags and FLAG_UNDERLINE != 0 + paint.isStrikeThruText = flags and FLAG_STRIKETHROUGH != 0 + } + + private fun drawCursor(canvas: Canvas, currentFrame: TerminalFrame) { + val left = contentPadding + currentFrame.cursorX * cellWidthPx + val top = contentPadding + currentFrame.cursorY * cellHeightPx + val right = left + cellWidthPx + val bottom = top + cellHeightPx + paint.color = currentFrame.cursorColor + paint.isUnderlineText = false + paint.isStrikeThruText = false + when (currentFrame.cursorStyle) { + 0 -> canvas.drawRect(left, top, left + max(2f * density, 2f), bottom, paint) + 2 -> canvas.drawRect(left, bottom - max(2f * density, 2f), right, bottom, paint) + 3 -> { + paint.style = Paint.Style.STROKE + paint.strokeWidth = max(density, 1f) + canvas.drawRect(left, top, right, bottom, paint) + } + else -> { + paint.style = Paint.Style.FILL + canvas.drawRect(left, top, right, bottom, paint) + val index = currentFrame.cursorY * currentFrame.cols + currentFrame.cursorX + val text = currentFrame.cellText[index] + if (text.isNotEmpty()) { + configureTextPaint(currentFrame.cellFlags[index], currentFrame.background) + canvas.drawText(text, left, top + baselineOffsetPx, paint) + } + } + } + } + + private fun blend(foreground: Int, background: Int, amount: Float): Int { + val inverseAmount = 1f - amount + return Color.rgb( + (Color.red(foreground) * amount + Color.red(background) * inverseAmount).toInt(), + (Color.green(foreground) * amount + Color.green(background) * inverseAmount).toInt(), + (Color.blue(foreground) * amount + Color.blue(background) * inverseAmount).toInt(), + ) + } + + private inner class TerminalGestureListener : GestureDetector.SimpleOnGestureListener() { + override fun onDown(event: MotionEvent): Boolean { + onRequestKeyboard?.invoke() + return true + } + + override fun onSingleTapUp(event: MotionEvent): Boolean { + performClick() + return true + } + + override fun onScroll( + first: MotionEvent?, + current: MotionEvent, + distanceX: Float, + distanceY: Float, + ): Boolean { + scrollRemainder += distanceY / cellHeightPx + val rows = scrollRemainder.toInt() + if (rows != 0) { + scrollRemainder -= rows + onScrollRows?.invoke(rows) + } + return true + } + } +} diff --git a/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/TerminalFrame.kt b/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/TerminalFrame.kt new file mode 100644 index 00000000000..5d333fa8308 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/TerminalFrame.kt @@ -0,0 +1,81 @@ +package expo.modules.t3terminal + +import java.nio.ByteBuffer +import java.nio.ByteOrder + +internal data class TerminalFrame( + val cols: Int, + val rows: Int, + val cursorX: Int, + val cursorY: Int, + val cursorVisible: Boolean, + val cursorStyle: Int, + val cursorBlinking: Boolean, + val foreground: Int, + val background: Int, + val cursorColor: Int, + val cellForegrounds: IntArray, + val cellBackgrounds: IntArray, + val cellFlags: IntArray, + val cellText: Array, +) { + companion object { + private const val MAGIC = 0x54563354 + private const val VERSION = 1 + private const val HEADER_BYTES = 32 + private const val CELL_HEADER_BYTES = 12 + + fun decode(bytes: ByteArray): TerminalFrame? { + if (bytes.size < HEADER_BYTES) return null + val buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN) + if (buffer.int != MAGIC || buffer.short.toInt() != VERSION) return null + + val cols = buffer.short.toInt() and 0xFFFF + val rows = buffer.short.toInt() and 0xFFFF + val cursorX = buffer.short.toInt() and 0xFFFF + val cursorY = buffer.short.toInt() and 0xFFFF + val cursorVisible = buffer.get().toInt() != 0 + val cursorStyle = buffer.get().toInt() and 0xFF + val cursorBlinking = buffer.get().toInt() != 0 + buffer.get() + val foreground = buffer.int + val background = buffer.int + val cursorColor = buffer.int + val cellCount = cols * rows + val foregrounds = IntArray(cellCount) + val backgrounds = IntArray(cellCount) + val flags = IntArray(cellCount) + val text = Array(cellCount) { "" } + + for (index in 0 until cellCount) { + if (buffer.remaining() < CELL_HEADER_BYTES) return null + foregrounds[index] = buffer.int + backgrounds[index] = buffer.int + flags[index] = buffer.short.toInt() and 0xFFFF + val textLength = buffer.short.toInt() and 0xFFFF + if (buffer.remaining() < textLength) return null + if (textLength > 0) { + text[index] = String(bytes, buffer.position(), textLength, Charsets.UTF_8) + buffer.position(buffer.position() + textLength) + } + } + + return TerminalFrame( + cols = cols, + rows = rows, + cursorX = cursorX, + cursorY = cursorY, + cursorVisible = cursorVisible, + cursorStyle = cursorStyle, + cursorBlinking = cursorBlinking, + foreground = foreground, + background = background, + cursorColor = cursorColor, + cellForegrounds = foregrounds, + cellBackgrounds = backgrounds, + cellFlags = flags, + cellText = text, + ) + } + } +} diff --git a/apps/mobile/modules/t3-terminal/android/src/main/jniLibs/arm64-v8a/libghostty-vt.so b/apps/mobile/modules/t3-terminal/android/src/main/jniLibs/arm64-v8a/libghostty-vt.so new file mode 100755 index 00000000000..e086ca83155 Binary files /dev/null and b/apps/mobile/modules/t3-terminal/android/src/main/jniLibs/arm64-v8a/libghostty-vt.so differ diff --git a/apps/mobile/modules/t3-terminal/android/src/main/jniLibs/armeabi-v7a/libghostty-vt.so b/apps/mobile/modules/t3-terminal/android/src/main/jniLibs/armeabi-v7a/libghostty-vt.so new file mode 100755 index 00000000000..5592142ffce Binary files /dev/null and b/apps/mobile/modules/t3-terminal/android/src/main/jniLibs/armeabi-v7a/libghostty-vt.so differ diff --git a/apps/mobile/modules/t3-terminal/android/src/main/jniLibs/x86/libghostty-vt.so b/apps/mobile/modules/t3-terminal/android/src/main/jniLibs/x86/libghostty-vt.so new file mode 100755 index 00000000000..bacab790833 Binary files /dev/null and b/apps/mobile/modules/t3-terminal/android/src/main/jniLibs/x86/libghostty-vt.so differ diff --git a/apps/mobile/modules/t3-terminal/android/src/main/jniLibs/x86_64/libghostty-vt.so b/apps/mobile/modules/t3-terminal/android/src/main/jniLibs/x86_64/libghostty-vt.so new file mode 100755 index 00000000000..f6e0b646a2f Binary files /dev/null and b/apps/mobile/modules/t3-terminal/android/src/main/jniLibs/x86_64/libghostty-vt.so differ diff --git a/apps/mobile/modules/t3-terminal/scripts/build-libghostty-android.sh b/apps/mobile/modules/t3-terminal/scripts/build-libghostty-android.sh new file mode 100755 index 00000000000..7b9aa9b6dc6 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/scripts/build-libghostty-android.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +VENDOR_DIR="${MODULE_DIR}/Vendor/libghostty-vt" +PATCH_DIR="${SCRIPT_DIR}/libghostty-android-patches" + +GHOSTTY_REVISION="${GHOSTTY_REVISION:-9f62873bf195e4d8a762d768a1405a5f2f7b1697}" +GHOSTTY_SOURCE_DIR="${GHOSTTY_SOURCE_DIR:-${HOME}/.cache/t3code/ghostty-${GHOSTTY_REVISION:0:8}}" +GHOSTTY_ZIG_VERSION="${GHOSTTY_ZIG_VERSION:-0.15.2}" +GHOSTTY_ZIG="${GHOSTTY_ZIG:-}" +ANDROID_NDK_HOME="${ANDROID_NDK_HOME:-}" + +log() { + printf '[libghostty-vt-android] %s\n' "$*" +} + +die() { + printf '[libghostty-vt-android] error: %s\n' "$*" >&2 + exit 1 +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "missing required command: $1" +} + +ensure_zig() { + if [[ -n "${GHOSTTY_ZIG}" ]]; then + [[ -x "${GHOSTTY_ZIG}" ]] || die "GHOSTTY_ZIG is not executable: ${GHOSTTY_ZIG}" + return + fi + if command -v zig >/dev/null 2>&1 && [[ "$(zig version)" == "${GHOSTTY_ZIG_VERSION}" ]]; then + GHOSTTY_ZIG="$(command -v zig)" + return + fi + + local host_os host_arch cache_dir + host_os="$(uname -s | tr '[:upper:]' '[:lower:]')" + host_arch="$(uname -m)" + case "${host_os}" in + darwin) host_os="macos" ;; + linux) ;; + *) die "unsupported host OS for Zig download: ${host_os}" ;; + esac + case "${host_arch}" in + arm64) host_arch="aarch64" ;; + aarch64 | x86_64) ;; + *) die "unsupported host architecture for Zig download: ${host_arch}" ;; + esac + + cache_dir="${HOME}/.cache/t3code/zig-${GHOSTTY_ZIG_VERSION}" + GHOSTTY_ZIG="${cache_dir}/zig" + if [[ -x "${GHOSTTY_ZIG}" ]]; then + return + fi + + require_cmd curl + require_cmd tar + mkdir -p "${cache_dir}" + log "downloading Zig ${GHOSTTY_ZIG_VERSION}" + curl -fsSL \ + "https://ziglang.org/download/${GHOSTTY_ZIG_VERSION}/zig-${host_arch}-${host_os}-${GHOSTTY_ZIG_VERSION}.tar.xz" \ + | tar -xJ --strip-components=1 -C "${cache_dir}" +} + +ensure_ghostty_source() { + if [[ ! -d "${GHOSTTY_SOURCE_DIR}/.git" ]]; then + require_cmd git + log "cloning Ghostty ${GHOSTTY_REVISION}" + git clone --filter=blob:none --no-checkout https://github.com/ghostty-org/ghostty.git \ + "${GHOSTTY_SOURCE_DIR}" + git -C "${GHOSTTY_SOURCE_DIR}" fetch --depth=1 origin "${GHOSTTY_REVISION}" + git -C "${GHOSTTY_SOURCE_DIR}" checkout --detach "${GHOSTTY_REVISION}" + fi + + local actual_revision + actual_revision="$(git -C "${GHOSTTY_SOURCE_DIR}" rev-parse HEAD)" + [[ "${actual_revision}" == "${GHOSTTY_REVISION}" ]] || \ + die "expected Ghostty ${GHOSTTY_REVISION}, found ${actual_revision}" +} + +apply_ghostty_patches() { + [[ -d "${PATCH_DIR}" ]] || return + + local patch_file patch_name + for patch_file in "${PATCH_DIR}"/*.patch; do + [[ -e "${patch_file}" ]] || continue + patch_name="$(basename "${patch_file}")" + if git -C "${GHOSTTY_SOURCE_DIR}" apply --reverse --check "${patch_file}" >/dev/null 2>&1; then + log "patch already applied: ${patch_name}" + continue + fi + log "applying patch: ${patch_name}" + git -C "${GHOSTTY_SOURCE_DIR}" apply --check "${patch_file}" + git -C "${GHOSTTY_SOURCE_DIR}" apply "${patch_file}" + done +} + +if [[ -z "${ANDROID_NDK_HOME}" ]]; then + die "ANDROID_NDK_HOME must point to an installed Android NDK" +fi +[[ -d "${ANDROID_NDK_HOME}" ]] || die "Android NDK not found: ${ANDROID_NDK_HOME}" + +ensure_zig +ensure_ghostty_source +apply_ghostty_patches + +strip_tool="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt" +strip_tool="$(find "${strip_tool}" -path '*/bin/llvm-strip' -print -quit)" +[[ -x "${strip_tool}" ]] || die "llvm-strip not found under ${ANDROID_NDK_HOME}" + +targets=( + "arm64-v8a:aarch64-linux-android" + "armeabi-v7a:arm-linux-androideabi" + "x86:x86-linux-android" + "x86_64:x86_64-linux-android" +) + +build_root="$(mktemp -d)" +trap 'rm -rf "${build_root}"' EXIT +mkdir -p "${VENDOR_DIR}/include" + +for entry in "${targets[@]}"; do + abi="${entry%%:*}" + target="${entry#*:}" + prefix="${build_root}/${abi}" + log "building ${abi} (${target})" + ( + cd "${GHOSTTY_SOURCE_DIR}" + ANDROID_NDK_HOME="${ANDROID_NDK_HOME}" "${GHOSTTY_ZIG}" build \ + -Demit-lib-vt \ + -Dtarget="${target}" \ + -Doptimize=ReleaseFast \ + -Dstrip=true \ + -Dsimd=false \ + -p "${prefix}" + ) + + mkdir -p "${MODULE_DIR}/android/src/main/jniLibs/${abi}" + cp "${prefix}/lib/libghostty-vt.so.0.1.0" \ + "${MODULE_DIR}/android/src/main/jniLibs/${abi}/libghostty-vt.so" + "${strip_tool}" --strip-unneeded \ + "${MODULE_DIR}/android/src/main/jniLibs/${abi}/libghostty-vt.so" +done + +rm -rf "${VENDOR_DIR}/include/ghostty" +cp -R "${build_root}/arm64-v8a/include/ghostty" "${VENDOR_DIR}/include/ghostty" +cp "${GHOSTTY_SOURCE_DIR}/LICENSE" "${VENDOR_DIR}/LICENSE" +printf '%s\n' "${GHOSTTY_REVISION}" > "${VENDOR_DIR}/VERSION" +log "done" diff --git a/apps/mobile/modules/t3-terminal/scripts/libghostty-android-patches/0001-link-android-libvt-with-libc.patch b/apps/mobile/modules/t3-terminal/scripts/libghostty-android-patches/0001-link-android-libvt-with-libc.patch new file mode 100644 index 00000000000..2713eb739a0 --- /dev/null +++ b/apps/mobile/modules/t3-terminal/scripts/libghostty-android-patches/0001-link-android-libvt-with-libc.patch @@ -0,0 +1,25 @@ +diff --git a/src/build/GhosttyZig.zig b/src/build/GhosttyZig.zig +index 44c300e15..f8d4296a2 100644 +--- a/src/build/GhosttyZig.zig ++++ b/src/build/GhosttyZig.zig +@@ -123,7 +123,7 @@ fn initVt( + // no-libcxx mode (HWY_NO_LIBCXX / SIMDUTF_NO_LIBCXX) so we + // don't need libcpp. System-provided simdutf headers still + // use C++ stdlib headers, so we need libcpp in that case. +- .link_libc = if (cfg.simd) true else null, ++ .link_libc = if (cfg.simd or cfg.target.result.abi.isAndroid()) true else null, + .link_libcpp = if (cfg.simd and + b.systemIntegrationOption("simdutf", .{}) and + cfg.target.result.abi != .msvc) true else null, +diff --git a/src/terminal/c/sys.zig b/src/terminal/c/sys.zig +index c4b2b17f2..afb98ed4b 100644 +--- a/src/terminal/c/sys.zig ++++ b/src/terminal/c/sys.zig +@@ -227,6 +227,7 @@ pub fn logStderr( + message_len: usize, + ) callconv(lib.calling_conv) void { + if (comptime builtin.target.cpu.arch.isWasm()) return; ++ if (comptime builtin.target.abi.isAndroid()) return; + + const scope = scope_ptr[0..scope_len]; + const message = message_ptr[0..message_len]; diff --git a/apps/mobile/package.json b/apps/mobile/package.json index c9e8051ede3..d0bef94e527 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -22,6 +22,7 @@ "ios:dev": "APP_VARIANT=development EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform ios && expo run:ios", "ios:preview": "APP_VARIANT=preview EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform ios && expo run:ios", "ios:prod": "APP_VARIANT=production EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform ios && expo run:ios", + "ios:release": "APP_VARIANT=production EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform ios && expo run:ios --configuration Release --no-bundler", "eas:ios:dev": "eas build --profile development -p ios", "eas:ios:preview": "eas build --profile preview -p ios", "eas:ios:preview:dev": "eas build --profile preview:dev -p ios", @@ -60,6 +61,7 @@ "@t3tools/mobile-review-diff-native": "file:./modules/t3-review-diff", "@t3tools/mobile-terminal-native": "file:./modules/t3-terminal", "@t3tools/shared": "workspace:*", + "@tabler/icons-react-native": "^3.44.0", "clsx": "^2.1.1", "diff": "8.0.3", "effect": "catalog:", diff --git a/apps/mobile/plugins/withoutIosPersonalTeamCapabilities.cjs b/apps/mobile/plugins/withoutIosPersonalTeamCapabilities.cjs new file mode 100644 index 00000000000..035c46b34c8 --- /dev/null +++ b/apps/mobile/plugins/withoutIosPersonalTeamCapabilities.cjs @@ -0,0 +1,10 @@ +const { withEntitlementsPlist } = require("expo/config-plugins"); + +module.exports = function withoutIosPersonalTeamCapabilities(config) { + return withEntitlementsPlist(config, (modConfig) => { + delete modConfig.modResults["aps-environment"]; + delete modConfig.modResults["com.apple.developer.applesignin"]; + delete modConfig.modResults["com.apple.security.application-groups"]; + return modConfig; + }); +}; diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index c0e95c3d3b5..ca76cda982f 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -5,6 +5,7 @@ import { DMSans_700Bold, useFonts, } from "@expo-google-fonts/dm-sans"; +import { RegistryContext } from "@effect/atom-react"; import { usePathname } from "expo-router"; import Stack from "expo-router/stack"; import { useCallback } from "react"; @@ -18,7 +19,6 @@ import { LoadingScreen } from "../components/LoadingScreen"; import { useWorkspaceState } from "../state/workspace"; import { useThreadOutboxDrain } from "../state/use-thread-outbox-drain"; -import { RegistryContext } from "@effect/atom-react"; import { appAtomRegistry } from "../state/atom-registry"; import { CloudAuthProvider } from "../features/cloud/CloudAuthProvider"; import { diff --git a/apps/mobile/src/app/connections/index.tsx b/apps/mobile/src/app/connections/index.tsx index 12a06996447..f35e39a1c22 100644 --- a/apps/mobile/src/app/connections/index.tsx +++ b/apps/mobile/src/app/connections/index.tsx @@ -2,10 +2,11 @@ import { Stack, useRouter } from "expo-router"; import { SymbolView } from "expo-symbols"; import type { EnvironmentId } from "@t3tools/contracts"; import { useCallback, useState } from "react"; -import { ScrollView, View } from "react-native"; +import { Platform, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useThemeColor } from "../../lib/useThemeColor"; +import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; import { AppText as Text } from "../../components/AppText"; import { cn } from "../../lib/cn"; import { useRemoteConnections } from "../../state/use-remote-environment-registry"; @@ -33,16 +34,31 @@ export default function ConnectionsRouteScreen() { - - router.push("/connections/new")} - separateBackground + {Platform.OS === "android" ? ( + router.back()} + actions={[ + { + accessibilityLabel: "Add environment", + icon: "plus", + onPress: () => router.push("/connections/new"), + }, + ]} /> - + ) : ( + + router.push("/connections/new")} + separateBackground + /> + + )} - - { - if (showScanner) { - closeScanner(); - } else { - void openScanner(); - } - }} - separateBackground + {Platform.OS === "android" ? ( + router.back()} + actions={[ + { + accessibilityLabel: showScanner ? "Close scanner" : "Scan QR code", + icon: showScanner ? "xmark" : "camera", + onPress: () => { + if (showScanner) { + closeScanner(); + } else { + void openScanner(); + } + }, + }, + ]} /> - + ) : ( + + { + if (showScanner) { + closeScanner(); + } else { + void openScanner(); + } + }} + separateBackground + /> + + )} - - - {layout.usesSplitView ? ( + + {Platform.OS === "android" ? ( + router.back() : undefined} + actions={[ + { + accessibilityLabel: "Add project", + icon: "plus", + onPress: () => router.push("/new/add-project"), + }, + ]} + /> + ) : ( + + {layout.usesSplitView ? ( + router.back()} + separateBackground + /> + ) : null} router.back()} + icon="plus" + onPress={() => router.push("/new/add-project")} separateBackground /> - ) : null} - router.push("/new/add-project")} - separateBackground - /> - + + )} - - router.push("/settings/environment-new")} - separateBackground + {Platform.OS === "android" ? ( + router.back()} + actions={[ + { + accessibilityLabel: "Add environment", + icon: "plus", + onPress: () => router.push("/settings/environment-new"), + }, + ]} /> - + ) : ( + + router.push("/settings/environment-new")} + separateBackground + /> + + )} - {layout.usesSplitView ? ( + {Platform.OS !== "android" && layout.usesSplitView ? ( - + diff --git a/apps/mobile/src/components/AndroidScreenHeader.tsx b/apps/mobile/src/components/AndroidScreenHeader.tsx new file mode 100644 index 00000000000..2b46a889e06 --- /dev/null +++ b/apps/mobile/src/components/AndroidScreenHeader.tsx @@ -0,0 +1,146 @@ +import type { ReactNode } from "react"; +import { Pressable, Text as RNText, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +import { SymbolView, type AppSymbolName } from "./AppSymbol"; +import { MOBILE_TYPOGRAPHY } from "../lib/typography"; +import { useThemeColor } from "../lib/useThemeColor"; + +export interface AndroidHeaderAction { + readonly accessibilityLabel: string; + readonly icon: AppSymbolName; + readonly onPress: () => void; + readonly disabled?: boolean; +} + +export function AndroidHeaderIconButton(props: { + readonly accessibilityLabel: string; + readonly icon: AppSymbolName; + readonly onPress?: () => void; + readonly disabled?: boolean; +}) { + const foregroundColor = useThemeColor("--color-foreground"); + const subtleColor = useThemeColor("--color-subtle"); + const disabledColor = useThemeColor("--color-icon-subtle"); + + return ( + + + + ); +} + +export function AndroidScreenHeader(props: { + readonly title: string; + readonly subtitle?: string | null; + readonly actions?: ReadonlyArray; + readonly trailing?: ReactNode; + readonly onBack?: () => void; +}) { + const insets = useSafeAreaInsets(); + const backgroundColor = useThemeColor("--color-header"); + const borderColor = useThemeColor("--color-header-border"); + const foregroundColor = useThemeColor("--color-foreground"); + const mutedColor = useThemeColor("--color-foreground-muted"); + + return ( + + + {props.onBack ? ( + + + + ) : null} + + + + {props.title} + + {props.subtitle ? ( + + {props.subtitle} + + ) : null} + + + {props.actions?.map((action) => ( + + ))} + {props.trailing} + + + ); +} diff --git a/apps/mobile/src/components/AppSymbol.tsx b/apps/mobile/src/components/AppSymbol.tsx new file mode 100644 index 00000000000..8f4ee899b42 --- /dev/null +++ b/apps/mobile/src/components/AppSymbol.tsx @@ -0,0 +1,142 @@ +import { + IconAdjustmentsHorizontal, + IconAlertTriangle, + IconArchive, + IconArrowBackUp, + IconArrowDownCircle, + IconArrowRightCircle, + IconArrowUp, + IconArrowUpRight, + IconBellRinging, + IconBolt, + IconCamera, + IconCheck, + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronUp, + IconCircleCheck, + IconCircleXFilled, + IconCopy, + IconDeviceDesktop, + IconDots, + IconDotsCircleHorizontal, + IconEdit, + IconExternalLink, + IconEye, + IconFileText, + IconFilter, + IconFolder, + IconFolderPlus, + IconGitBranch, + IconGitMerge, + IconInfoCircle, + IconLayoutColumns, + IconLink, + IconMessage, + IconNetwork, + IconPlayerPlay, + IconPlayerStopFilled, + IconPlus, + IconQrcode, + IconRefresh, + IconSearch, + IconServer, + IconSettings, + IconLayoutSidebarRight, + IconTerminal2, + IconTrash, + IconTypography, + IconUserCircle, + IconWifiOff, + IconX, + type Icon, +} from "@tabler/icons-react-native"; +import { Platform } from "react-native"; +import { SymbolView as ExpoSymbolView, type SFSymbol, type SymbolViewProps } from "expo-symbols"; + +const ANDROID_ICON_BY_SF_SYMBOL: Partial> = { + "arrow.branch": IconGitBranch, + "arrow.clockwise": IconRefresh, + "arrow.down.circle": IconArrowDownCircle, + "arrow.right.circle": IconArrowRightCircle, + "arrow.triangle.branch": IconGitBranch, + "arrow.turn.left.up": IconArrowBackUp, + "arrow.up": IconArrowUp, + "arrow.up.right": IconArrowUpRight, + archivebox: IconArchive, + "archivebox.fill": IconArchive, + "bell.badge": IconBellRinging, + "bolt.circle": IconBolt, + "bolt.horizontal.circle": IconBolt, + camera: IconCamera, + checkmark: IconCheck, + "checkmark.circle": IconCircleCheck, + "chevron.down": IconChevronDown, + "chevron.left": IconChevronLeft, + "chevron.right": IconChevronRight, + "chevron.up": IconChevronUp, + desktopcomputer: IconDeviceDesktop, + "doc.on.doc": IconCopy, + "doc.text": IconFileText, + ellipsis: IconDots, + "ellipsis.circle": IconDotsCircleHorizontal, + "exclamationmark.triangle": IconAlertTriangle, + eye: IconEye, + folder: IconFolder, + "folder.badge.plus": IconFolderPlus, + "folder.fill": IconFolder, + gearshape: IconSettings, + "info.circle": IconInfoCircle, + link: IconLink, + "line.3.horizontal.decrease.circle": IconFilter, + "line.3.horizontal.decrease.circle.fill": IconFilter, + magnifyingglass: IconSearch, + "person.crop.circle": IconUserCircle, + play: IconPlayerPlay, + plus: IconPlus, + "qrcode.viewfinder": IconQrcode, + "point.3.connected.trianglepath.dotted": IconNetwork, + "point.topleft.down.curvedto.point.bottomright.up": IconGitMerge, + safari: IconExternalLink, + "server.rack": IconServer, + "sidebar.right": IconLayoutSidebarRight, + "slider.horizontal.3": IconAdjustmentsHorizontal, + "square.and.pencil": IconEdit, + "square.split.2x1": IconLayoutColumns, + "stop.fill": IconPlayerStopFilled, + terminal: IconTerminal2, + "text.bubble": IconMessage, + "textformat.size": IconTypography, + trash: IconTrash, + "wifi.slash": IconWifiOff, + xmark: IconX, + "xmark.circle.fill": IconCircleXFilled, +}; + +export type { SFSymbol } from "expo-symbols"; +export type AppSymbolName = SymbolViewProps["name"]; + +export function SymbolView(props: SymbolViewProps) { + if (Platform.OS !== "android") { + return ; + } + + const sfSymbol = typeof props.name === "string" ? props.name : props.name.ios; + const AndroidIcon = sfSymbol ? ANDROID_ICON_BY_SF_SYMBOL[sfSymbol] : undefined; + + if (!AndroidIcon) { + return props.fallback ?? null; + } + + return ( + + ); +} diff --git a/apps/mobile/src/components/ComposerToolbarTrigger.tsx b/apps/mobile/src/components/ComposerToolbarTrigger.tsx index e054a13f697..98e9f6130bb 100644 --- a/apps/mobile/src/components/ComposerToolbarTrigger.tsx +++ b/apps/mobile/src/components/ComposerToolbarTrigger.tsx @@ -1,4 +1,3 @@ -import { SymbolView } from "expo-symbols"; import type { ComponentProps, ReactNode } from "react"; import { useCallback, useMemo, useState } from "react"; import { @@ -16,6 +15,7 @@ import { import { useThemeColor } from "../lib/useThemeColor"; import { cn } from "../lib/cn"; import { AppText as Text } from "./AppText"; +import { SymbolView } from "./AppSymbol"; export const COMPOSER_TOOLBAR_CONTROL_HEIGHT = 44; export const COMPOSER_TOOLBAR_GAP = 8; diff --git a/apps/mobile/src/components/ControlPill.tsx b/apps/mobile/src/components/ControlPill.tsx index 62c1b061785..cce05663ba7 100644 --- a/apps/mobile/src/components/ControlPill.tsx +++ b/apps/mobile/src/components/ControlPill.tsx @@ -1,10 +1,10 @@ import { MenuView } from "@react-native-menu/menu"; import type { ComponentProps, ReactNode } from "react"; import { Pressable, useColorScheme, View } from "react-native"; -import { SymbolView } from "expo-symbols"; import { useThemeColor } from "../lib/useThemeColor"; import { cn } from "../lib/cn"; +import { SymbolView } from "./AppSymbol"; import { AppText as Text } from "./AppText"; export function ControlPill(props: { diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index 3e1934100cd..06cca7806d2 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -10,6 +10,7 @@ import { SymbolView } from "expo-symbols"; import { useCallback, useRef } from "react"; import { ActivityIndicator, + Platform, Pressable, RefreshControl, ScrollView, @@ -61,6 +62,10 @@ function ArchivedThreadsHeader(props: { readonly onSortOrderChange: (sortOrder: ArchivedThreadSortOrder) => void; }) { const hasCustomFilter = props.selectedEnvironmentId !== null || props.sortOrder !== "newest"; + const searchBackgroundColor = useThemeColor("--color-input"); + const searchIconColor = useThemeColor("--color-icon"); + const searchPlaceholderColor = useThemeColor("--color-placeholder"); + const searchTextColor = useThemeColor("--color-foreground"); return ( <> @@ -73,6 +78,14 @@ function ArchivedThreadsHeader(props: { obscureBackground: false, placeholder: "Search archived threads", placement: "stacked", + ...(Platform.OS === "android" + ? { + barTintColor: searchBackgroundColor, + headerIconColor: searchIconColor, + hintTextColor: searchPlaceholderColor, + textColor: searchTextColor, + } + : {}), onChangeText: (event) => { props.onSearchQueryChange(event.nativeEvent.text); }, diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index c6760cad044..97b09035930 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -5,6 +5,7 @@ import { useHeaderHeight } from "expo-router/build/react-navigation/elements"; import { useCallback, useMemo, useRef, useState } from "react"; import { ActivityIndicator, + Platform, Pressable, ScrollView, Text as RNText, @@ -19,7 +20,8 @@ import { ThreadId, } from "@t3tools/contracts"; -import { AppText as Text } from "../../components/AppText"; +import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; +import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; import { CopyTextButton } from "../../components/CopyTextButton"; import { EmptyState } from "../../components/EmptyState"; import { LoadingScreen } from "../../components/LoadingScreen"; @@ -467,7 +469,9 @@ export function ThreadFilesTreeScreen() { const { fileInspector, layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); const [searchQuery, setSearchQuery] = useState(""); const colorScheme = useColorScheme(); + const isAndroid = Platform.OS === "android"; const highlightTheme = colorScheme === "dark" ? "dark" : "light"; + const iconColor = String(useThemeColor("--color-icon-muted")); const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace(); const entriesQuery = useEnvironmentQuery( environmentId !== null && cwd !== null && !fileInspector.supported @@ -560,7 +564,7 @@ export function ThreadFilesTreeScreen() { - - {layout.usesSplitView ? ( - + - ) : null} - - - - - + + + + + + ) : ( + <> + + {layout.usesSplitView ? ( + + ) : null} + + + + + + + )} ; + readonly searchQuery: string; readonly selectedEnvironmentId: EnvironmentId | null; readonly projectSortOrder: HomeProjectSortOrder; readonly threadSortOrder: SidebarThreadSortOrder; @@ -38,6 +46,269 @@ export function HomeHeader(props: { readonly onOpenSettings: () => void; readonly onStartNewTask: () => void; }) { + if (Platform.OS === "android") { + return ; + } + + return ; +} + +type HomeHeaderProps = Parameters[0]; + +function checkedMenuTitle(checked: boolean, title: string) { + return checked ? `✓ ${title}` : title; +} + +function AndroidHomeHeader(props: HomeHeaderProps) { + const insets = useSafeAreaInsets(); + const iconColor = useThemeColor("--color-icon"); + const mutedColor = useThemeColor("--color-foreground-muted"); + const subtleColor = useThemeColor("--color-subtle"); + const headerColor = useThemeColor("--color-header"); + const headerBorderColor = useThemeColor("--color-header-border"); + const inputColor = useThemeColor("--color-input"); + const inputBorderColor = useThemeColor("--color-input-border"); + const placeholderColor = useThemeColor("--color-placeholder"); + const hasCustomListOptions = + props.selectedEnvironmentId !== null || + props.projectSortOrder !== DEFAULT_SIDEBAR_PROJECT_SORT_ORDER || + props.threadSortOrder !== DEFAULT_SIDEBAR_THREAD_SORT_ORDER || + props.projectGroupingMode !== DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE; + const menuActions = useMemo( + () => [ + { + id: "environment", + title: "Environment", + subactions: [ + { + id: "environment:all", + title: checkedMenuTitle(props.selectedEnvironmentId === null, "All environments"), + }, + ...props.environments.map((environment) => ({ + id: `environment:${environment.environmentId}`, + title: checkedMenuTitle( + props.selectedEnvironmentId === environment.environmentId, + environment.label, + ), + })), + ], + }, + { + id: "project-sort", + title: "Sort projects", + subactions: PROJECT_SORT_OPTIONS.map((option) => ({ + id: `project-sort:${option.value}`, + title: checkedMenuTitle(props.projectSortOrder === option.value, option.label), + })), + }, + { + id: "thread-sort", + title: "Sort threads", + subactions: THREAD_SORT_OPTIONS.map((option) => ({ + id: `thread-sort:${option.value}`, + title: checkedMenuTitle(props.threadSortOrder === option.value, option.label), + })), + }, + { + id: "project-grouping", + title: "Group projects", + subactions: PROJECT_GROUPING_OPTIONS.map((option) => ({ + id: `project-grouping:${option.value}`, + title: checkedMenuTitle(props.projectGroupingMode === option.value, option.label), + })), + }, + ], + [ + props.environments, + props.projectGroupingMode, + props.projectSortOrder, + props.selectedEnvironmentId, + props.threadSortOrder, + ], + ); + const handleMenuAction = useCallback( + (event: { nativeEvent: { event: string } }) => { + const id = event.nativeEvent.event; + if (id === "environment:all") { + props.onEnvironmentChange(null); + return; + } + + if (id.startsWith("environment:")) { + const environmentId = id.slice("environment:".length); + const environment = props.environments.find( + (candidate) => candidate.environmentId === environmentId, + ); + if (environment) { + props.onEnvironmentChange(environment.environmentId); + } + return; + } + + const projectSort = PROJECT_SORT_OPTIONS.find( + (option) => id === `project-sort:${option.value}`, + ); + if (projectSort) { + props.onProjectSortOrderChange(projectSort.value); + return; + } + + const threadSort = THREAD_SORT_OPTIONS.find((option) => id === `thread-sort:${option.value}`); + if (threadSort) { + props.onThreadSortOrderChange(threadSort.value); + return; + } + + const grouping = PROJECT_GROUPING_OPTIONS.find( + (option) => id === `project-grouping:${option.value}`, + ); + if (grouping) { + props.onProjectGroupingModeChange(grouping.value); + } + }, + [props], + ); + + return ( + <> + + + + + + + T3 Code + + + + Alpha + + + + + + + + + + + + + + + + + {props.searchQuery.length > 0 ? ( + props.onSearchQueryChange("")} + > + + + ) : null} + + + + + ); +} + +function IosHomeHeader(props: HomeHeaderProps) { const searchBarRef = useRef(null); const iconColor = useThemeColor("--color-icon"); const mutedColor = useThemeColor("--color-foreground-muted"); diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index d38ae93158a..e36f33ccbb3 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -7,10 +7,19 @@ import type { SidebarProjectGroupingMode, SidebarThreadSortOrder, } from "@t3tools/contracts"; -import { SymbolView } from "expo-symbols"; +import * as Haptics from "expo-haptics"; import { useCallback, useMemo, useRef, useState } from "react"; -import { ActivityIndicator, Pressable, ScrollView, useWindowDimensions, View } from "react-native"; -import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; +import { + ActivityIndicator, + Platform, + Pressable, + ScrollView, + useWindowDimensions, + View, +} from "react-native"; +import ReanimatedSwipeable, { + type SwipeableMethods, +} from "react-native-gesture-handler/ReanimatedSwipeable"; import Animated, { Easing, LinearTransition, @@ -22,6 +31,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useThemeColor } from "../../lib/useThemeColor"; import { AppText as Text } from "../../components/AppText"; +import { SymbolView } from "../../components/AppSymbol"; import { EmptyState } from "../../components/EmptyState"; import { ProjectFavicon } from "../../components/ProjectFavicon"; import type { WorkspaceState } from "../../state/workspaceModel"; @@ -29,7 +39,11 @@ import type { SavedRemoteConnection } from "../../lib/connection"; import { relativeTime } from "../../lib/time"; import { threadStatusTone } from "../threads/threadPresentation"; import { buildHomeThreadGroups, type HomeProjectSortOrder } from "./homeThreadList"; -import { ThreadSwipeable } from "./thread-swipe-actions"; +import { + THREAD_SWIPE_ACTIONS_WIDTH, + THREAD_SWIPE_SPRING, + ThreadSwipeActions, +} from "./thread-swipe-actions"; import { WorkspaceConnectionStatus } from "./WorkspaceConnectionStatus"; import { shouldShowWorkspaceConnectionStatus } from "./workspace-connection-status"; @@ -214,10 +228,13 @@ function ThreadRow(props: { readonly onSwipeableClose: (methods: SwipeableMethods) => void; readonly isLast: boolean; }) { + const swipeableRef = useRef(null); + const fullSwipeArmedRef = useRef(false); const { width: windowWidth } = useWindowDimensions(); const separatorColor = useThemeColor("--color-separator"); const iconSubtleColor = useThemeColor("--color-icon-subtle"); const cardColor = useThemeColor("--color-card"); + const fullSwipeThreshold = Math.max(THREAD_SWIPE_ACTIONS_WIDTH + 44, (windowWidth - 32) * 0.58); const { bg, fg } = statusColors(props.thread); const tone = threadStatusTone(props.thread); const timestamp = relativeTime( @@ -227,107 +244,150 @@ function ThreadRow(props: { const subtitleParts = [props.environmentLabel, branch].filter((part): part is string => Boolean(part), ); + const handleFullSwipeArmedChange = useCallback((armed: boolean) => { + if (armed && !fullSwipeArmedRef.current && process.env.EXPO_OS === "ios") { + void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + } + fullSwipeArmedRef.current = armed; + }, []); return ( - { + fullSwipeArmedRef.current = false; + if (swipeableRef.current) { + props.onSwipeableClose(swipeableRef.current); + } + }} + onSwipeableOpenStartDrag={() => { + if (swipeableRef.current) { + props.onSwipeableWillOpen(swipeableRef.current); + } }} - threadTitle={props.thread.title} + onSwipeableWillOpen={() => { + const methods = swipeableRef.current; + if (!methods) { + return; + } + + props.onSwipeableWillOpen(methods); + if (fullSwipeArmedRef.current) { + fullSwipeArmedRef.current = false; + methods.close(); + props.onDelete(); + } + }} + overshootFriction={1} + overshootRight + renderRightActions={(_progress, translation, methods) => ( + + )} + rightThreshold={THREAD_SWIPE_ACTIONS_WIDTH * 0.42} > - {(close) => ( - { - close(); - props.onPress(); + { + swipeableRef.current?.close(); + props.onPress(); + }} + style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} + > + ({ opacity: pressed ? 0.7 : 1 })} > - - + + + + + + + {props.thread.title} + + + + + {tone.label} + + + + {timestamp} + + - - + {subtitleParts.length > 0 ? ( + + - {props.thread.title} + {subtitleParts.join(" · ")} - - - - {tone.label} - - - - {timestamp} - - - - {subtitleParts.length > 0 ? ( - - - - {subtitleParts.join(" · ")} - - - ) : null} - + ) : null} - - )} - + + + ); } @@ -401,16 +461,19 @@ export function HomeScreen(props: HomeScreenProps) { return ( openSwipeableRef.current?.close()} className="flex-1" contentContainerStyle={{ + alignSelf: "center", + width: "100%", + maxWidth: 720, paddingHorizontal: 16, - paddingTop: 8, - paddingBottom: 24, + paddingTop: Platform.OS === "android" ? 16 : 8, + paddingBottom: Math.max(insets.bottom, 24), gap: 20, }} > diff --git a/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx b/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx index 0bc729f6223..084582133d7 100644 --- a/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx +++ b/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx @@ -1,12 +1,13 @@ import { Stack } from "expo-router"; import type { ReactNode } from "react"; +import { Platform } from "react-native"; import { useAdaptiveWorkspaceLayout } from "./AdaptiveWorkspaceLayout"; export function WorkspaceSidebarToolbar(props: { readonly children?: ReactNode } = {}) { const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); - if (!layout.usesSplitView) { + if (Platform.OS === "android" || !layout.usesSplitView) { return null; } diff --git a/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx b/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx index d35c48e8a9b..0b29790faf6 100644 --- a/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx +++ b/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx @@ -1,14 +1,21 @@ import { useLocalSearchParams, useRouter } from "expo-router"; -import { SymbolView } from "expo-symbols"; import { TextInputWrapper } from "expo-paste-input"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { Pressable, ScrollView, View, useColorScheme, useWindowDimensions } from "react-native"; -import { KeyboardAvoidingView } from "react-native-keyboard-controller"; +import { + Platform, + Pressable, + ScrollView, + View, + useColorScheme, + useWindowDimensions, +} from "react-native"; +import { KeyboardAvoidingView, KeyboardStickyView } from "react-native-keyboard-controller"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import ImageViewing from "react-native-image-viewing"; import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; +import { SymbolView } from "../../components/AppSymbol"; import { ComposerAttachmentStrip } from "../../components/ComposerAttachmentStrip"; import { ControlPill } from "../../components/ControlPill"; import { cn } from "../../lib/cn"; @@ -41,6 +48,7 @@ import { const REVIEW_COMMENT_PREVIEW_MAX_LINES = 5; export function ReviewCommentComposerSheet() { + const isAndroid = Platform.OS === "android"; const router = useRouter(); const insets = useSafeAreaInsets(); const { width } = useWindowDimensions(); @@ -140,6 +148,21 @@ export function ReviewCommentComposerSheet() { } } + const handleSubmit = useCallback(() => { + if (!target || !environmentId || !threadId || commentText.trim().length === 0) { + return; + } + + appendReviewCommentToDraft({ + environmentId, + threadId, + text: formatReviewCommentContext(target, commentText), + attachments, + }); + setAttachments([]); + dismissComposer(); + }, [attachments, commentText, dismissComposer, environmentId, target, threadId]); + return ( @@ -147,8 +170,8 @@ export function ReviewCommentComposerSheet() { style={{ flex: 1, paddingHorizontal: 20, - paddingTop: 8, - paddingBottom: target ? 0 : Math.max(insets.bottom, 18), + paddingTop: isAndroid ? insets.top + 8 : 8, + paddingBottom: target ? (isAndroid ? 72 : 0) : Math.max(insets.bottom, 18), }} > @@ -274,7 +297,7 @@ export function ReviewCommentComposerSheet() { )} - {target ? ( + {!isAndroid && target ? ( { - if (!target || !environmentId || !threadId || commentText.trim().length === 0) { - return; - } - - appendReviewCommentToDraft({ - environmentId, - threadId, - text: formatReviewCommentContext(target, commentText), - attachments, - }); - setAttachments([]); - dismissComposer(); - }} + onPress={handleSubmit} /> ) : null} + {isAndroid && target ? ( + + + void handlePickImages()} + /> + + + + + ) : null} { showAuxiliaryPane("inspector"); @@ -531,9 +537,63 @@ export function ReviewSheet() { hasCachedSelectedDiff, hasAnyCachedDiff, }); + const androidSectionMenuActions = useMemo(() => { + const sectionAction = (section: ReviewSectionItem | null, title: string): MenuAction => ({ + id: section ? `section:${section.id}` : `unavailable:${title}`, + title: section?.id === selectedSection?.id ? `${title} (selected)` : title, + attributes: section ? undefined : { disabled: true }, + }); + const actions: MenuAction[] = [ + sectionAction(sectionMenu.workingTree, "Working tree"), + sectionAction(sectionMenu.branchChanges, "Branch changes"), + sectionAction(sectionMenu.latestTurn, "Latest turn"), + ]; + + if (sectionMenu.turns.length > 0) { + actions.push({ + id: "turns", + title: "Turn", + subactions: sectionMenu.turns.map((section) => ({ + id: `section:${section.id}`, + title: section.id === selectedSection?.id ? `${section.title} (selected)` : section.title, + subtitle: section.subtitle ?? undefined, + })), + }); + } + + actions.push({ + id: "refresh", + title: "Refresh current diff", + attributes: { + disabled: + !selectedSection || + loadingGitDiffs || + (selectedSection.kind === "turn" && loadingTurnIds[selectedSection.id] === true), + }, + }); + return actions; + }, [loadingGitDiffs, loadingTurnIds, sectionMenu, selectedSection]); + const handleAndroidSectionMenuAction = useCallback( + (event: { nativeEvent: { event: string } }) => { + const id = event.nativeEvent.event; + if (id === "refresh") { + void refreshSelectedSection(); + } else if (id.startsWith("section:")) { + selectSection(id.slice("section:".length)); + } + }, + [refreshSelectedSection, selectSection], + ); const handleRetryEnvironment = useCallback(() => { void retryEnvironment(environmentId); }, [environmentId, retryEnvironment]); + const androidHeaderSubtitle = [ + selectedSection?.title, + headerDiffSummary.additions, + headerDiffSummary.deletions, + ] + .filter((part): part is string => Boolean(part)) + .join(" · "); const listHeader = useMemo(() => { const children: ReactElement[] = []; @@ -581,18 +641,44 @@ export function ReviewSheet() { return ( <> - {layout.usesSplitView || showSectionToolbar || panes.supportsAuxiliaryPane ? ( + {isAndroid ? ( + router.back()} + trailing={ + showSectionToolbar ? ( + + + + ) : null + } + /> + ) : null} + + {!isAndroid && (layout.usesSplitView || showSectionToolbar || panes.supportsAuxiliaryPane) ? ( {layout.usesSplitView ? ( (null); + const keyboardTextValueRef = useRef(""); + const [keyboardInputValue, setKeyboardInputValue] = useState(""); const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; const theme = props.theme ?? getPierreTerminalTheme(appearanceScheme); const { onInput, onResize } = props; @@ -222,11 +224,25 @@ export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurf }, [NativeTerminalSurfaceView, props.keyboardFocusRequest]); const handleKeyboardInput = useCallback( - (data: string) => { - if (data.length > 0) { - onInput(data); - keyboardInputRef.current?.clear(); + (nextText: string) => { + if (nextText.length === 0) { + keyboardTextValueRef.current = ""; + setKeyboardInputValue(""); + return; } + + const previousText = keyboardTextValueRef.current; + const input = + previousText.length > 0 && nextText.startsWith(previousText) + ? nextText.slice(previousText.length) + : nextText; + + keyboardTextValueRef.current = nextText; + if (input.length > 0) { + onInput(input); + } + setKeyboardInputValue(""); + keyboardInputRef.current?.clear(); }, [onInput], ); @@ -256,6 +272,7 @@ export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurf editable={props.isRunning} keyboardType="ascii-capable" style={{ bottom: 0, height: 1, left: 0, opacity: 0.01, position: "absolute", width: 1 }} + value={keyboardInputValue} onChangeText={handleKeyboardInput} onKeyPress={(event) => { if (event.nativeEvent.key === "Backspace") { diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index a48a2cc6cae..f73cff9725c 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -19,6 +19,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState, type RefObject import { ActivityIndicator, Image, + Platform, Pressable, useColorScheme, View, @@ -64,7 +65,7 @@ import { ComposerCommandPopover, type ComposerCommandItem } from "./ComposerComm * Height of the collapsed composer (pill + vertical padding, excluding safe-area inset). * Exported so the parent can compute feed overlap / content insets. */ -export const COMPOSER_COLLAPSED_CHROME = 60; +export const COMPOSER_COLLAPSED_CHROME = Platform.OS === "android" ? 120 : 60; /** * Height of the expanded composer (card + toolbar + vertical padding, excluding safe-area inset). @@ -214,9 +215,14 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer const [previewImageUri, setPreviewImageUri] = useState(null); const hasContent = props.draftMessage.trim().length > 0 || props.draftAttachments.length > 0; - const isExpanded = isFocused; + const isExpanded = isFocused || (Platform.OS === "android" && hasContent); const canSend = hasContent; + useEffect(() => { + if (Platform.OS !== "android") return; + onExpandedChange?.(isExpanded); + }, [isExpanded, onExpandedChange]); + const onPressImage = useCallback( (uri: string) => { wasExpandedBeforePreviewRef.current = isFocused; @@ -234,12 +240,16 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer const handleFocus = useCallback(() => { setIsFocused(true); - onExpandedChange?.(true); + if (Platform.OS !== "android") { + onExpandedChange?.(true); + } }, [onExpandedChange]); const handleBlur = useCallback(() => { setIsFocused(false); - onExpandedChange?.(false); + if (Platform.OS !== "android") { + onExpandedChange?.(false); + } }, [onExpandedChange]); const showStopAction = props.selectedThread.session?.status === "running" || @@ -783,14 +793,14 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer ) : null} - {/* Toolbar row — matches draft page layout (expanded only) */} - {isExpanded ? ( + {isExpanded || Platform.OS === "android" ? ( void props.onPickDraftImages()} showChevron={false} @@ -817,8 +827,9 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer label={configurationLabel} /> - {showStopAction ? ( + {isExpanded && showStopAction ? ( ) : null} - + {isExpanded ? ( + + ) : null} ) : null} diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 2f9e4a94821..700deb367c8 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -27,7 +27,7 @@ import { AppText as Text } from "../../components/AppText"; import type { ComposerEditorHandle } from "../../components/ComposerEditor"; import type { StatusTone } from "../../components/StatusPill"; import type { DraftComposerImageAttachment } from "../../lib/composerImages"; -import { CHAT_CONTENT_MAX_WIDTH, type LayoutVariant } from "../../lib/layout"; +import type { LayoutVariant } from "../../lib/layout"; import { scopedThreadKey } from "../../lib/scopedEntities"; import type { PendingApproval, @@ -68,8 +68,9 @@ export interface ThreadDetailScreenProps { readonly threadCwd: string | null; readonly selectedThreadQueueCount: number; readonly serverConfig: T3ServerConfig | null; - readonly layoutVariant?: LayoutVariant; + readonly contentTopInset?: number; readonly usesAutomaticContentInsets?: boolean; + readonly layoutVariant?: LayoutVariant; readonly onOpenDrawer: () => void; readonly onOpenConnectionEditor: () => void; readonly onChangeDraftMessage: (value: string) => void; @@ -209,6 +210,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread const insets = useSafeAreaInsets(); const headerHeight = useHeaderHeight(); + const contentTopInset = props.contentTopInset ?? headerHeight; const agentLabel = `${props.selectedThread.modelSelection.instanceId} agent`; const selectedThreadKey = scopedThreadKey(props.environmentId, props.selectedThread.id); const composerEditorRef = useRef(null); @@ -235,7 +237,6 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread const showContent = props.showContent ?? true; const layoutVariant = props.layoutVariant ?? "compact"; const isSplitLayout = layoutVariant === "split"; - const contentMaxWidth = isSplitLayout ? CHAT_CONTENT_MAX_WIDTH : undefined; const selectedInstanceId = props.selectedThread.modelSelection.instanceId; useStreamingHaptics(props.selectedThread.id, props.selectedThreadFeed); const selectedProviderSkills = useMemo( @@ -384,11 +385,9 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread freeze={freeze} anchorMessageId={anchorMessageId} contentInsetEndAdjustment={contentInsetEndAdjustment} - contentTopInset={headerHeight} + contentTopInset={contentTopInset} contentBottomInset={estimatedOverlayHeight} - contentMaxWidth={contentMaxWidth} layoutVariant={layoutVariant} - usesAutomaticContentInsets={props.usesAutomaticContentInsets} skills={selectedProviderSkills} /> @@ -402,46 +401,39 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread style={{ position: "absolute", bottom: 0, left: 0, right: 0 }} offset={{ closed: 0, opened: 0 }} > - - - {props.activeWorkStartedAt ? ( - - ) : null} - - {props.activePendingApproval || props.activePendingUserInput ? ( - - {props.activePendingApproval ? ( - - ) : null} - {props.activePendingUserInput ? ( - - ) : null} - - ) : null} - + + {props.activeWorkStartedAt ? ( + + ) : null} + + {props.activePendingApproval || props.activePendingUserInput ? ( + + {props.activePendingApproval ? ( + + ) : null} + {props.activePendingUserInput ? ( + + ) : null} + + ) : null} ; readonly contentTopInset?: number; readonly contentBottomInset?: number; - readonly contentMaxWidth?: number; readonly layoutVariant?: LayoutVariant; - readonly usesAutomaticContentInsets?: boolean; readonly skills?: ReadonlyArray; } @@ -175,6 +176,12 @@ const MARKDOWN_COLORS = { }, } as const; +const MARKDOWN_MONO_FONT = Platform.select({ + ios: "ui-monospace", + android: "monospace", + default: "monospace", +}); + interface MarkdownStyleSets { readonly user: MarkdownStyleSet; readonly assistant: MarkdownStyleSet; @@ -251,6 +258,127 @@ const MarkdownExternalLink = memo(function MarkdownExternalLink(props: { ); }); +function MarkdownCodeBlock(props: { + readonly backgroundColor: string; + readonly borderColor: string; + readonly content: string; + readonly headerTextColor: string; + readonly highlightCode: boolean; + readonly language?: string | null; + readonly textColor: string; + readonly theme: ReviewDiffTheme; +}) { + const highlighted = useMarkdownCodeHighlight({ + code: props.content, + enabled: props.highlightCode && Boolean(props.language?.trim()), + language: props.language, + theme: props.theme, + }); + let tokenOffset = 0; + + return ( + + {props.language ? ( + + + {props.language} + + + ) : null} + + + {highlighted + ? highlighted.map((line, lineIndex) => { + const lineStartOffset = tokenOffset; + const lineText = line.map((token) => token.content).join(""); + const renderedLine = ( + + {line.map((token) => { + const startOffset = tokenOffset; + tokenOffset += token.content.length; + const fontStyle = + token.fontStyle !== null && (token.fontStyle & 1) === 1 + ? ("italic" as const) + : ("normal" as const); + const fontWeight = + token.fontStyle !== null && (token.fontStyle & 2) === 2 + ? ("700" as const) + : ("400" as const); + + return ( + + {token.content} + + ); + })} + {lineIndex + 1 < highlighted.length ? "\n" : ""} + + ); + if (lineIndex + 1 < highlighted.length) { + tokenOffset += 1; + } + return renderedLine; + }) + : props.content} + + + + ); +} + function useReviewCommentColors(): ReviewCommentColors { const colorScheme = useColorScheme(); const isDark = colorScheme === "dark"; @@ -276,7 +404,8 @@ function useReviewCommentColors(): ReviewCommentColors { function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSets { const colorScheme = useColorScheme(); - const colors = MARKDOWN_COLORS[colorScheme === "dark" ? "dark" : "light"]; + const themeMode = colorScheme === "dark" ? "dark" : "light"; + const colors = MARKDOWN_COLORS[themeMode]; const inlineSkillForeground = String(useThemeColor("--color-inline-skill-foreground")); return useMemo(() => { @@ -331,7 +460,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe fontFamilies: { regular: "DMSans_400Regular", heading: "DMSans_700Bold", - mono: "ui-monospace", + mono: MARKDOWN_MONO_FONT, }, headingWeight: "700", borderRadius: { @@ -386,6 +515,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe blockBackgroundColor: string, blockTextColor: string, preserveSoftBreaks: boolean, + highlightCode: boolean, ): CustomRenderers => ({ link: ({ children, href = "" }) => { const presentation = resolveMarkdownLinkPresentation(href); @@ -477,9 +607,10 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe {value} @@ -491,58 +622,17 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe soft_break: () => {"\n"}, } : {}), - code_block: ({ content, language }) => ( - - {language ? ( - - - {language} - - - ) : null} - - - {content} - - - + code_block: ({ content = "", language }) => ( + ), }); @@ -601,6 +691,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe markdownUserFenceBg, markdownUserFenceText, true, + false, ), nativeTextStyle: { color: markdownUserBodyColor, @@ -630,6 +721,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe markdownCodeBg, markdownCodeText, false, + true, ), nativeTextStyle: { color: markdownBodyColor, @@ -1146,12 +1238,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { headers?: Record; } | null>(null); const horizontalPadding = props.layoutVariant === "split" ? 20 : 16; - const contentHorizontalPadding = deriveCenteredContentHorizontalPadding({ - viewportWidth, - maxContentWidth: props.contentMaxWidth ?? null, - minimumPadding: horizontalPadding, - }); - const contentWidth = Math.max(0, viewportWidth - contentHorizontalPadding * 2); + const contentWidth = Math.max(0, viewportWidth - horizontalPadding * 2); const userBubbleMaxWidth = contentWidth * 0.85; const reviewCommentBubbleWidth = Math.min(Math.max(280, contentWidth * 0.85), contentWidth); const insets = useSafeAreaInsets(); @@ -1203,7 +1290,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { markdownStyles, reviewCommentColors, userBubbleColor, - viewportWidth, }), [ copiedRowId, @@ -1212,7 +1298,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { markdownStyles, reviewCommentColors, userBubbleColor, - viewportWidth, ], ); const expandedWorkGroupIds = useMemo(() => { @@ -1473,14 +1558,8 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { ref={props.listRef} key={props.threadId} style={{ flex: 1 }} - contentInsetAdjustmentBehavior="never" automaticallyAdjustsScrollIndicatorInsets={false} - {...(props.usesAutomaticContentInsets - ? { - contentInset: { top: topContentInset, left: 0, right: 0, bottom: 0 }, - scrollIndicatorInsets: { top: 0, left: 0, right: 0, bottom: 0 }, - } - : { scrollIndicatorInsets: { top: topContentInset, bottom: 0 } })} + scrollIndicatorInsets={{ top: topContentInset, bottom: 0 }} {...(anchoredEndSpace ? { anchoredEndSpace } : {})} contentInsetEndAdjustment={props.contentInsetEndAdjustment} freeze={props.freeze} @@ -1509,12 +1588,10 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { keyboardLiftBehavior="whenAtEnd" estimatedItemSize={180} initialScrollAtEnd - ListHeaderComponent={ - props.usesAutomaticContentInsets ? null : - } + ListHeaderComponent={} contentContainerStyle={{ paddingTop: 12, - paddingHorizontal: contentHorizontalPadding, + paddingHorizontal: horizontalPadding, }} /> {props.feed.length === 0 ? ( diff --git a/apps/mobile/src/features/threads/ThreadNavigationDrawer.tsx b/apps/mobile/src/features/threads/ThreadNavigationDrawer.tsx index 3710bf255ac..8acaba53747 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationDrawer.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationDrawer.tsx @@ -1,4 +1,3 @@ -import { SymbolView } from "expo-symbols"; import { useCallback, useEffect, useMemo, useState } from "react"; import { type ColorValue, @@ -19,6 +18,7 @@ import Animated, { import { useThemeColor } from "../../lib/useThemeColor"; import { AppText as Text } from "../../components/AppText"; +import { SymbolView } from "../../components/AppSymbol"; import { StatusPill } from "../../components/StatusPill"; import { useProjects, useThreadShells } from "../../state/entities"; import { scopedThreadKey } from "../../lib/scopedEntities"; @@ -131,14 +131,13 @@ export function ThreadNavigationDrawer(props: { bottom: 0, width: drawerWidth, backgroundColor: drawerBg, - paddingTop: insets.top + 10, - paddingBottom: Math.max(insets.bottom, 18), + paddingTop: insets.top, boxShadow: `20px 0 36px ${String(drawerShadow)}`, }, drawerStyle, ]} > - + Threads { diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 22ca6cb5d29..c4c651df95c 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -1,7 +1,6 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { LegendList } from "@legendapp/list/react-native"; import type { MenuAction } from "@react-native-menu/menu"; -import { SymbolView } from "expo-symbols"; import { useRouter } from "expo-router"; import { memo, useCallback, useMemo, useRef, useState } from "react"; import type { ColorValue } from "react-native"; @@ -11,6 +10,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppText as Text } from "../../components/AppText"; import { ControlPillMenu } from "../../components/ControlPill"; +import { SymbolView } from "../../components/AppSymbol"; import { StatusPill } from "../../components/StatusPill"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { relativeTime } from "../../lib/time"; diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index f0aeae4a724..c64e77d228e 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -13,6 +13,10 @@ import { dismissGitActionResult, useGitActionProgress } from "../../state/use-vc import { vcsEnvironment } from "../../state/vcs"; import { EmptyState } from "../../components/EmptyState"; +import { + AndroidScreenHeader, + type AndroidHeaderAction, +} from "../../components/AndroidScreenHeader"; import { LoadingScreen } from "../../components/LoadingScreen"; import { buildThreadFilesNavigation, @@ -339,11 +343,28 @@ function ThreadRouteContent( }, [layout.usesSplitView]); const handleOpenGitInspector = useCallback(() => { + if (!fileInspector.supported) { + if (selectedThread === null) { + return; + } + router.push({ + pathname: "/threads/[environmentId]/[threadId]/git", + params: { + environmentId: selectedThread.environmentId, + threadId: selectedThread.id, + }, + }); + return; + } setInspectorSelection({ routeThreadIdentity, mode: "git" }); showAuxiliaryPane("inspector"); - }, [routeThreadIdentity, showAuxiliaryPane]); + }, [fileInspector.supported, routeThreadIdentity, router, selectedThread, showAuxiliaryPane]); const handleOpenFilesInspector = useCallback(() => { - if (!fileInspector.supported || selectedThread === null || selectedThreadCwd === null) { + if (selectedThread === null || selectedThreadCwd === null) { + return; + } + if (!fileInspector.supported) { + router.push(buildThreadFilesNavigation(selectedThread)); return; } setInspectorSelection({ @@ -355,6 +376,7 @@ function ThreadRouteContent( fileInspector.supported, props.renderInspector, routeThreadIdentity, + router, selectedThread, selectedThreadCwd, showAuxiliaryPane, @@ -562,13 +584,61 @@ function ThreadRouteContent( connectionState: routeConnectionState, }); const serverConfig = routeEnvironmentRuntime?.serverConfig ?? null; + const androidHeaderActions = useMemo>(() => { + if (Platform.OS !== "android") return []; + + const actions: AndroidHeaderAction[] = []; + if (props.onReturnToThread) { + actions.push({ + accessibilityLabel: "Return to chat", + icon: "chevron.left", + onPress: props.onReturnToThread, + }); + } + if (selectedThreadCwd !== null) { + actions.push({ + accessibilityLabel: "Open files", + icon: "folder", + onPress: handleOpenFilesInspector, + }); + } + if (selectedThreadProject?.workspaceRoot) { + actions.push({ + accessibilityLabel: "Open terminal", + icon: "terminal", + onPress: () => handleOpenTerminal(null), + }); + } + actions.push({ + accessibilityLabel: "Open git controls", + icon: "point.topleft.down.curvedto.point.bottomright.up", + onPress: handleOpenGitInspector, + }); + if (fileInspector.supported && selectedThreadCwd !== null) { + actions.push({ + accessibilityLabel: "Toggle inspector", + icon: "sidebar.right", + onPress: handleToggleInspector, + }); + } + return actions; + }, [ + fileInspector.supported, + handleOpenFilesInspector, + handleOpenTerminal, + handleOpenGitInspector, + handleToggleInspector, + props.onReturnToThread, + selectedThreadCwd, + selectedThreadProject?.workspaceRoot, + ]); return ( <> {activeInspectorRenderer ? : null} - - router.back()} + actions={androidHeaderActions} /> - - - - {props.onReturnToThread ? ( - + + + + + + {props.onReturnToThread ? ( + + ) : null} + + + - ) : null} - - - + + )} @@ -676,6 +757,7 @@ function ThreadRouteContent( onNativePasteImages={composer.onNativePasteImages} onRemoveDraftImage={composer.onRemoveDraftImage} serverConfig={serverConfig} + contentTopInset={Platform.OS === "android" ? 0 : undefined} onStopThread={handleStopThread} onSendMessage={composer.onSendMessage} onReconnectEnvironment={handleReconnectEnvironment} diff --git a/apps/mobile/src/features/threads/markdownCodeHighlightState.ts b/apps/mobile/src/features/threads/markdownCodeHighlightState.ts new file mode 100644 index 00000000000..c415cf73980 --- /dev/null +++ b/apps/mobile/src/features/threads/markdownCodeHighlightState.ts @@ -0,0 +1,87 @@ +import { useAtomValue } from "@effect/atom-react"; +import * as Data from "effect/Data"; +import * as Effect from "effect/Effect"; +import { AsyncResult, Atom } from "effect/unstable/reactivity"; +import { useMemo } from "react"; + +import { + highlightCodeSnippet, + type ReviewDiffTheme, + type ReviewHighlightedToken, +} from "../review/shikiReviewHighlighter"; + +const MARKDOWN_CODE_HIGHLIGHT_IDLE_TTL_MS = 5 * 60_000; + +export type MarkdownHighlightedCode = ReadonlyArray>; + +export interface MarkdownCodeHighlightInput { + readonly code: string; + readonly enabled: boolean; + readonly language: string; + readonly theme: ReviewDiffTheme; +} + +type MarkdownCodeHighlighter = ( + input: MarkdownCodeHighlightInput, +) => Promise; + +class MarkdownCodeHighlightCacheKey extends Data.Class {} + +class MarkdownCodeHighlightError extends Data.TaggedError("MarkdownCodeHighlightError")<{ + readonly cause: unknown; +}> {} + +export function createMarkdownCodeHighlightAtomFamily(options?: { + readonly highlight?: MarkdownCodeHighlighter; + readonly idleTtlMs?: number; +}) { + const highlight = + options?.highlight ?? + ((input: MarkdownCodeHighlightInput) => + input.enabled + ? highlightCodeSnippet({ + code: input.code, + language: input.language, + theme: input.theme, + }) + : Promise.resolve(null)); + const idleTtlMs = options?.idleTtlMs ?? MARKDOWN_CODE_HIGHLIGHT_IDLE_TTL_MS; + const family = Atom.family((request: MarkdownCodeHighlightCacheKey) => + Atom.make( + Effect.tryPromise({ + try: () => highlight(request), + catch: (cause) => new MarkdownCodeHighlightError({ cause }), + }), + ).pipe( + Atom.setIdleTTL(idleTtlMs), + Atom.withLabel(`mobile:thread-markdown-code-highlight:${request.theme}:${request.language}`), + ), + ); + + return (input: MarkdownCodeHighlightInput) => family(new MarkdownCodeHighlightCacheKey(input)); +} + +export const markdownCodeHighlightAtom = createMarkdownCodeHighlightAtomFamily(); + +export function useMarkdownCodeHighlight(input: { + readonly code: string; + readonly enabled: boolean; + readonly language: string | null | undefined; + readonly theme: ReviewDiffTheme; +}): MarkdownHighlightedCode | null { + const normalizedLanguage = input.language?.trim() || "text"; + const enabled = input.enabled && Boolean(input.language?.trim()); + const atomLanguage = enabled ? normalizedLanguage : "text"; + const highlightAtom = useMemo( + () => + markdownCodeHighlightAtom({ + code: enabled ? input.code : "", + enabled, + language: atomLanguage, + theme: input.theme, + }), + [atomLanguage, enabled, input.code, input.theme], + ); + const result = useAtomValue(highlightAtom); + return AsyncResult.isSuccess(result) ? result.value : null; +} diff --git a/apps/mobile/src/features/threads/sidebar-header-actions.android.tsx b/apps/mobile/src/features/threads/sidebar-header-actions.android.tsx new file mode 100644 index 00000000000..cc34b0766ad --- /dev/null +++ b/apps/mobile/src/features/threads/sidebar-header-actions.android.tsx @@ -0,0 +1,21 @@ +import { View } from "react-native"; + +import { T3HeaderButton } from "../../native/T3HeaderButton.android"; +import type { SidebarHeaderActionsProps } from "./sidebar-header-actions"; + +export function SidebarHeaderActions(props: SidebarHeaderActionsProps) { + return ( + + + + + ); +} diff --git a/apps/mobile/src/native/T3ComposerEditor.android.tsx b/apps/mobile/src/native/T3ComposerEditor.android.tsx new file mode 100644 index 00000000000..ccf88c2de4e --- /dev/null +++ b/apps/mobile/src/native/T3ComposerEditor.android.tsx @@ -0,0 +1,6 @@ +export { ComposerEditor } from "./T3ComposerEditor.native"; +export type { + ComposerEditorHandle, + ComposerEditorProps, + ComposerEditorSelection, +} from "./T3ComposerEditor.types"; diff --git a/apps/mobile/src/native/T3ComposerEditor.native.tsx b/apps/mobile/src/native/T3ComposerEditor.native.tsx new file mode 100644 index 00000000000..eb935030e81 --- /dev/null +++ b/apps/mobile/src/native/T3ComposerEditor.native.tsx @@ -0,0 +1,281 @@ +import { collectComposerInlineTokens } from "@t3tools/shared/composerInlineTokens"; +import { requireNativeView } from "expo"; +import { + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, + type Ref, +} from "react"; +import type { NativeSyntheticEvent, StyleProp, ViewProps, ViewStyle } from "react-native"; +import { Image, StyleSheet } from "react-native"; + +import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; +import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links"; +import { MOBILE_TYPOGRAPHY } from "../lib/typography"; +import { useThemeColor } from "../lib/useThemeColor"; +import { + acknowledgeComposerNativeEvent, + isComposerNativeEcho, + pruneAcknowledgedComposerNativeEvents, + resolveComposerControlledEventCount, + type ComposerNativeEventSnapshot, +} from "./composerEditorRevision"; +import type { ComposerEditorProps, ComposerEditorSelection } from "./T3ComposerEditor.types"; + +const NATIVE_MODULE_NAME = "T3ComposerEditor"; +const EMPTY_SKILLS: NonNullable = []; + +type NativeEditorEvent = NativeSyntheticEvent<{ + readonly value: string; + readonly selection: ComposerEditorSelection; + readonly eventCount: number; +}>; + +type NativeSelectionEvent = NativeSyntheticEvent<{ + readonly value: string; + readonly selection: ComposerEditorSelection; + readonly eventCount: number; +}>; + +type NativePasteImagesEvent = NativeSyntheticEvent<{ + readonly uris: ReadonlyArray; +}>; + +interface NativeComposerEditorRef { + focus: () => Promise; + blur: () => Promise; + setSelection: (start: number, end: number) => Promise; +} + +interface NativeComposerEditorProps extends ViewProps { + readonly ref?: Ref; + readonly controlledDocumentJson: string; + readonly themeJson: string; + readonly placeholder: string; + readonly fontFamily: string; + readonly fontSize: number; + readonly lineHeight: number; + readonly contentInsetVertical: number; + readonly editable: boolean; + readonly scrollEnabled: boolean; + readonly autoFocus: boolean; + readonly autoCorrect: boolean; + readonly spellCheck: boolean; + readonly onComposerChange: (event: NativeEditorEvent) => void; + readonly onComposerSelectionChange?: (event: NativeSelectionEvent) => void; + readonly onComposerPasteImages?: (event: NativePasteImagesEvent) => void; + readonly onComposerFocus?: () => void; + readonly onComposerBlur?: () => void; +} + +const NativeView = requireNativeView(NATIVE_MODULE_NAME); + +function basename(path: string): string { + const separator = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")); + return separator >= 0 ? path.slice(separator + 1) : path; +} + +function fileIconUri(path: string): string { + return Image.resolveAssetSource(markdownFileIconSource(resolveMarkdownFileIcon(path))).uri; +} + +export function ComposerEditor({ + ref, + skills = EMPTY_SKILLS, + selection, + style, + textStyle, + onChangeText, + onSelectionChange, + onPasteImages, + onFocus, + onBlur, + contentInsetVertical = 0, + ...props +}: ComposerEditorProps) { + const nativeRef = useRef(null); + const mostRecentEventCountRef = useRef(0); + const [mostRecentEventCount, setMostRecentEventCount] = useState(0); + const [nativeEventSequence, setNativeEventSequence] = useState(0); + const previousRenderedEventSequenceRef = useRef(0); + const nativeEventSnapshotsRef = useRef([ + { eventCount: 0, value: props.value, selection: selection ?? null }, + ]); + const confirmedTokensRef = useRef(collectComposerInlineTokens(props.value)); + const textColor = useThemeColor("--color-foreground"); + const placeholderColor = useThemeColor("--color-placeholder"); + const chipBackground = useThemeColor("--color-subtle"); + const chipBorder = useThemeColor("--color-border"); + const chipText = useThemeColor("--color-foreground"); + const skillBackground = useThemeColor("--color-inline-skill-background"); + const skillBorder = useThemeColor("--color-inline-skill-border"); + const skillText = useThemeColor("--color-inline-skill-foreground"); + const fileTint = useThemeColor("--color-icon-muted"); + + useImperativeHandle( + ref, + () => ({ + focus: () => void nativeRef.current?.focus(), + blur: () => void nativeRef.current?.blur(), + setSelection: (nextSelection) => + void nativeRef.current?.setSelection(nextSelection.start, nextSelection.end), + }), + [], + ); + + const skillLabels = useMemo( + () => new Map(skills.map((skill) => [skill.name, skill.displayName?.trim() || skill.name])), + [skills], + ); + const tokensJson = useMemo(() => { + const tokens = collectComposerInlineTokens(props.value, { + preserveTrailingFrom: confirmedTokensRef.current, + }); + confirmedTokensRef.current = tokens; + return JSON.stringify( + tokens.map((token) => ({ + type: token.type, + source: token.source, + start: token.start, + end: token.end, + label: + token.type === "skill" + ? (skillLabels.get(token.value) ?? token.value) + : basename(token.value), + iconUri: token.type === "mention" ? fileIconUri(token.value) : null, + })), + ); + }, [props.value, skillLabels]); + const includesNativeEvent = nativeEventSequence !== previousRenderedEventSequenceRef.current; + const controlledEventCount = includesNativeEvent + ? resolveComposerControlledEventCount( + props.value, + selection ?? null, + mostRecentEventCount, + nativeEventSnapshotsRef.current, + ) + : mostRecentEventCount; + const acknowledgesLatestNativeEvent = isComposerNativeEcho( + props.value, + selection ?? null, + mostRecentEventCount, + nativeEventSnapshotsRef.current, + ); + const isNativeEcho = + includesNativeEvent && + controlledEventCount === mostRecentEventCount && + acknowledgesLatestNativeEvent; + const controlledDocumentJson = JSON.stringify({ + value: props.value, + selection: isNativeEcho ? null : (selection ?? null), + tokensJson, + mostRecentEventCount: controlledEventCount, + isNativeEcho, + }); + useEffect(() => { + previousRenderedEventSequenceRef.current = nativeEventSequence; + }, [nativeEventSequence]); + useEffect(() => { + if (!acknowledgesLatestNativeEvent) return; + nativeEventSnapshotsRef.current = pruneAcknowledgedComposerNativeEvents( + nativeEventSnapshotsRef.current, + mostRecentEventCount, + ); + }, [acknowledgesLatestNativeEvent, mostRecentEventCount]); + const acceptNativeEvent = useCallback( + (eventCount: number, value: string, nextSelection: ComposerEditorSelection) => { + const acknowledgedEventCount = acknowledgeComposerNativeEvent( + mostRecentEventCountRef.current, + eventCount, + ); + if (acknowledgedEventCount === null) { + return false; + } + mostRecentEventCountRef.current = acknowledgedEventCount; + nativeEventSnapshotsRef.current.push({ + eventCount: acknowledgedEventCount, + value, + selection: nextSelection, + }); + return acknowledgedEventCount; + }, + [], + ); + const themeJson = JSON.stringify({ + text: String(textColor), + placeholder: String(placeholderColor), + chipBackground: String(chipBackground), + chipBorder: String(chipBorder), + chipText: String(chipText), + skillBackground: String(skillBackground), + skillBorder: String(skillBorder), + skillText: String(skillText), + fileTint: String(fileTint), + }); + const resolvedTextStyle = StyleSheet.flatten(textStyle) ?? {}; + return ( + } + onComposerChange={(event) => { + const acknowledgedEventCount = acceptNativeEvent( + event.nativeEvent.eventCount, + event.nativeEvent.value, + event.nativeEvent.selection, + ); + if (acknowledgedEventCount === false) return; + onChangeText(event.nativeEvent.value); + onSelectionChange?.(event.nativeEvent.selection); + setMostRecentEventCount(acknowledgedEventCount); + setNativeEventSequence((sequence) => sequence + 1); + }} + onComposerSelectionChange={(event) => { + const acknowledgedEventCount = acceptNativeEvent( + event.nativeEvent.eventCount, + event.nativeEvent.value, + event.nativeEvent.selection, + ); + if (acknowledgedEventCount === false) return; + onSelectionChange?.(event.nativeEvent.selection); + setMostRecentEventCount(acknowledgedEventCount); + setNativeEventSequence((sequence) => sequence + 1); + }} + onComposerPasteImages={(event) => onPasteImages?.(event.nativeEvent.uris)} + onComposerFocus={onFocus} + onComposerBlur={onBlur} + /> + ); +} + +export type { + ComposerEditorHandle, + ComposerEditorProps, + ComposerEditorSelection, +} from "./T3ComposerEditor.types"; diff --git a/apps/mobile/src/native/T3HeaderButton.android.tsx b/apps/mobile/src/native/T3HeaderButton.android.tsx new file mode 100644 index 00000000000..74908abd16c --- /dev/null +++ b/apps/mobile/src/native/T3HeaderButton.android.tsx @@ -0,0 +1,26 @@ +import { requireNativeView } from "expo"; +import type { NativeSyntheticEvent, StyleProp, ViewProps, ViewStyle } from "react-native"; + +interface NativeHeaderButtonProps extends ViewProps { + readonly label: string; + readonly systemImage: "gearshape" | "square.and.pencil"; + readonly onTriggered: (event: NativeSyntheticEvent>) => void; +} + +const NativeHeaderButton = requireNativeView("T3NativeControls"); + +export function T3HeaderButton(props: { + readonly accessibilityLabel: string; + readonly icon: NativeHeaderButtonProps["systemImage"]; + readonly onPress: () => void; + readonly style?: StyleProp; +}) { + return ( + + ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c2f25f952c..1a35dce1161 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -256,6 +256,9 @@ importers: '@t3tools/shared': specifier: workspace:* version: link:../../packages/shared + '@tabler/icons-react-native': + specifier: ^3.44.0 + version: 3.44.0(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react@19.2.3) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -4364,6 +4367,15 @@ packages: '@t3tools/mobile-terminal-native@file:apps/mobile/modules/t3-terminal': resolution: {directory: apps/mobile/modules/t3-terminal, type: directory} + '@tabler/icons-react-native@3.44.0': + resolution: {integrity: sha512-/Ppp68bpl9vQQsI/c+YiDC9dnLq4JUo0YI5qP72OQMC1WmCaRzdYn3CU79q01M2L3MfXO9a8WctWN8ay1Odkow==} + peerDependencies: + react: '>= 16.5.1' + react-native-svg: '>= 13.0.0' + + '@tabler/icons@3.44.0': + resolution: {integrity: sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==} + '@tailwindcss/node@4.2.1': resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} @@ -13866,6 +13878,14 @@ snapshots: '@t3tools/mobile-terminal-native@file:apps/mobile/modules/t3-terminal': {} + '@tabler/icons-react-native@3.44.0(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react@19.2.3)': + dependencies: + '@tabler/icons': 3.44.0 + react: 19.2.3 + react-native-svg: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + + '@tabler/icons@3.44.0': {} + '@tailwindcss/node@4.2.1': dependencies: '@jridgewell/remapping': 2.3.5