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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.gitattributes text eol=lf
*.patch text eol=lf
12 changes: 10 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,17 @@ jobs:
id: ghostty-cache
with:
path: ghostty-src
key: ghostty-src-${{ steps.ghostty.outputs.full }}
key: ghostty-src-${{ steps.ghostty.outputs.full }}-${{ hashFiles('packages/libghostty/patches/*.patch') }}
- name: download ghostty source
if: steps.ghostty-cache.outputs.cache-hit != 'true'
run: |
curl -fsSL "https://github.com/ghostty-org/ghostty/archive/${{ steps.ghostty.outputs.full }}.tar.gz" -o ghostty.tar.gz
mkdir -p ghostty-src
tar xzf ghostty.tar.gz -C ghostty-src --strip-components=1
git -C ghostty-src init --quiet
git -C ghostty-src apply --unidiff-zero "$GITHUB_WORKSPACE/packages/libghostty/patches/osc52-clipboard-write.patch"
rm -rf ghostty-src/.git
touch ghostty-src/.git
rm ghostty.tar.gz
- name: compile
working-directory: ghostty-src
Expand Down Expand Up @@ -164,13 +168,17 @@ jobs:
id: ghostty-cache
with:
path: ghostty-src
key: ghostty-src-${{ steps.ghostty.outputs.full }}
key: ghostty-src-${{ steps.ghostty.outputs.full }}-${{ hashFiles('packages/libghostty/patches/*.patch') }}
- name: download ghostty source
if: steps.ghostty-cache.outputs.cache-hit != 'true'
run: |
curl -fsSL "https://github.com/ghostty-org/ghostty/archive/${{ steps.ghostty.outputs.full }}.tar.gz" -o ghostty.tar.gz
mkdir -p ghostty-src
tar xzf ghostty.tar.gz -C ghostty-src --strip-components=1
git -C ghostty-src init --quiet
git -C ghostty-src apply --unidiff-zero "$GITHUB_WORKSPACE/packages/libghostty/patches/osc52-clipboard-write.patch"
rm -rf ghostty-src/.git
touch ghostty-src/.git
rm ghostty.tar.gz
- name: compile wasm
working-directory: ghostty-src
Expand Down
15 changes: 12 additions & 3 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,17 @@ jobs:
id: ghostty-cache
with:
path: ghostty
key: ghostty-src-${{ steps.ghostty.outputs.commit }}
key: ghostty-src-${{ steps.ghostty.outputs.commit }}-${{ hashFiles('packages/libghostty/patches/*.patch') }}
- name: download ghostty source
if: steps.ghostty-cache.outputs.cache-hit != 'true'
shell: bash
run: |
curl -fsSL "https://github.com/ghostty-org/ghostty/archive/${{ steps.ghostty.outputs.commit }}.tar.gz" -o ghostty.tar.gz
mkdir -p ghostty
tar xzf ghostty.tar.gz -C ghostty --strip-components=1
git -C ghostty init --quiet
git -C ghostty apply --unidiff-zero "$GITHUB_WORKSPACE/packages/libghostty/patches/osc52-clipboard-write.patch"
rm -rf ghostty/.git
rm ghostty.tar.gz
- run: flutter pub get
- run: dart pub global activate very_good_cli
Expand Down Expand Up @@ -229,13 +232,16 @@ jobs:
id: ghostty-cache
with:
path: ghostty
key: ghostty-src-${{ steps.ghostty.outputs.commit }}
key: ghostty-src-${{ steps.ghostty.outputs.commit }}-${{ hashFiles('packages/libghostty/patches/*.patch') }}
- name: download ghostty source
if: steps.ghostty-cache.outputs.cache-hit != 'true'
run: |
curl -fsSL "https://github.com/ghostty-org/ghostty/archive/${{ steps.ghostty.outputs.commit }}.tar.gz" -o ghostty.tar.gz
mkdir -p ghostty
tar xzf ghostty.tar.gz -C ghostty --strip-components=1
git -C ghostty init --quiet
git -C ghostty apply --unidiff-zero "$GITHUB_WORKSPACE/packages/libghostty/patches/osc52-clipboard-write.patch"
rm -rf ghostty/.git
rm ghostty.tar.gz
- name: isolate from repo git
run: touch ghostty/.git
Expand Down Expand Up @@ -282,14 +288,17 @@ jobs:
id: ghostty-cache
with:
path: ghostty
key: ghostty-src-${{ steps.ghostty.outputs.commit }}
key: ghostty-src-${{ steps.ghostty.outputs.commit }}-${{ hashFiles('packages/libghostty/patches/*.patch') }}
- name: download ghostty source
if: steps.ghostty-cache.outputs.cache-hit != 'true'
shell: bash
run: |
curl -fsSL "https://github.com/ghostty-org/ghostty/archive/${{ steps.ghostty.outputs.commit }}.tar.gz" -o ghostty.tar.gz
mkdir -p ghostty
tar xzf ghostty.tar.gz -C ghostty --strip-components=1
git -C ghostty init --quiet
git -C ghostty apply --unidiff-zero "$GITHUB_WORKSPACE/packages/libghostty/patches/osc52-clipboard-write.patch"
rm -rf ghostty/.git
rm ghostty.tar.gz
- run: flutter pub get
- name: install very_good_cli
Expand Down
7 changes: 7 additions & 0 deletions packages/flterm/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### Added

- **OSC 52 clipboard writes**: `TerminalController.onClipboardWrite` forwards
write-only clipboard requests from libghostty to terminal embedders.

## 0.0.4

### Breaking
Expand Down
3 changes: 3 additions & 0 deletions packages/flterm/lib/src/widgets/terminal_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ abstract class TerminalController extends ChangeNotifier
/// Called when the terminal receives a BEL character (0x07).
VoidCallback? onBell;

/// Called when the terminal receives an OSC 52 clipboard write request.
ValueChanged<ClipboardWrite>? onClipboardWrite;

/// Called when the terminal title changes. Read [title] for the value.
VoidCallback? onTitleChanged;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,7 @@ class TerminalControllerImpl extends TerminalController
void _wireTerminalCallbacks() {
terminal.onWritePty = _emitOutput;
terminal.onBell = () => onBell?.call();
terminal.onClipboardWrite = (value) => onClipboardWrite?.call(value);
terminal.onTitleChanged = () => onTitleChanged?.call();
terminal.onPwdChanged = _handlePwdChanged;
terminal.onColorScheme = () => _brightness == .light ? .light : .dark;
Expand Down
12 changes: 12 additions & 0 deletions packages/flterm/test/widgets/terminal_controller_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,18 @@ void main() {
});
});

group('clipboard writes', () {
test('forwards OSC 52 writes', () {
ClipboardWrite? received;
controller.onClipboardWrite = (value) => received = value;

writeTerminalUtf8(controller.terminal, '\x1b]52;c;aGVsbG8=\x1b\\');

expect(received?.selector, 'c'.codeUnitAt(0));
expect(String.fromCharCodes(received!.payload), 'aGVsbG8=');
});
});

group('pwd', () {
test('updates via OSC 7 escape sequence', () {
writeTerminalUtf8(controller.terminal, '\x1b]7;file:///tmp\x07');
Expand Down
19 changes: 19 additions & 0 deletions packages/libghostty/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## Unreleased

### Added

- **OSC 52 clipboard writes**: `Terminal.onClipboardWrite` exposes write-only
clipboard requests with their raw selector and base64 payload. Clipboard read
queries remain disabled.

### Fixed

- **Native asset refreshes**: rerunning the build hook replaces an existing
output library so source or ABI changes cannot reuse a stale binary.
- **Source patch isolation**: downloaded Ghostty sources are patched in their
own Git boundary and marked before cache reuse.
- **Embedded tagged builds**: source compilation passes Ghostty's own version
explicitly instead of inheriting Git tags from an embedding repository.
- **Windows source builds**: each native-asset output uses an isolated local
Zig cache and retries the transient generated-helper scanner race.

## 0.0.11

### Added
Expand Down
11 changes: 7 additions & 4 deletions packages/libghostty/hook/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:io';
import 'package:code_assets/code_assets.dart';
import 'package:hooks/hooks.dart';
import 'package:libghostty/src/hook/fix_ios_page_alignment.dart';
import 'package:libghostty/src/hook/ghostty_source.dart';
import 'package:libghostty/src/hook/library_provider.dart';

void main(List<String> args) async {
Expand All @@ -13,16 +14,18 @@ Future<void> _build(BuildInput input, BuildOutputBuilder output) async {
if (!input.config.buildCodeAssets) return;

output.dependencies.add(input.packageRoot.resolve('ghostty.version'));
for (final patch in ghosttyPatchFiles(input.packageRoot)) {
output.dependencies.add(patch.uri);
}

final targetOS = input.config.code.targetOS;
final libFileName = targetOS.dylibFileName('ghostty');
final installDir = input.outputDirectory;
final libFile = File.fromUri(installDir.resolve('lib/$libFileName'));

if (!libFile.existsSync()) {
final provider = LibraryProvider.resolve(input);
await provider.provide(libFile);
}
if (libFile.existsSync()) libFile.deleteSync();
final provider = LibraryProvider.resolve(input);
await provider.provide(libFile);

if (!libFile.existsSync()) {
throw Exception(
Expand Down
7 changes: 6 additions & 1 deletion packages/libghostty/lib/libghostty.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ library;

export 'src/bindings/bindings.dart' show initializeForWeb;
export 'src/bindings/types/aliases.dart'
show DecodedImage, PngDecoder, TerminalGeometry, X11ColorName;
show
ClipboardWrite,
DecodedImage,
PngDecoder,
TerminalGeometry,
X11ColorName;
export 'src/bindings/types/types.dart'
show
CellColor,
Expand Down
4 changes: 4 additions & 0 deletions packages/libghostty/lib/src/bindings/interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ abstract interface class GhosttyBindings {

void terminalSetOnWritePty(int handle, ValueSetter<Uint8List>? callback);
void terminalSetOnBell(int handle, VoidCallback? callback);
void terminalSetOnClipboardWrite(
int handle,
ValueSetter<ClipboardWrite>? callback,
);
void terminalSetOnTitleChanged(int handle, VoidCallback? callback);
void terminalSetOnPwdChanged(int handle, VoidCallback? callback);
void terminalSetOnEnquiry(int handle, ValueGetter<Uint8List>? callback);
Expand Down
40 changes: 40 additions & 0 deletions packages/libghostty/lib/src/bindings/native/native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3480,6 +3480,46 @@ class NativeBindings implements GhosttyBindings {
);
}

@override
void terminalSetOnClipboardWrite(
int handle,
ValueSetter<ClipboardWrite>? callback,
) {
final map = _callables.putIfAbsent(handle, () => {});
const option = TerminalOption.clipboardWrite;
map[option]?.close();

if (callback == null) {
map.remove(option);
ghostty_terminal_set(Pointer.fromAddress(handle), option, nullptr);
return;
}

final callable =
NativeCallable<
Void Function(Terminal, Pointer<Void>, Uint8, Pointer<Uint8>, Size)
>.isolateLocal((
Terminal terminal,
Pointer<Void> userdata,
int selector,
Pointer<Uint8> data,
int len,
) {
try {
callback((
selector: selector,
payload: Uint8List.fromList(data.asTypedList(len)),
));
} on Object catch (_) {}
});
map[option] = callable;
ghostty_terminal_set(
Pointer.fromAddress(handle),
option,
callable.nativeFunction.cast(),
);
}

@override
void terminalSetOnTitleChanged(int handle, VoidCallback? callback) {
final map = _callables.putIfAbsent(handle, () => {});
Expand Down
6 changes: 6 additions & 0 deletions packages/libghostty/lib/src/bindings/types/aliases.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ typedef ValueGetter<T> = T Function();
typedef ValueSetter<T> = void Function(T value);
typedef VoidCallback = void Function();

/// An OSC 52 clipboard write request.
///
/// [selector] is the raw one-byte clipboard selector and [payload] is the raw
/// base64 data. Clipboard read queries are never emitted.
typedef ClipboardWrite = ({int selector, Uint8List payload});

/// An untracked grid reference value.
///
/// The value follows libghostty's untracked grid-reference lifetime rules and
Expand Down
32 changes: 32 additions & 0 deletions packages/libghostty/lib/src/bindings/wasm/wasm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1539,6 +1539,38 @@ class WasmBindings implements GhosttyBindings {
_exports.ghostty_terminal_set(handle, option.value, index);
}

@override
void terminalSetOnClipboardWrite(
int handle,
ValueSetter<ClipboardWrite>? callback,
) {
final map = _callbacks.putIfAbsent(handle, () => {});
const option = TerminalOption.clipboardWrite;

if (callback == null) {
final existing = map.remove(option);
if (existing != null) _table.set(existing.$1);
_exports.ghostty_terminal_set(handle, option.value, 0);
return;
}

final reuseIndex = map[option]?.$1;
final index = _registerCallback(
((int terminal, int userdata, int selector, int dataPtr, int len) {
try {
callback((
selector: selector,
payload: Uint8List.fromList(_mem.readBytes(dataPtr, len)),
));
} on Object catch (_) {}
}).toJS,
['i32', 'i32', 'i32', 'i32', 'i32'],
reuseIndex: reuseIndex,
);
map[option] = (index, callback);
_exports.ghostty_terminal_set(handle, option.value, index);
}

@override
void terminalSetOnTitleChanged(int handle, VoidCallback? callback) {
final map = _callbacks.putIfAbsent(handle, () => {});
Expand Down
27 changes: 27 additions & 0 deletions packages/libghostty/lib/src/ffi/libghostty.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6102,6 +6102,33 @@ typedef TerminalBellFn =
>
>;

/// Callback function type for OSC 52 clipboard writes.
///
/// The payload is the raw base64 data from the OSC sequence. An empty payload
/// requests clearing the selected clipboard. Clipboard read queries are
/// ignored and do not invoke this callback. The payload pointer is valid only
/// for the duration of the callback.
///
/// @param terminal The terminal handle
/// @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
/// @param kind OSC 52 clipboard selector (for example 'c' or 'p')
/// @param data Pointer to the raw base64 payload
/// @param len Length of the payload in bytes
///
/// @ingroup terminal
typedef TerminalClipboardWriteFn =
ffi.Pointer<
ffi.NativeFunction<
ffi.Void Function(
Terminal terminal,
ffi.Pointer<ffi.Void> userdata,
ffi.Uint8 kind,
ffi.Pointer<ffi.Uint8> data,
ffi.Size len,
)
>
>;

/// Callback function type for color scheme queries (CSI ? 996 n).
///
/// Called when the terminal receives a color scheme device status report
Expand Down
9 changes: 8 additions & 1 deletion packages/libghostty/lib/src/ffi/libghostty_enums.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2891,7 +2891,13 @@ enum TerminalOption {
/// to ignore pwd change events.
///
/// Input type: TerminalPwdChangedFn
pwdChanged(25);
pwdChanged(25),

/// Callback invoked for OSC 52 clipboard writes. Clipboard read queries are
/// ignored. Set to NULL to ignore clipboard writes.
///
/// Input type: TerminalClipboardWriteFn
clipboardWrite(26);

final int value;
const TerminalOption(this.value);
Expand Down Expand Up @@ -2923,6 +2929,7 @@ enum TerminalOption {
23 => defaultCursorBlink,
24 => glyphProtocol,
25 => pwdChanged,
26 => clipboardWrite,
_ => throw ArgumentError('Unknown value for TerminalOption: $value'),
};
}
Expand Down
Loading