Skip to content
Merged
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
43 changes: 16 additions & 27 deletions .github/workflows/build-terminal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,11 @@ jobs:
path: |
artifacts/tools
artifacts/ghostty-src
native/ghostty
native/linux-pty
native/ghostty/*/*
native/linux-pty/*/dt-pty-host
native/noto-emoji/NotoColorEmoji.ttf
key: native-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
restore-keys: |
native-${{ runner.os }}-${{ runner.arch }}-
# Never restore native sources or reuse binaries built from different inputs.
key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}

- name: Set up .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
Expand Down Expand Up @@ -74,13 +73,10 @@ jobs:
path: |
artifacts/tools
artifacts/ghostty-src
native/ghostty
native/linux-pty
native/ghostty/*/*
native/linux-pty/*/dt-pty-host
native/noto-emoji/NotoColorEmoji.ttf
native/noto-emoji/NotoColorEmoji.ttf
key: native-${{ runner.os }}-${{ runner.arch }}-${{ matrix.rid }}-${{ hashFiles('native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
restore-keys: |
native-${{ runner.os }}-${{ runner.arch }}-
key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ matrix.rid }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}

- name: Set up .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
Expand Down Expand Up @@ -115,12 +111,10 @@ jobs:
path: |
artifacts/tools
artifacts/ghostty-src
native/ghostty
native/linux-pty
native/ghostty/*/*
native/linux-pty/*/dt-pty-host
native/noto-emoji/NotoColorEmoji.ttf
key: native-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
restore-keys: |
native-${{ runner.os }}-${{ runner.arch }}-
key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}

- name: Set up .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
Expand Down Expand Up @@ -149,12 +143,10 @@ jobs:
path: |
artifacts/tools
artifacts/ghostty-src
native/ghostty
native/linux-pty
native/ghostty/*/*
native/linux-pty/*/dt-pty-host
native/noto-emoji/NotoColorEmoji.ttf
key: native-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
restore-keys: |
native-${{ runner.os }}-${{ runner.arch }}-
key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}

- name: Set up .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
Expand Down Expand Up @@ -184,12 +176,10 @@ jobs:
path: |
artifacts/tools
artifacts/ghostty-src
native/ghostty
native/linux-pty
native/ghostty/*/*
native/linux-pty/*/dt-pty-host
native/noto-emoji/NotoColorEmoji.ttf
key: native-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}
restore-keys: |
native-${{ runner.os }}-${{ runner.arch }}-
key: native-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/**/*.ps1', 'native/ghostty/ghostty-upstream.json', 'native/linux-pty/dt-pty-host.c', 'native/noto-emoji/noto-emoji.json') }}

- name: Set up .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
Expand Down Expand Up @@ -603,4 +593,3 @@ jobs:
gh release upload "v${RELEASE_VERSION}" artifacts/release/* --clobber



34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,40 @@ Build and release gates are documented in [docs/release.md](docs/release.md).

## Compatibility inventory

### Safety and compatibility settings

Large or multi-line pastes requiring confirmation are cancelled unless explicitly
approved. The application prompts asynchronously; closing the prompt cancels the
paste. Embedded controls without a confirmation handler also cancel warned pastes.

`warning.confirmOnClose` applies to user-initiated window, tab, pane, and bulk
close actions. `never` skips confirmation; `always` confirms closing any running
session; `automatic` confirms when an action closes more than one running session
(the legacy `confirmCloseAllTabs` behavior). Already-exited sessions and automatic
process-exit cleanup never prompt.

PTY input is queued in order off the UI thread, with limits of 256 pending writes
and 4 MiB (including framing on Unix). Overflow rejects the entire new write and
reports an error rather than blocking or silently dropping input. Async writes
complete after transport delivery; caller cancellation skips writes not yet
started. Cancelling an in-flight write terminates its session because input may
have been partially delivered and Unix framing cannot safely resume. Closing a
blocked Unix session has a one-second grace period before host termination;
undelivered input is reported.

The editor disables options that are currently retained only for settings-file
compatibility: `compatibility.textMeasurement`, `compatibility.ambiguousWidth`,
`experimental.detectURLs`, and `disableAnimations`. The terminal engine determines
text measurement and character widths. Plain-text URL detection is not implemented;
explicit OSC 8 hyperlinks remain supported. Window/pane animation effects are not
configurable through `disableAnimations`.

Broker retries share active requests and retain completed responses for at least
five seconds after completion. Admission is bounded at 128 active requests and
1024 total retained requests. When full, new requests receive an explicit
unavailable response without executing their action; existing retries still join
their original operation.

The port tracks Windows Terminal settings, actions, VT dispatch, command line,
and settings-page surfaces in
[`compat/windows-terminal.json`](compat/windows-terminal.json). Tests use that
Expand Down
177 changes: 102 additions & 75 deletions native/linux-pty/dt-pty-host.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#endif

#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#if defined(__APPLE__)
#include <util.h>
Expand All @@ -20,60 +21,13 @@
#include <sys/wait.h>
#include <unistd.h>

static bool write_all(int fd, const void *buffer, size_t length) {
const uint8_t *bytes = buffer;
while (length > 0) {
ssize_t written = write(fd, bytes, length);
if (written < 0) {
if (errno == EINTR) continue;
return false;
}

bytes += written;
length -= (size_t)written;
}

return true;
static bool nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL);
return flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
}

static bool read_all(int fd, void *buffer, size_t length) {
uint8_t *bytes = buffer;
while (length > 0) {
ssize_t count = read(fd, bytes, length);
if (count == 0) return false;
if (count < 0) {
if (errno == EINTR) continue;
return false;
}

bytes += count;
length -= (size_t)count;
}

return true;
}

static bool read_header(char *buffer, size_t capacity) {
size_t length = 0;
while (length + 1 < capacity) {
char value;
ssize_t count = read(STDIN_FILENO, &value, 1);
if (count == 0) return false;
if (count < 0) {
if (errno == EINTR) continue;
return false;
}

if (value == '\n') {
buffer[length] = '\0';
return true;
}

buffer[length++] = value;
}

errno = EMSGSIZE;
return false;
static bool retryable(void) {
return errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK;
}

static void resize_pty(int master, pid_t child, unsigned columns, unsigned rows) {
Expand Down Expand Up @@ -122,54 +76,125 @@ int main(int argc, char **argv) {
_exit(127);
}

signal(SIGPIPE, SIG_IGN);
if (!nonblocking(master) || !nonblocking(STDIN_FILENO) || !nonblocking(STDOUT_FILENO)) {
perror("nonblocking relay");
kill(-child, SIGKILL);
close(master);
waitpid(child, NULL, 0);
return 74;
}

bool input_open = true;
bool master_open = true;
uint8_t buffer[16384];
while (master_open) {
struct pollfd descriptors[2] = {
{ .fd = master, .events = POLLIN },
{ .fd = input_open ? STDIN_FILENO : -1, .events = POLLIN },
bool master_hungup = false;
uint8_t input[16384], output[16384];
size_t input_length = 0, input_offset = 0;
size_t output_length = 0, output_offset = 0;
size_t remaining = 0, header_length = 0;
char header[96];
bool failed = false;
while (master_open || output_length > 0) {
struct pollfd descriptors[3] = {
{ .fd = master_open && (output_length == 0 || (input_length > 0 && !master_hungup)) ? master : -1,
.events = (output_length == 0 ? POLLIN : 0) | (input_length > 0 ? POLLOUT : 0) },
{ .fd = input_open && input_length == 0 ? STDIN_FILENO : -1, .events = POLLIN },
{ .fd = output_length > 0 ? STDOUT_FILENO : -1, .events = POLLOUT },
};
int result = poll(descriptors, 2, -1);
int result = poll(descriptors, 3, -1);
if (result < 0) {
if (errno == EINTR) continue;
perror("poll");
failed = true;
break;
}

if (descriptors[0].revents & (POLLIN | POLLHUP)) {
ssize_t count = read(master, buffer, sizeof(buffer));
if (descriptors[0].revents & (POLLHUP | POLLERR)) master_hungup = true;

if (output_length == 0 && descriptors[0].revents & (POLLIN | POLLHUP | POLLERR)) {
ssize_t count = read(master, output, sizeof(output));
if (count > 0) {
if (!write_all(STDOUT_FILENO, buffer, (size_t)count)) break;
output_length = (size_t)count;
output_offset = 0;
} else if (count == 0 || errno == EIO) {
master_open = false;
} else if (errno != EINTR) {
} else if (!retryable()) {
perror("read pty");
failed = true;
break;
}
}

if (descriptors[2].revents & (POLLOUT | POLLERR | POLLHUP)) {
ssize_t count = write(STDOUT_FILENO, output + output_offset, output_length);
if (count > 0) {
output_offset += (size_t)count;
output_length -= (size_t)count;
} else if (count == 0 || !retryable()) {
perror("write stdout");
failed = true;
break;
}
}

if (master_open && input_length > 0 && descriptors[0].revents & POLLOUT) {
ssize_t count = write(master, input + input_offset, input_length);
if (count > 0) {
input_offset += (size_t)count;
input_length -= (size_t)count;
} else if (count == 0 || !retryable()) {
perror("write pty");
failed = true;
break;
}
}

if (input_open && descriptors[1].revents & (POLLIN | POLLHUP)) {
char header[96];
if (!read_header(header, sizeof(header))) {
size_t capacity = remaining > 0
? (remaining < sizeof(input) ? remaining : sizeof(input)) : 1;
ssize_t count = read(STDIN_FILENO, input, capacity);
if (count == 0) {
input_open = false;
kill(-child, SIGHUP);
continue;
}

if (count < 0) {
if (retryable()) continue;
perror("read stdin");
failed = true;
break;
}

if (remaining > 0) {
input_length = (size_t)count;
input_offset = 0;
remaining -= (size_t)count;
continue;
}

if (input[0] != '\n') {
if (header_length + 1 >= sizeof(header)) {
fprintf(stderr, "input header too long\n");
failed = true;
break;
}
header[header_length++] = (char)input[0];
continue;
}

header[header_length] = '\0';
header_length = 0;
if (header[0] == 'D' && header[1] == ' ') {
size_t length = (size_t)strtoull(header + 2, NULL, 10);
while (length > 0) {
size_t chunk = length < sizeof(buffer) ? length : sizeof(buffer);
if (!read_all(STDIN_FILENO, buffer, chunk) ||
!write_all(master, buffer, chunk)) {
input_open = false;
kill(-child, SIGHUP);
break;
}

length -= chunk;
char *end;
errno = 0;
unsigned long length = strtoul(header + 2, &end, 10);
if (errno != 0 || end == header + 2 || *end != '\0' || length > 4 * 1024 * 1024) {
fprintf(stderr, "invalid input frame length\n");
failed = true;
break;
}
remaining = (size_t)length;
} else if (header[0] == 'R' && header[1] == ' ') {
unsigned new_columns = 0;
unsigned new_rows = 0;
Expand All @@ -185,11 +210,13 @@ int main(int argc, char **argv) {
}
}

if (failed) kill(-child, SIGKILL);
close(master);
int status = 0;
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {
}

if (failed) return 74;
if (WIFEXITED(status)) return WEXITSTATUS(status);
if (WIFSIGNALED(status)) return 128 + WTERMSIG(status);
return 1;
Expand Down
14 changes: 14 additions & 0 deletions src/Devolutions.Terminal.App/Actions/CloseConfirmationPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Devolutions.Terminal.Settings;

namespace Devolutions.Terminal.App.Actions;

public static class CloseConfirmationPolicy
{
public static bool RequiresConfirmation(ConfirmOnClose policy, int runningSessions, bool automaticExit = false) =>
!automaticExit && policy switch
{
ConfirmOnClose.Always => runningSessions > 0,
ConfirmOnClose.Automatic => runningSessions > 1,
_ => false,
};
}
1 change: 1 addition & 0 deletions src/Devolutions.Terminal.App/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Devolutions.Terminal.App.Tests")]
[assembly: InternalsVisibleTo("Devolutions.Terminal.UI.Tests")]
Loading
Loading