Windows: official port -- UCRT static build, UTF-8 throughout, Low IL sandbox, ncurses TUI, installer; v0.6.0

Build
  * cross build switches to pkgsCross.ucrt64 (shell-win.nix); curl (Schannel),
    winpthreads and ncurses 6.6 are linked statically, so the release is
    cst-agent.exe + busybox.exe (busybox-w32 Unicode) with no extra DLLs
  * windres embeds a manifest with activeCodePage=UTF-8 and a version resource
  * ncurses 6.6 Windows console driver patches (mouse button state, A_DIM,
    wide-char declarations): scripts/patches/
  * scripts/bootstrap_msys2.sh builds the same release natively in MSYS2
    UCRT64; PR #1's MINGW64 notes now point there
  * vendor/tre-regex (musl's TRE) provides <regex.h> on MinGW

UTF-8
  * paths cross the system call boundary as UTF-16 (*W APIs); console code
    pages set to 65001 and restored on exit; files are read and written in
    binary mode
  * child output that is not valid UTF-8 is decoded from the console, OEM or
    ANSI system code page
  * read_file hides CR; edit_file, apply_patch and write_file keep CRLF line
    endings and a UTF-8 BOM

OS layer and processes
  * os_dir_* and cst_fnmatch replace the POSIX-only directory walks in
    subagent, plugins, skills, undo and fs_tools; realpath / path_under and
    symlink rejection work on Windows
  * every child goes through CreateProcessW with its own environment block
    inside a Job Object, so timeouts and Esc end the whole process tree

Sandbox (auto mode, "auto · lowil")
  * run_shell runs with a restricted Low integrity token; the workspace gets
    an inheritable Low label once, the sandbox TEMP is labeled Low, and the
    credentials file gets a no-read-up label
  * drive roots, the profile and system folders are never labeled
  * network commands still ask, since Low IL does not block the network;
    Access denied and MSYS2's 0xc0000142 offer a rerun outside the sandbox
  * /sandbox lists labeled folders, /sandbox reset-labels restores them

TUI
  * raster console fonts are switched to Consolas / NSimSun while running
  * the home logo falls back to text when the console font lacks U+2580
  * mouse drag copy goes to the Windows clipboard

Distribution
  * scripts/install_windows.ps1 installs a Gitea release into
    %LOCALAPPDATA%\Programs\cst-agent and adds it to the user PATH;
    cst-agent-update.cmd reruns it; scripts/release_windows.sh uploads the
    zip with the token taken from the environment
  * scripts/run_windows.bat is gone; docs/README-windows.md is rewritten

Tests
  * new test_lowil, test_win_utf8 and test_os_dir; tests made portable
    (cst_test_util.h)
  * scripts/win_test.sh and scripts/winvm/ run the suite on English and
    Chinese Windows Server VMs as Administrator, a standard user and a
    Chinese user name: 29/29 on all six
This commit is contained in:
WHC
2026-10-06 23:08:14 +08:00
parent 9862c29f58
commit 2a43ae6622
84 changed files with 9784 additions and 1083 deletions
@@ -0,0 +1,350 @@
--- a/ncurses/curses.priv.h
+++ b/ncurses/curses.priv.h
@@ -2294,7 +2294,10 @@
#if defined(_NC_WINDOWS_NATIVE) && !defined(_NC_MSC) && !USE_NAMED_PIPES
/*
* MinGW has wide-character functions, but they do not work correctly.
+ * Declare the originals first so a later <wchar.h> is not macro-expanded.
*/
+#include <stdlib.h>
+#include <wchar.h>
extern int __MINGW_NOTHROW _nc_wctomb(char *, wchar_t);
#define wctomb(s,wc) _nc_wctomb(s,wc)
#define wcrtomb(s,wc,n) _nc_wctomb(s,wc)
--- a/ncurses/tinfo/lib_win32con.c
+++ b/ncurses/tinfo/lib_win32con.c
@@ -653,6 +653,21 @@
return result;
}
+/* Whether handle_mouse() turns this record into an event: wheel, a button
+ * change (press or release), or a drag. */
+static bool
+mouse_wanted(const SCREEN *sp, const MOUSE_EVENT_RECORD *mer)
+{
+ int held = (int) (mer->dwButtonState & BUTTON_MASK);
+
+ if (mer->dwEventFlags & MOUSE_WHEELED)
+ return TRUE;
+ if (held != sp->_drv_mouse_new_buttons)
+ return TRUE;
+ return (mer->dwEventFlags & MOUSE_MOVED) && held
+ && (sp->_mouse_mask & REPORT_MOUSE_POSITION);
+}
+
#define AdjustY() (WINCONSOLE.buffered \
? 0 \
: (int) WINCONSOLE.SBI.srWindow.Top)
@@ -661,36 +676,47 @@
handle_mouse(SCREEN *sp, MOUSE_EVENT_RECORD mer)
{
MEVENT work;
- bool result = FALSE;
+ int old_buttons;
+ int new_buttons;
assert(sp);
- sp->_drv_mouse_old_buttons = sp->_drv_mouse_new_buttons;
- sp->_drv_mouse_new_buttons = mer.dwButtonState & BUTTON_MASK;
+ if (sp->_drv_mouse_tail >= FIFO_SIZE)
+ return FALSE;
- /*
- * We're only interested if the button is pressed or released.
- * FIXME: implement continuous event-tracking.
- */
- if (sp->_drv_mouse_new_buttons != sp->_drv_mouse_old_buttons) {
- memset(&work, 0, sizeof(work));
+ memset(&work, 0, sizeof(work));
+ work.x = mer.dwMousePosition.X;
+ work.y = mer.dwMousePosition.Y - AdjustY();
+
+ /* the wheel reports like xterm: button 4 up, button 5 down */
+ if (mer.dwEventFlags & MOUSE_WHEELED) {
+ work.bstate = ((SHORT) HIWORD(mer.dwButtonState) > 0)
+ ? BUTTON4_PRESSED
+ : BUTTON5_PRESSED;
+ sp->_drv_mouse_fifo[sp->_drv_mouse_tail++] = work;
+ return TRUE;
+ }
- if (sp->_drv_mouse_new_buttons) {
- work.bstate |= decode_mouse(sp, sp->_drv_mouse_new_buttons);
+ old_buttons = sp->_drv_mouse_new_buttons;
+ new_buttons = (int) (mer.dwButtonState & BUTTON_MASK);
+ sp->_drv_mouse_old_buttons = old_buttons;
+ sp->_drv_mouse_new_buttons = new_buttons;
+
+ if (new_buttons != old_buttons) {
+ if (new_buttons & ~old_buttons) {
+ work.bstate = decode_mouse(sp, new_buttons & ~old_buttons);
} else {
/* cf: BUTTON_PRESSED, BUTTON_RELEASED */
- work.bstate |= (decode_mouse(sp, sp->_drv_mouse_old_buttons)
- >> 1);
- result = TRUE;
+ work.bstate = decode_mouse(sp, old_buttons & ~new_buttons) >> 1;
}
-
- work.x = mer.dwMousePosition.X;
- work.y = mer.dwMousePosition.Y - AdjustY();
-
- sp->_drv_mouse_fifo[sp->_drv_mouse_tail] = work;
- sp->_drv_mouse_tail += 1;
+ } else if ((mer.dwEventFlags & MOUSE_MOVED) && new_buttons
+ && (sp->_mouse_mask & REPORT_MOUSE_POSITION)) {
+ work.bstate = REPORT_MOUSE_POSITION; /* drag */
+ } else {
+ return FALSE;
}
- return result;
+ sp->_drv_mouse_fifo[sp->_drv_mouse_tail++] = work;
+ return TRUE;
}
static int
@@ -813,6 +839,13 @@
returnCode(found);
}
+#if USE_WIDEC_SUPPORT
+/* UTF-8 bytes of the last non-ASCII key event not yet read */
+static int utf8_pend[4];
+static int utf8_len, utf8_pos;
+static unsigned utf8_high;
+#endif
+
NCURSES_EXPORT(int)
_nc_console_twait(
const SCREEN *sp,
@@ -845,6 +878,14 @@
TR(TRACE_IEVENT, ("start twait: hdl=%p, %d milliseconds, mode: %d",
hdl, milliseconds, mode));
+#if USE_WIDEC_SUPPORT
+ if ((mode & TW_INPUT) && utf8_pos < utf8_len) {
+ if (timeleft)
+ *timeleft = milliseconds;
+ return TW_INPUT;
+ }
+#endif
+
if (milliseconds < 0)
milliseconds = NC_INFINITY;
@@ -940,9 +981,7 @@
continue;
case MOUSE_EVENT:
T(("twait:event MOUSE_EVENT"));
- if (decode_mouse(sp,
- (inp_rec.Event.MouseEvent.dwButtonState
- & BUTTON_MASK)) == 0) {
+ if (!mouse_wanted(sp, &inp_rec.Event.MouseEvent)) {
CONSUME();
} else if (mode & TW_MOUSE) {
code = TW_MOUSE;
@@ -1024,6 +1063,16 @@
T((T_CALLED("lib_win32con::_nc_console_read(%p)"), sp));
+#if USE_WIDEC_SUPPORT
+ /* wget_wch() decodes the input as a byte stream in the locale's
+ * multibyte encoding (UTF-8), so non-ASCII UTF-16 key events are
+ * handed over one UTF-8 byte per call. */
+ if (utf8_pos < utf8_len) {
+ *buf = utf8_pend[utf8_pos++];
+ returnCode(1);
+ }
+#endif
+
while ((b = read_keycode(hdl, &inp_rec, 1, &nRead))) {
if (b && nRead > 0) {
if (rc < 0)
@@ -1034,6 +1083,36 @@
continue;
*buf = (int) inp_rec.Event.KeyEventChar;
vk = inp_rec.Event.KeyEvent.wVirtualKeyCode;
+#if USE_WIDEC_SUPPORT
+ if (*buf >= 0x80) {
+ unsigned cp = (unsigned) *buf;
+ if (cp >= 0xD800 && cp < 0xDC00) {
+ utf8_high = cp;
+ continue;
+ }
+ if (cp >= 0xDC00 && cp < 0xE000) {
+ if (!utf8_high)
+ continue;
+ cp = 0x10000 + ((utf8_high - 0xD800) << 10) + (cp - 0xDC00);
+ }
+ utf8_high = 0;
+ utf8_len = 0;
+ if (cp < 0x800) {
+ utf8_pend[utf8_len++] = (int) (0xC0 | (cp >> 6));
+ } else if (cp < 0x10000) {
+ utf8_pend[utf8_len++] = (int) (0xE0 | (cp >> 12));
+ utf8_pend[utf8_len++] = (int) (0x80 | ((cp >> 6) & 0x3F));
+ } else {
+ utf8_pend[utf8_len++] = (int) (0xF0 | (cp >> 18));
+ utf8_pend[utf8_len++] = (int) (0x80 | ((cp >> 12) & 0x3F));
+ utf8_pend[utf8_len++] = (int) (0x80 | ((cp >> 6) & 0x3F));
+ }
+ utf8_pend[utf8_len++] = (int) (0x80 | (cp & 0x3F));
+ *buf = utf8_pend[0];
+ utf8_pos = 1;
+ break;
+ }
+#endif
/*
* There are 24 virtual function-keys, and typically
* 12 function-keys on a keyboard. Use the shift-modifier
--- a/ncurses/widechar/widechars.c
+++ b/ncurses/widechar/widechars.c
@@ -97,20 +97,10 @@
wchar_t temp;
if (s != NULL && n != 0) {
+ /* _nc_mbtowc() returns the bytes used, not a character count */
count = _nc_mbtowc(&temp, s, n);
- if (count == 1) {
- int check = WideCharToMultiByte(CP_UTF8,
- 0,
- &temp,
- 1,
- NULL,
- 0, /* compute length only */
- NULL,
- NULL);
- TR(TRACE_BITS, ("\tcheck ->%d\n", check));
- if (check > 0 && (size_t) check <= n) {
- result = check;
- }
+ if (count > 0 && (size_t) count <= n) {
+ result = count;
}
} else {
result = 0;
--- a/ncurses/win32con/win32_driver.c
+++ b/ncurses/win32con/win32_driver.c
@@ -87,8 +87,13 @@
if (ch & A_BOLD)
res |= FOREGROUND_INTENSITY;
- if (ch & A_DIM)
- res |= BACKGROUND_INTENSITY;
+ /* dim lowers the foreground: bright -> normal, light gray -> dark gray */
+ if ((ch & A_DIM) && !(ch & A_BOLD)) {
+ if (res & FOREGROUND_INTENSITY)
+ res &= ~FOREGROUND_INTENSITY;
+ else if ((res & 7) == 7)
+ res = (res & ~7) | FOREGROUND_INTENSITY;
+ }
return res;
}
@@ -159,7 +164,6 @@
int y, int x, cchar_t *str, int limit)
{
int actual = 0;
- MakeArray(ci, CHAR_INFO, limit);
COORD loc, siz;
SMALL_RECT rec;
int i;
@@ -169,39 +173,71 @@
AssertTCB();
SetSP();
- for (i = actual = 0; i < limit; i++) {
- ch = str[i];
- if (isWidecExt(ch))
- continue;
- ci[actual].CharInfoChar = CharOf(ch);
- ci[actual].Attributes = MapAttr(WINCONSOLE.SBI.wAttributes,
- AttrOf(ch));
- if (AttrOf(ch) & A_ALTCHARSET) {
- if (_nc_wacs) {
- int which = CharOf(ch);
- if (which > 0
- && which < ACS_LEN
- && CharOf(_nc_wacs[which]) != 0) {
- ci[actual].CharInfoChar = CharOf(_nc_wacs[which]);
+ /*
+ * The console stores a double-width character as two cells flagged
+ * leading/trailing byte. Never start or end a write inside one, and
+ * emit both halves so the columns after it stay put.
+ */
+ if (limit > 0 && x > 0 && isWidecExt(str[0])) {
+ --str;
+ --x;
+ ++limit;
+ }
+ if (limit > 0 && x + limit < screen_columns(sp) && isWidecExt(str[limit]))
+ ++limit;
+
+ {
+ MakeArray(ci, CHAR_INFO, limit);
+
+ for (i = actual = 0; i < limit; i++) {
+ ch = str[i];
+ if (isWidecExt(ch)) {
+ if (actual > 0
+ && (ci[actual - 1].Attributes & COMMON_LVB_LEADING_BYTE)) {
+ ci[actual] = ci[actual - 1];
+ ci[actual].Attributes = (WORD)
+ ((ci[actual].Attributes & ~COMMON_LVB_LEADING_BYTE)
+ | COMMON_LVB_TRAILING_BYTE);
} else {
ci[actual].CharInfoChar = ' ';
+ ci[actual].Attributes = MapAttr(WINCONSOLE.SBI.wAttributes,
+ AttrOf(ch));
}
+ ++actual;
+ continue;
}
+ ci[actual].CharInfoChar = CharOf(ch);
+ ci[actual].Attributes = MapAttr(WINCONSOLE.SBI.wAttributes,
+ AttrOf(ch));
+ if (AttrOf(ch) & A_ALTCHARSET) {
+ if (_nc_wacs) {
+ int which = CharOf(ch);
+ if (which > 0
+ && which < ACS_LEN
+ && CharOf(_nc_wacs[which]) != 0) {
+ ci[actual].CharInfoChar = CharOf(_nc_wacs[which]);
+ } else {
+ ci[actual].CharInfoChar = ' ';
+ }
+ }
+ }
+ if (i + 1 < limit && isWidecExt(str[i + 1]))
+ ci[actual].Attributes |= COMMON_LVB_LEADING_BYTE;
+ ++actual;
}
- ++actual;
- }
- loc.X = (SHORT) 0;
- loc.Y = (SHORT) 0;
- siz.X = (SHORT) actual;
- siz.Y = 1;
-
- rec.Left = (SHORT) x;
- rec.Top = (SHORT) (y + AdjustY());
- rec.Right = (SHORT) (x + limit - 1);
- rec.Bottom = rec.Top;
+ loc.X = (SHORT) 0;
+ loc.Y = (SHORT) 0;
+ siz.X = (SHORT) actual;
+ siz.Y = 1;
+
+ rec.Left = (SHORT) x;
+ rec.Top = (SHORT) (y + AdjustY());
+ rec.Right = (SHORT) (x + actual - 1);
+ rec.Bottom = rec.Top;
- return write_screen(WINCONSOLE.hdl, ci, siz, loc, &rec);
+ return write_screen(WINCONSOLE.hdl, ci, siz, loc, &rec);
+ }
}
#define con_write(tcb, y, x, str, n) con_write16(tcb, y, x, str, n)
#else