Skip to content
Draft
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
42 changes: 42 additions & 0 deletions .github/scripts/check_ioctl_regression.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Prove the old ioctl signature fails on macOS ARM64 in an isolated copy."""
import pathlib
import platform
import re
import shutil
import subprocess
import tempfile

if platform.system() != "Darwin" or platform.machine() != "arm64":
raise SystemExit("This control must run natively on macOS ARM64")

root = pathlib.Path(__file__).resolve().parents[2]
with tempfile.TemporaryDirectory(prefix="dart-console-old-ioctl-") as temporary:
target = pathlib.Path(temporary)
for name in ("lib", "test"):
shutil.copytree(root / name, target / name)
for name in ("pubspec.yaml", "pubspec.lock"):
if (root / name).exists():
shutil.copy2(root / name, target / name)
binding = target / "lib/src/ffi/unix/termios.dart"
source = binding.read_text()
original = "VarArgs<(Pointer<WinSize>,)> winsize"
if source.count(original) != 1:
raise SystemExit("Expected exactly one reviewed variadic ioctl binding")
binding.write_text(source.replace(original, "Pointer<WinSize> winsize"))
subprocess.run(["dart", "pub", "get", "--offline"], cwd=target,
check=True, timeout=120)
for mode in ("JIT", "AOT"):
result = subprocess.run(
["dart", "test", "--concurrency=1", "--reporter=expanded",
"test/window_size_test.dart", "--plain-name", f"{mode} 80x24 both"],
cwd=target, capture_output=True, text=True, timeout=180,
)
output = result.stdout + result.stderr
print(output, flush=True)
# Accept an incorrect native result or a signal-killed helper, not an
# unrelated build/setup failure or a timeout, as regression evidence.
reproduced = ("Package ioctl returned incorrect dimensions" in output
or re.search(r"Actual: <-[0-9]+>", output))
if result.returncode == 0 or not reproduced:
raise SystemExit(f"Old {mode} signature did not reproduce the regression")
print(f"Confirmed: old {mode} ioctl signature fails on macOS ARM64", flush=True)
41 changes: 33 additions & 8 deletions .github/workflows/dart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,70 @@ on:
pull_request:
branches: [ main ]

permissions:
contents: read

jobs:
build:
runs-on: ${{matrix.os}}
name: ${{ matrix.os }} / ${{ matrix.arch }} / Dart ${{ matrix.sdk }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest]
sdk: [stable]
include:
- {os: ubuntu-24.04, arch: x64, sdk: stable}
- {os: ubuntu-24.04-arm, arch: arm64, sdk: stable}
- {os: macos-15-intel, arch: x64, sdk: stable}
- {os: macos-15, arch: arm64, sdk: stable}
- {os: windows-2025, arch: x64, sdk: stable}
- {os: windows-11-arm, arch: arm64, sdk: stable}
- {os: ubuntu-24.04, arch: x64, sdk: '3.10.0'}
# Linux ARM32 and RISC-V lack standard GitHub-hosted runners.
# See doc/ci-architectures.md for the unresolved execution requirements.

steps:
- name: Fetch sources
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Set up Dart
uses: dart-lang/setup-dart@v1
with:
sdk: ${{matrix.sdk}}
architecture: ${{ matrix.arch }}

- name: Print Dart SDK version
run: dart --version

- name: Install dependencies
run: dart pub get

# Exercise each ABI even if a style check subsequently fails.
- name: Run tests
run: dart test --concurrency=2

- name: Confirm original ioctl regression on macOS ARM64
if: matrix.os == 'macos-15' && matrix.arch == 'arm64'
run: python3 .github/scripts/check_ioctl_regression.py

- name: Verify formatting
run: dart format --output=none --set-exit-if-changed .

- name: Analyze project source
run: dart analyze --fatal-warnings

- name: Run tests
run: dart test

- name: Add code coverage package
if: matrix.os == 'ubuntu-24.04' && matrix.sdk == 'stable'
run: dart pub global activate coverage

- name: Generate code coverage
if: matrix.os == 'ubuntu-24.04' && matrix.sdk == 'stable'
run: dart pub global run coverage:test_with_coverage

- name: Upload code coverage
if: github.event_name == 'push' && matrix.os == 'ubuntu-24.04' && matrix.sdk == 'stable'
uses: codecov/codecov-action@v3
with:
files: coverage/lcov.info
files: coverage/lcov.info
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Unreleased
- Fix the variadic ioctl binding on macOS ARM64 (mafreud, #15).
- Use platform-specific termios layouts and libc raw-mode flags on Linux and
macOS; preserve terminal settings and free temporary native allocations.
- Add native ABI and PTY regression tests and explicit x64/ARM64 CI jobs.

# 5.1.0
- Enhance table rendering and string handling for East Asian characters (#12)
- Fix #9: query terminal window size via platform APIs (#13)
Expand Down
48 changes: 48 additions & 0 deletions doc/ci-architectures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# CI architecture coverage

The package declares Linux, macOS, and Windows, without a documented CPU
support matrix. Its README records tests on those operating systems; the
changelog mentions an ARM64 compatibility fix. Neither explicitly promises
ARM32 or RISC-V support. This workflow targets six native x64/ARM64 combinations.

Dart additionally supports Linux ARM32 and RISC-V. Those are potential package
expansion targets, not established dart_console support promises. Current Dart
does not support IA32. Linux here means glibc: the implementation loads libc.so.6.

The workflow explicitly selects six native hosted OS/CPU pairs and verifies
stable Dart on each, plus the declared minimum Dart 3.10.0 on Linux x64.
Unix tests execute the PTY helper in both JIT and AOT modes; Windows runs the
remaining suite and skips the Unix-specific PTY tests. Native Unix regressions
compile `test/termios_native.c` with the system `cc` against the runner's actual
headers. Local contributors need a C compiler (Linux build tools or macOS
Command Line Tools) to run these tests; consumers do not need one.

The macOS ARM64 job also runs a negative control in a temporary source copy:
it restores the old nonvariadic ioctl signature and requires the targeted JIT
and AOT tests to fail with an incorrect native result or a signal-killed helper.
The checked-out production source remains unchanged.

## Potential expansion targets

Linux ARM32 and Linux RISC-V have no standard GitHub-hosted runner labels.
They are not silently treated as covered by ubuntu-24.04. Completing coverage
requires either a reviewed QEMU userspace/container setup on a disposable
hosted runner, with matching glibc and Dart SDK, or isolated ephemeral native
runners. Do not run fork PR code on persistent self-hosted machines containing
credentials. RISC-V SDK installation also requires verification beyond the
setup-dart action's documented architecture list.

The prior code used the macOS termios layout on Linux. Separate glibc and Darwin
structs now correct that mismatch, and raw-mode flag masks come from libc's
cfmakeraw. Native C regression probes verify the host headers against Dart
fields and PTY mode restoration on the tested Unix targets. ARM32 and RISC-V
still need actual execution before claiming package support.

Workflow configuration is not evidence of a completed platform test run.
Record actual runs and results before claiming a target passed.

Sources:
- https://dart.dev/get-dart#system-requirements
- https://docs.github.com/en/actions/reference/runners/github-hosted-runners
- https://github.com/dart-lang/setup-dart/blob/main/action.yml
- https://github.com/bminor/glibc/blob/master/sysdeps/unix/sysv/linux/bits/termios-struct.h
178 changes: 15 additions & 163 deletions example/ffi/ffi_rawmode.dart
Original file line number Diff line number Diff line change
@@ -1,168 +1,20 @@
// Ignore these lints, since these are UNIX identifiers that we're replicating
//
// ignore_for_file: non_constant_identifier_names, constant_identifier_names

import 'dart:ffi';
// Use the package's platform-specific native bindings instead of duplicating
// the incompatible Linux and macOS termios layouts in this example.
import 'dart:io';

import 'package:ffi/ffi.dart';

// int tcgetattr(int, struct termios *);
typedef TCGetAttrNative = Int32 Function(
Int32 fildes, Pointer<TermIOS> termios);
typedef TCGetAttrDart = int Function(int fildes, Pointer<TermIOS> termios);

// int tcsetattr(int, int, const struct termios *);
typedef TCSetAttrNative = Int32 Function(
Int32 fildes,
Int32 optional_actions,
Pointer<TermIOS> termios,
);
typedef TCSetAttrDart = int Function(
int fildes, int optional_actions, Pointer<TermIOS> termios);

const STDIN_FILENO = 0;
const STDOUT_FILENO = 1;
const STDERR_FILENO = 2;

// INPUT FLAGS
const int IGNBRK = 0x00000001; // ignore BREAK condition
const int BRKINT = 0x00000002; // map BREAK to SIGINTR
const int IGNPAR = 0x00000004; // ignore (discard) parity errors
const int PARMRK = 0x00000008; // mark parity and framing errors
const int INPCK = 0x00000010; // enable checking of parity errors
const int ISTRIP = 0x00000020; // strip 8th bit off chars
const int INLCR = 0x00000040; // map NL into CR
const int IGNCR = 0x00000080; // ignore CR
const int ICRNL = 0x00000100; // map CR to NL (ala CRMOD)
const int IXON = 0x00000200; // enable output flow control
const int IXOFF = 0x00000400; // enable input flow control
const int IXANY = 0x00000800; // any char will restart after stop
const int IMAXBEL = 0x00002000; // ring bell on input queue full
const int IUTF8 = 0x00004000; // maintain state for UTF-8 VERASE

// OUTPUT FLAGS
const int OPOST = 0x00000001; // enable following output processing
const int ONLCR = 0x00000002; // map NL to CR-NL (ala CRMOD)
const int OXTABS = 0x00000004; // expand tabs to spaces
const int ONOEOT = 0x00000008; // discard EOT's (^D) on output)

// CONTROL FLAGS
const int CIGNORE = 0x00000001; // ignore control flags
const int CSIZE = 0x00000300; // character size mask
const int CS5 = 0x00000000; // 5 bits (pseudo)
const int CS6 = 0x00000100; // 6 bits
const int CS7 = 0x00000200; // 7 bits
const int CS8 = 0x00000300; // 8 bits

// LOCAL FLAGS
const int ECHOKE = 0x00000001; // visual erase for line kill
const int ECHOE = 0x00000002; // visually erase chars
const int ECHOK = 0x00000004; // echo NL after line kill
const int ECHO = 0x00000008; // enable echoing
const int ECHONL = 0x00000010; // echo NL even if ECHO is off
const int ECHOPRT = 0x00000020; // visual erase mode for hardcopy
const int ECHOCTL = 0x00000040; // echo control chars as ^(Char)
const int ISIG = 0x00000080; // enable signals INTR, QUIT, [D]SUSP
const int ICANON = 0x00000100; // canonicalize input lines
const int ALTWERASE = 0x00000200; // use alternate WERASE algorithm
const int IEXTEN = 0x00000400; // enable DISCARD and LNEXT
const int EXTPROC = 0x00000800; // external processing
const int TOSTOP = 0x00400000; // stop background jobs from output
const int FLUSHO = 0x00800000; // output being flushed (state)
const int NOKERNINFO = 0x02000000; // no kernel output from VSTATUS
const int PENDIN = 0x20000000; // retype pending input (state)
const int NOFLSH = 0x80000000; // don't flush after interrupt

const int TCSANOW = 0; // make change immediate
const int TCSADRAIN = 1; // drain output, then change
const int TCSAFLUSH = 2; // drain output, flush input

const int VMIN = 16; // minimum number of characters to receive
const int VTIME = 17; // time in 1/10s before returning

// typedef unsigned long tcflag_t;
// typedef unsigned char cc_t;
// typedef unsigned long speed_t;
// #define NCCS 20

// struct termios {
// tcflag_t c_iflag; /* input flags */
// tcflag_t c_oflag; /* output flags */
// tcflag_t c_cflag; /* control flags */
// tcflag_t c_lflag; /* local flags */
// cc_t c_cc[NCCS]; /* control chars */
// speed_t c_ispeed; /* input speed */
// speed_t c_ospeed; /* output speed */
// };
base class TermIOS extends Struct {
@IntPtr()
external int c_iflag;
@IntPtr()
external int c_oflag;
@IntPtr()
external int c_cflag;
@IntPtr()
external int c_lflag;

@Array(20)
external Array<Uint8> c_cc;

@IntPtr()
external int c_ispeed;
@IntPtr()
external int c_ospeed;
}
import 'package:dart_console/src/ffi/unix/termlib_unix.dart';

void main() {
final libc = Platform.isMacOS
? DynamicLibrary.open('/usr/lib/libSystem.dylib')
: DynamicLibrary.open('libc-2.28.so');

final tcgetattr = libc.lookupFunction<TCGetAttrNative, TCGetAttrDart>(
'tcgetattr',
);
final tcsetattr = libc.lookupFunction<TCSetAttrNative, TCSetAttrDart>(
'tcsetattr',
);

final origTermIOS = calloc<TermIOS>();
var result = tcgetattr(STDIN_FILENO, origTermIOS);
print('result is $result');

print('origTermIOS.c_iflag: 0b${origTermIOS.ref.c_iflag.toRadixString(2)}');
print('Copying and modifying...');

final newTermIOS = calloc<TermIOS>()
..ref.c_iflag =
origTermIOS.ref.c_iflag & ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON)
..ref.c_oflag = origTermIOS.ref.c_oflag & ~OPOST
..ref.c_cflag = origTermIOS.ref.c_cflag | CS8
..ref.c_lflag = origTermIOS.ref.c_lflag & ~(ECHO | ICANON | IEXTEN | ISIG)
..ref.c_ispeed = origTermIOS.ref.c_ispeed
..ref.c_oflag = origTermIOS.ref.c_ospeed
..ref.c_cc = origTermIOS.ref.c_cc
..ref.c_cc[VMIN] = 0 // VMIN -- return each byte, or 0 for timeout
..ref.c_cc[VTIME] = 1; // VTIME -- 100ms timeout (unit is 1/10s)

print('origTermIOS.c_iflag: 0b${origTermIOS.ref.c_iflag.toRadixString(2)}');
print('newTermIOS.c_iflag: 0b${newTermIOS.ref.c_iflag.toRadixString(2)}');
print('origTermIOS.c_oflag: 0b${origTermIOS.ref.c_oflag.toRadixString(2)}');
print('newTermIOS.c_oflag: 0b${newTermIOS.ref.c_oflag.toRadixString(2)}');
print('origTermIOS.c_cflag: 0b${origTermIOS.ref.c_cflag.toRadixString(2)}');
print('newTermIOS.c_cflag: 0b${newTermIOS.ref.c_cflag.toRadixString(2)}');
print('origTermIOS.c_lflag: 0b${origTermIOS.ref.c_lflag.toRadixString(2)}');
print('newTermIOS.c_lflag: 0b${newTermIOS.ref.c_lflag.toRadixString(2)}');

result = tcsetattr(STDIN_FILENO, TCSAFLUSH, newTermIOS);
print('result is $result\n');

print('RAW MODE: Here is some text.\nHere is some more text.');
result = tcsetattr(STDIN_FILENO, TCSAFLUSH, origTermIOS);
print('result is $result\n');

print('\nORIGINAL MODE: Here is some text.\nHere is some more text.');

calloc.free(origTermIOS);
calloc.free(newTermIOS);
if (!Platform.isLinux && !Platform.isMacOS || !stdin.hasTerminal) {
stderr.writeln('Run this example in a Linux or macOS terminal.');
return;
}
final terminal = TermLibUnix();
terminal.enableRawMode();
try {
stdout.write('RAW MODE: Here is some text.\nHere is some more text.\r\n');
} finally {
terminal.disableRawMode();
}
stdout.writeln('ORIGINAL MODE: Here is some text.');
}
6 changes: 4 additions & 2 deletions example/kilo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ void editorInsertChar(String char) {
fileRows.add(char);
renderRows.add(char);
} else {
fileRows[cursorRow] = fileRows[cursorRow].substring(0, cursorCol) +
fileRows[cursorRow] =
fileRows[cursorRow].substring(0, cursorCol) +
char +
fileRows[cursorRow].substring(cursorCol);
}
Expand All @@ -92,7 +93,8 @@ void editorBackspaceChar() {
if (cursorCol == 0 && cursorRow == 0) return;

if (cursorCol > 0) {
fileRows[cursorRow] = fileRows[cursorRow].substring(0, cursorCol - 1) +
fileRows[cursorRow] =
fileRows[cursorRow].substring(0, cursorCol - 1) +
fileRows[cursorRow].substring(cursorCol);
editorUpdateRenderRow(cursorRow);
cursorCol--;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/calendar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class Calendar extends Table {
List<String> dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

Calendar(DateTime dateTime)
: calendarDate = dateTime.subtract(Duration(days: dateTime.day - 1)) {
: calendarDate = dateTime.subtract(Duration(days: dateTime.day - 1)) {
for (final day in dayLabels) {
insertColumn(header: day, alignment: TextAlignment.right);
}
Expand Down
Loading
Loading