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
118 changes: 115 additions & 3 deletions .github/workflows/differential.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Differential gates for the SDL2-based libx11-compat stack.
#
# Each downstream target (Motif, ViolaWWW, Mosaic, Osiris, Xfig, GIMP) is
# built twice on the same GitHub-hosted runner: once against the
# Each downstream target (Motif, ViolaWWW, Mosaic, Osiris, Xfig, GIMP,
# XNEdit) is built twice on the same GitHub-hosted runner: once against the
# system libX11 stack from apt and once against libx11-compat. The
# resulting screenshots are compared under Xvfb. This is the local
# counterpart to the SSH-driven path in scripts/run-*-differential-tests.py
Expand All @@ -10,7 +10,7 @@
# Differential gates live in a separate workflow from ci.yml so the
# fast PR feedback loop (lint / build / smoke) is not delayed by the
# heavier double-build pipeline, and so each downstream target reports
# its own status check independent of the others. The six jobs run
# its own status check independent of the others. The seven jobs run
# in parallel; total wallclock is bounded by the slowest single job
# rather than the sum of all jobs.
#
Expand Down Expand Up @@ -629,3 +629,115 @@ jobs:
- name: ccache stats
if: ${{ !cancelled() }}
run: ccache --show-stats

# ---- XNEdit (Motif NEdit fork, Xft/fontconfig text) differential ----
#
# XNEdit renders editor text through Xft/fontconfig rather than core X
# fonts, so this is the differential gate for the libXft-compat path. It
# is built twice on the runner: once against system libX11 + OpenMotif
# (libmotif-dev) + libXft, and once against libx11-compat + the bundled
# thentenaar/motif + the SDL_ttf-backed libXft-compat. Both render the
# editor under Xvfb and the startup + opened-fixture screens are compared.
# Validated on node11 (OpenMotif 2.3.8 + system Xft); the GitHub-runner
# path is the local counterpart and shares the script with the SSH route.
# The compat side builds the bundled Motif, so this job carries the
# MOTIF_BUILD_PKGS autoreconf chain and the MOTIF_YACC override.
xnedit-differential:
runs-on: ubuntu-24.04
timeout-minutes: 20
env:
MOTIF_YACC: "bison -y"
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Install build + system X11 dependencies
# libmotif-dev supplies the system OpenMotif baseline; libxft-dev /
# libfontconfig1-dev / libxrender-dev back the system-side Xft text
# path. fonts-dejavu-core gives fontconfig a scalable family so the
# system side renders real glyphs rather than tofu under Xvfb.
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
${{ env.COMMON_BUILD_PKGS }} ${{ env.SYSTEM_X11_DEV_PKGS }} \
${{ env.DIFFERENTIAL_PKGS }} ${{ env.MOTIF_BUILD_PKGS }} \
libmotif-dev libfontconfig1-dev libxft-dev libxrender-dev \
fonts-dejavu-core

- name: Cache upstream tarballs and extracted source/headers
# Exclude the XNEdit clone so its dedicated cache below owns it,
# matching how the other big source trees are split out.
uses: actions/cache@v5
with:
path: |
build/upstream
!build/upstream/**/*.o
!build/upstream/**/*.d
!build/upstream/motif
!build/upstream/mosaic
!build/upstream/osiris
!build/upstream/xfig-*
!build/upstream/.cache/xfig-*
!build/upstream/gimp-*
!build/upstream/.cache/gimp-*
!build/upstream/xnedit
key: upstream-src-v2-${{ runner.os }}-${{ hashFiles('scripts/sync-upstream-headers.py') }}
restore-keys: |
upstream-src-v2-${{ runner.os }}-

- name: Cache Motif source clone and autoreconf output
# The compat side reuses the in-tree thentenaar/motif as libXm /
# libMrm, so share the motif job's clone+autoreconf cache key.
uses: actions/cache@v5
with:
path: build/upstream/motif
key: motif-src-${{ runner.os }}-${{ hashFiles('mk/motif.mk', 'compat/motif-patches/**') }}

- name: Cache XNEdit source clone
uses: actions/cache@v5
with:
path: build/upstream/xnedit
key: xnedit-src-${{ runner.os }}-${{ hashFiles('mk/xnedit.mk', 'compat/xnedit-patches/**') }}

- name: Cache ccache
# XNEdit builds the bundled Motif on the compat side, so it shares
# the ccache-motifstack-diff-v3-* restore-keys prefix with motif /
# violawww / mosaic to prime the Motif gcc object cache; the save
# key stays distinct so this job persists its own snapshot.
uses: actions/cache@v5
with:
path: ~/.cache/ccache
key: ccache-motifstack-diff-v3-${{ runner.os }}-${{ github.sha }}-xnedit
restore-keys: |
ccache-motifstack-diff-v3-${{ runner.os }}-

- name: Configure ccache
run: |
ccache --max-size=2048M
ccache --zero-stats

- name: Run XNEdit differential
env:
XNEDIT_DIFF_LOCAL: "1"
XNEDIT_DIFF_COMPARE_LOCATION: local
XNEDIT_DIFF_JOBS: "2"
run: make check-differential-xnedit

- name: Upload XNEdit differential artifacts on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: xnedit-differential
path: |
build/xnedit-differential/report.tsv
build/xnedit-differential/junit.xml
build/xnedit-differential/logs
build/xnedit-differential/diff
build/xnedit-differential/system
build/xnedit-differential/compat
if-no-files-found: warn
retention-days: 7

- name: ccache stats
if: ${{ !cancelled() }}
run: ccache --show-stats
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ include mk/osiris.mk
include mk/xclock.mk
include mk/xfig.mk
include mk/gimp-motif.mk
include mk/xnedit.mk
include mk/tests.mk
include mk/examples.mk
include mk/upstream-headers.mk
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,19 @@ but each exercises behavior that small examples do not reach.
make check-differential-gimp-motif # toolbox diff vs system OpenMotif (needs remote host)
```

- [XNEdit](https://github.com/unixwork/xnedit): the Motif NEdit fork that renders editor text through Xft/fontconfig builds against the bundled Motif, `libXt-compat`, and the SDL_ttf-backed `libXft-compat`.
It validates Unicode text rendering in an interactive Motif editor, including Latin-1, Greek, and CJK glyph fallback through the Xft path.
Replay smoke checks cover startup and opening a controlled UTF-8 fixture; screenshot-based differential checks compare the same states against native libX11/OpenMotif/Xft on `node11`.

<a href="assets/xnedit.png"><img src="assets/xnedit.png" alt="XNEdit running through libx11-compat on macOS" width="420"></a>

```sh
make xnedit # build XNEdit (depends on motif + Xft shim)
build/xnedit/source/source/xnedit tests/ui/fixtures/xnedit-fixture.txt
make check-smoke-xnedit # replay-driven startup + Unicode fixture smoke
make check-differential-xnedit # startup + fixture diff vs native X11/Xft (needs remote host)
```

The `check-smoke-*` targets use deterministic replay files and in-process snapshots, with artifacts written under `build/ui-smoke/`.
`make profile-ui` runs the Motif, ViolaWWW, and Mosaic replay smokes with timing capture and prints the generated `metrics.tsv` and `render-stats.tsv` paths; the Osiris and Xfig smokes are still invoked individually via `make check-smoke-osiris` / `make check-smoke-xfig`.
They do not require `node11`, `xdotool`, or a native X11 reference run.
Expand Down
Binary file added assets/xnedit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions compat/libxt-patches/keyboard-focus-xinput.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
diff --git a/Keyboard.c b/Keyboard.c
index 6dcb8d4..73f10f5 100644
--- a/Keyboard.c
+++ b/Keyboard.c
@@ -782,6 +782,9 @@ XtSetKeyboardFocus(Widget widget, Widget descendant)
False, QueryEventMask, (XtPointer) widget);
pwi->map_handler_added = TRUE;
pwi->queryEventDescendant = descendant;
+ } else if (target) {
+ XSetInputFocus(XtDisplay(widget), XtWindow(target),
+ RevertToParent, CurrentTime);
}
}
}
60 changes: 60 additions & 0 deletions compat/xnedit-patches/stdlib-textfield.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
diff --git a/util/textfield.c b/util/textfield.c
index 7ab1422..e42b0e2 100644
--- a/util/textfield.c
+++ b/util/textfield.c
@@ -23,6 +23,7 @@

#include <stdio.h>
#include <ctype.h>
+#include <stdlib.h>
#include <string.h>

#include "../source/textBuf.h" /* Utf8CharLen */
diff --git a/util/colorchooser.c b/util/colorchooser.c
index 73c0926..cd4f2e0 100644
--- a/util/colorchooser.c
+++ b/util/colorchooser.c
@@ -27,6 +27,7 @@

#include <inttypes.h>
#include <limits.h>
+#include <stdlib.h>

#define XNE_COLOR_DIALOG_TITLE "Select Color"

diff --git a/source/file.h b/source/file.h
index aab06a0..ef00b86 100644
--- a/source/file.h
+++ b/source/file.h
@@ -30,6 +30,7 @@
#include "../util/getfiles.h"

#include <X11/Intrinsic.h>
+#include <sys/stat.h>

/* flags for EditExistingFile */
#define CREATE 1
diff --git a/source/shift.c b/source/shift.c
index 17dbdcf..7f381a6 100644
--- a/source/shift.c
+++ b/source/shift.c
@@ -42,6 +42,7 @@

#include <string.h>
#include <limits.h>
+#include <stdlib.h>
#include <ctype.h>
#include <locale.h>
#include <wctype.h>
diff --git a/source/highlightData.c b/source/highlightData.c
index 4d14022..02d1a31 100644
--- a/source/highlightData.c
+++ b/source/highlightData.c
@@ -48,6 +48,7 @@

#include <stdio.h>
#include <string.h>
+#include <stdlib.h>
#include <limits.h>
#ifndef __MVS__
#include <sys/param.h>
5 changes: 5 additions & 0 deletions include/X11/Xft/Xft.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ XftDraw *XftDrawCreate(Display *dpy,
Colormap colormap);
void XftDrawDestroy(XftDraw *draw);
void XftDrawChange(XftDraw *draw, Drawable drawable);
Bool XftDrawSetClipRectangles(XftDraw *draw,
int x_origin,
int y_origin,
const XRectangle *rects,
int n);
Display *XftDrawDisplay(XftDraw *draw);
Drawable XftDrawDrawable(XftDraw *draw);
Colormap XftDrawColormap(XftDraw *draw);
Expand Down
23 changes: 22 additions & 1 deletion include/fontconfig/fontconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ typedef int FcResult;
typedef int FcType;
typedef struct _FcPattern FcPattern;
typedef struct _FcObjectSet FcObjectSet;
typedef struct _FcFontSet FcFontSet;
typedef struct _FcFontSet {
int nfont;
int sfont;
FcPattern **fonts;
} FcFontSet;
typedef struct _FcCharSet FcCharSet;
typedef struct _FcMatrix {
double xx;
Expand Down Expand Up @@ -45,6 +49,8 @@ typedef struct _FcValue {
#define FcResultNoId 3
#define FcResultOutOfMemory 4

#define FcMatchPattern 0

#define FcTypeVoid 0
#define FcTypeInteger 1
#define FcTypeDouble 2
Expand Down Expand Up @@ -121,9 +127,24 @@ void FcPatternReference(FcPattern *pattern);
FcBool FcPatternDel(FcPattern *pattern, const char *object);
FcBool FcPatternRemove(FcPattern *pattern, const char *object, int n);
int FcUcs4ToUtf8(FcChar32 ucs4, FcChar8 dest[FC_UTF8_MAX_LEN]);
int FcUtf8ToUcs4(const FcChar8 *src, FcChar32 *dst, int len);
FcPattern *FcNameParse(const FcChar8 *name);
FcBool FcConfigSubstitute(FcConfig *config, FcPattern *pattern, int kind);
void FcDefaultSubstitute(FcPattern *pattern);
FcPattern *FcFontMatch(FcConfig *config, FcPattern *pattern, FcResult *result);
FcFontSet *FcFontList(FcConfig *config, FcPattern *pattern, FcObjectSet *os);
void FcFontSetDestroy(FcFontSet *set);
FcCharSet *FcCharSetCreate(void);
FcBool FcCharSetAddChar(FcCharSet *charset, FcChar32 ucs4);
FcBool FcCharSetHasChar(const FcCharSet *charset, FcChar32 ucs4);
void FcCharSetDestroy(FcCharSet *charset);
FcObjectSet *FcObjectSetCreate(void);
FcBool FcObjectSetAdd(FcObjectSet *os, const char *object);
void FcObjectSetDestroy(FcObjectSet *os);
FcBool FcPatternAdd(FcPattern *pattern,
const char *object,
FcValue value,
FcBool append);
FcBool FcPatternAddString(FcPattern *pattern,
const char *object,
const FcChar8 *s);
Expand Down
8 changes: 7 additions & 1 deletion mk/config.mk
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ CPPFLAGS += -Iinclude -Isrc \
-iquote $(OUT)/upstream/src \
$(SDL_CPPFLAGS) $(PIXMAN_CFLAGS) \
-DNARROWPROTO -DXTHREADS -D_GNU_SOURCE
CFLAGS += -std=c99 -Wall -Wextra -Wno-unused-parameter -fPIC
# SDL3's SDL_system.h forward-declares `typedef union _XEvent XEvent;` (for the
# X11 event-hook API) with no opt-out, and Xlib.h defines the same typedef. The
# two are identical, so it is harmless; -std=c99 just flags the C11-legal
# repeat. Silence only that case (conflicting typedefs stay hard errors); gcc
# does not warn here and accepts the unknown -Wno- option quietly.
CFLAGS += -std=c99 -Wall -Wextra -Wno-unused-parameter \
-Wno-typedef-redefinition -fPIC
# Opt-in strict mode: STRICT=1 turns warnings into errors so CI surfaces
# new diagnostics at PR time. STRICT_CFLAGS is applied only to first-
# party objects via mk/common.mk's compile rule; upstream-derived libXt
Expand Down
34 changes: 30 additions & 4 deletions mk/libxt.mk
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ LIBXT_GEN_DIR := $(OUT)/libxt-gen
LIBXT_OBJ_DIR := $(OUT)/libxt
LIBXT_HOST_DIR := $(OUT)/host
LIBXT_TARGET := $(OUT)/libXt-compat.so
LIBXT_PATCHES := $(sort $(wildcard compat/libxt-patches/*.patch))
LIBXT_PATCH_LIST_FILE := $(OUT)/upstream/.libxt-patch-list
$(shell mkdir -p $(dir $(LIBXT_PATCH_LIST_FILE)); \
new='$(sort $(notdir $(wildcard compat/libxt-patches/*.patch)))'; \
old=$$(cat $(LIBXT_PATCH_LIST_FILE) 2>/dev/null || true); \
if [ "$$new" != "$$old" ]; then \
printf '%s\n' "$$new" > $(LIBXT_PATCH_LIST_FILE); \
fi)
LIBXT_PATCH_STAMP := $(LIBXT_SRC_DIR)/.compat-patches-stamp

# mk/libxt.mk is included before mk/upstream-headers.mk, but its rules need
# this stamp while make parses prerequisites. Keep the value aligned with the
Expand Down Expand Up @@ -98,15 +107,32 @@ $(LIBXT_HOST_DIR) $(LIBXT_GEN_DIR) $(LIBXT_OBJ_DIR):

# Compile makestrs as a host-side tool. It is a single-file standalone
# generator that depends only on libc; no need for any libx11-compat
# include paths.
$(LIBXT_HOST_MAKESTRS): $(UPSTREAM_HEADERS_STAMP) | $(LIBXT_HOST_DIR)
# include paths. Depend on the staged makestrs.c (not just the headers
# stamp) so the LIBXT_PATCH_STAMP recipe, which runs `fetch --force` and
# briefly removes and re-stages util/makestrs.c, cannot race this compile
# under parallel make and leave it reading a half-removed file.
$(LIBXT_HOST_MAKESTRS): $(LIBXT_UTIL_DIR)/makestrs.c | $(LIBXT_HOST_DIR)
@echo " HOSTCC $(LIBXT_UTIL_DIR)/makestrs.c"
$(Q)$(HOST_CC) -O2 -o $@ $(LIBXT_UTIL_DIR)/makestrs.c

# The upstream sync stages libXt sources and util files as side effects.
# Declare them so a clean build knows how to fetch these normal
# prerequisites before trying to build libXt objects or the host generator.
$(LIBXT_SRCS) $(LIBXT_UTIL_DIR)/makestrs.c $(LIBXT_STRING_LIST) $(LIBXT_TEMPLATES): $(UPSTREAM_HEADERS_STAMP)
$(LIBXT_PATCH_STAMP): $(UPSTREAM_HEADERS_STAMP) $(LIBXT_PATCHES) $(LIBXT_PATCH_LIST_FILE) mk/libxt.mk
@echo " PATCH libXt"
$(Q)$(PYTHON) $(UPSTREAM_SYNC) fetch --force $(UPSTREAM_HEADERS_DIR)
$(Q)set -e; for patch in $(abspath $(LIBXT_PATCHES)); do \
if patch -d $(abspath $(LIBXT_SRC_DIR)) -p1 --dry-run < "$$patch" >/dev/null; then \
patch -d $(abspath $(LIBXT_SRC_DIR)) -p1 < "$$patch" >/dev/null; \
elif patch -d $(abspath $(LIBXT_SRC_DIR)) -p1 -R --dry-run < "$$patch" >/dev/null; then \
:; \
else \
echo " PATCH failed $$patch" >&2; exit 1; \
fi; \
done
$(Q)touch $@

$(LIBXT_SRCS) $(LIBXT_UTIL_DIR)/makestrs.c $(LIBXT_STRING_LIST) $(LIBXT_TEMPLATES): $(UPSTREAM_HEADERS_STAMP) $(LIBXT_PATCH_STAMP)

# Run makestrs from the topdir so the "util/StrDefs.ct" / "util/StrDefs.ht"
# paths embedded in string.list resolve against cwd. The generated headers
Expand Down Expand Up @@ -142,7 +168,7 @@ $(OUT)/upstream/include/X11/Shell.h: $(LIBXT_GEN_DIR)/Shell.h
# files have been staged before the recipe reads them; the explicit
# dependency on LIBXT_GEN_HEADERS ensures StringDefs.h is available before
# any unit that quotes it compiles.
$(LIBXT_OBJ_DIR)/%.o: $(UPSTREAM_HEADERS_STAMP) $(LIBXT_GEN_HEADERS) \
$(LIBXT_OBJ_DIR)/%.o: $(UPSTREAM_HEADERS_STAMP) $(LIBXT_PATCH_STAMP) $(LIBXT_GEN_HEADERS) \
$(LIBXT_STAGED_H) $(SDL_BACKEND_STAMP) | $(LIBXT_OBJ_DIR)
@echo " CC $(LIBXT_SRC_DIR)/$*.c"
$(Q)$(CC) $(LIBXT_CPPFLAGS) $(CFLAGS) $(LIBXT_CFLAGS) $(CFLAGS_EXTRA) \
Expand Down
Loading
Loading