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
103 changes: 103 additions & 0 deletions examples/terminal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Cross-Platform Native Terminal Demo / 跨平台原生终端示例

[English](#english) | [简体中文](#简体中文)

---

<a name="english"></a>
## English

This example demonstrates how to build a **cross-platform native CLI/TUI application** with TypePHP using mixed C++ and PHP programming (`cpp-src` + `php-src`).

Standard PHP CLI applications traditionally depend on executing external processes (`stty`, `mode CON`) or installing separate PHP extensions to query window size, enable raw mode, or capture hidden passwords. When compiled into a standalone native binary with TypePHP (`tpc -m bin`), this implementation interacts directly with the operating system kernel and console APIs with zero external dependencies and zero `zval` boxing overhead on hot paths.

### Supported Operating Systems

- **Linux** (`x86_64`, `aarch64`): POSIX `termios`, `sys/ioctl.h`
- **macOS** (`Apple Silicon`, `Intel`): Darwin POSIX `termios`, `sys/ioctl.h`
- **Windows** (`x86_64`): Win32 Console API (`GetStdHandle`, `SetConsoleMode`, `ReadConsoleInputW`, `SetConsoleTitleW`)

### Features Implemented

1. **Terminal Sizing**: Queries columns and lines via `ioctl(TIOCGWINSZ)` on Unix and `GetConsoleScreenBufferInfo` on Windows.
2. **TTY Detection**: Fast in-process check for interactive terminal stream (`isatty` / `GetConsoleMode`).
3. **Raw Mode**: Non-canonical raw input mode without line buffering, featuring automatic terminal restoration on exit (`atexit`) to prevent terminal corruption.
4. **Secret Password Input**: In-process hidden input without echo, restoring terminal flags immediately upon completion.
5. **Window Title**: Sets terminal/console window title via OSC escape sequence on Unix and `SetConsoleTitleW` (UTF-8 to UTF-16) on Windows.
6. **ANSI Color Support**: Probes environment variables (`TERM`, `NO_COLOR`) to determine formatting compatibility.

### Project Structure

```
examples/terminal/
├── project.yml # TypePHP build configuration
├── main.php # Interactive CLI demonstration entrypoint
├── cpp-src/
│ └── terminal.cc # Cross-platform C++ implementation (POSIX & Win32)
└── php-src/
└── Terminal.php # PHP stubs and object-oriented Terminal wrapper
```

### Compilation & Usage

Compile the project into a standalone executable:

```bash
php bin/tpc.php examples/terminal/project.yml -o terminal-demo
```

Run the compiled executable:

```bash
./terminal-demo
```

---

<a name="简体中文"></a>
## 简体中文

本示例展示了如何使用 TypePHP 的 C++/PHP 混合编程能力(`cpp-src` + `php-src`),构建**跨平台原生命令行与终端(CLI/TUI)应用程序**。

在传统 PHP 命令行开发中,通常需要调用外部进程(例如 `stty` 或 Windows `mode CON`)或依赖额外安装的 C 扩展来获取终端尺寸、开启 Raw 模式或读取无回显密码。借助 TypePHP 编译为独立二进制(`tpc -m bin`)后,本实现直接调用底层操作系统内核与控制台 API,具备零外部依赖、零进程开销以及原生 C++ 调用性能。

### 支持的操作系统

- **Linux** (`x86_64`, `aarch64`):基于 POSIX `termios`、`sys/ioctl.h`
- **macOS** (`Apple Silicon`, `Intel`):基于 Darwin POSIX `termios`、`sys/ioctl.h`
- **Windows** (`x86_64`):基于 Win32 控制台 API(`GetStdHandle`、`SetConsoleMode`、`ReadConsoleInputW`、`SetConsoleTitleW`)

### 核心功能

1. **终端窗口尺寸**:在 Unix 下调用 `ioctl(TIOCGWINSZ)`,在 Windows 下调用 `GetConsoleScreenBufferInfo`,快速读取行列数。
2. **TTY 终端检测**:原生物理终端连接检测(`isatty` / `GetConsoleMode`)。
3. **Raw 模式**:禁用规范输入行缓冲,支持单键即时捕获,并注册 `atexit` 自动安全恢复终端状态,防止终端异常。
4. **安全密码输入**:进程内无回显密码读取,读取完成后自动重置控制台标志。
5. **设置窗口标题**:Unix 下通过 OSC 转义序列,Windows 下通过 `SetConsoleTitleW`(UTF-8 转 UTF-16)设置控制台标题。
6. **ANSI 颜色支持探测**:检测 `TERM` 与 `NO_COLOR` 环境变量。

### 项目结构

```
examples/terminal/
├── project.yml # TypePHP 项目编译配置
├── main.php # 交互式命令行演示入口
├── cpp-src/
│ └── terminal.cc # 跨平台 C++ 实现(POSIX 与 Win32)
└── php-src/
└── Terminal.php # PHP 原生方法签名与面向对象 Terminal 封装类
```

### 编译与运行

使用 TypePHP 编译为原生二进制程序:

```bash
php bin/tpc.php examples/terminal/project.yml -o terminal-demo
```

运行编译后的程序:

```bash
./terminal-demo
```
271 changes: 271 additions & 0 deletions examples/terminal/cpp-src/terminal.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
#include <phpx.h>

#if defined(_WIN32)
#include <windows.h>
#include <io.h>
#else
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#endif

using namespace php;

#if defined(_WIN32)
static DWORD orig_console_mode = 0;
static bool raw_mode_active = false;

static void cleanup_terminal_at_exit()
{
if (raw_mode_active) {
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
if (hStdin != INVALID_HANDLE_VALUE && hStdin != NULL) {
SetConsoleMode(hStdin, orig_console_mode);
}
raw_mode_active = false;
}
}
#else
static struct termios orig_termios;
static bool raw_mode_active = false;

static void cleanup_terminal_at_exit()
{
if (raw_mode_active) {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
raw_mode_active = false;
}
}
#endif

Int php_terminal_get_width()
{
#if defined(_WIN32)
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
if (hStdout != INVALID_HANDLE_VALUE && hStdout != NULL) {
CONSOLE_SCREEN_BUFFER_INFO csbi;
if (GetConsoleScreenBufferInfo(hStdout, &csbi)) {
return static_cast<Int>(csbi.srWindow.Right - csbi.srWindow.Left + 1);
}
}
return 80;
#else
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
return static_cast<Int>(ws.ws_col);
}
return 80;
#endif
}

Int php_terminal_get_height()
{
#if defined(_WIN32)
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
if (hStdout != INVALID_HANDLE_VALUE && hStdout != NULL) {
CONSOLE_SCREEN_BUFFER_INFO csbi;
if (GetConsoleScreenBufferInfo(hStdout, &csbi)) {
return static_cast<Int>(csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
}
}
return 24;
#else
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_row > 0) {
return static_cast<Int>(ws.ws_row);
}
return 24;
#endif
}

Bool php_terminal_isatty(Int fd)
{
#if defined(_WIN32)
HANDLE h = (fd == 0) ? GetStdHandle(STD_INPUT_HANDLE) :
((fd == 1) ? GetStdHandle(STD_OUTPUT_HANDLE) : GetStdHandle(STD_ERROR_HANDLE));
if (h == INVALID_HANDLE_VALUE || h == NULL) {
return false;
}
DWORD mode;
return GetConsoleMode(h, &mode) != 0;
#else
return isatty(static_cast<int>(fd)) != 0;
#endif
}

Bool php_terminal_set_raw_mode(Bool enable)
{
#if defined(_WIN32)
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
if (hStdin == INVALID_HANDLE_VALUE || hStdin == NULL) {
return false;
}

if (enable) {
if (!raw_mode_active) {
if (!GetConsoleMode(hStdin, &orig_console_mode)) {
return false;
}
atexit(cleanup_terminal_at_exit);
}
DWORD raw_mode = orig_console_mode & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT);
if (SetConsoleMode(hStdin, raw_mode)) {
raw_mode_active = true;
return true;
}
return false;
} else {
if (raw_mode_active) {
SetConsoleMode(hStdin, orig_console_mode);
raw_mode_active = false;
}
return true;
}
#else
if (enable) {
if (!raw_mode_active) {
if (tcgetattr(STDIN_FILENO, &orig_termios) == -1) {
return false;
}
atexit(cleanup_terminal_at_exit);
}
struct termios raw = orig_termios;
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
raw.c_oflag &= ~(OPOST);
raw.c_cflag |= (CS8);
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == 0) {
raw_mode_active = true;
return true;
}
return false;
} else {
if (raw_mode_active) {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
raw_mode_active = false;
}
return true;
}
#endif
}

String php_terminal_read_char()
{
#if defined(_WIN32)
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
if (hStdin == INVALID_HANDLE_VALUE || hStdin == NULL) {
return String("");
}
INPUT_RECORD rec;
DWORD count;
while (ReadConsoleInputW(hStdin, &rec, 1, &count) && count > 0) {
if (rec.EventType == KEY_EVENT && rec.Event.KeyEvent.bKeyDown) {
WCHAR wc = rec.Event.KeyEvent.uChar.UnicodeChar;
if (wc != 0) {
char utf8[8] = {0};
int len = WideCharToMultiByte(CP_UTF8, 0, &wc, 1, utf8, sizeof(utf8) - 1, NULL, NULL);
if (len > 0) {
return String(utf8, static_cast<size_t>(len));
}
}
}
}
return String("");
#else
char c = 0;
ssize_t n = read(STDIN_FILENO, &c, 1);
if (n > 0) {
return String(&c, 1);
}
return String("");
#endif
}

String php_terminal_read_secret(String prompt)
{
if (!prompt.empty()) {
fwrite(prompt.data(), 1, prompt.length(), stdout);
fflush(stdout);
}

#if defined(_WIN32)
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode = 0;
bool has_console = (hStdin != INVALID_HANDLE_VALUE && hStdin != NULL && GetConsoleMode(hStdin, &mode));
if (has_console) {
SetConsoleMode(hStdin, mode & (~ENABLE_ECHO_INPUT));
}
char buf[2048] = {0};
size_t len = 0;
if (fgets(buf, sizeof(buf), stdin) != nullptr) {
len = strlen(buf);
while (len > 0 && (buf[len - 1] == '\r' || buf[len - 1] == '\n')) {
buf[--len] = '\0';
}
}
if (has_console) {
SetConsoleMode(hStdin, mode);
}
printf("\n");
fflush(stdout);
return String(buf, len);
#else
struct termios oldt, newt;
bool has_tty = (tcgetattr(STDIN_FILENO, &oldt) == 0);
if (has_tty) {
newt = oldt;
newt.c_lflag &= ~(ECHO);
tcsetattr(STDIN_FILENO, TCSANOW, &newt);
}
char buf[2048] = {0};
size_t len = 0;
if (fgets(buf, sizeof(buf), stdin) != nullptr) {
len = strlen(buf);
while (len > 0 && (buf[len - 1] == '\r' || buf[len - 1] == '\n')) {
buf[--len] = '\0';
}
}
if (has_tty) {
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
}
printf("\n");
fflush(stdout);
return String(buf, len);
#endif
}

Bool php_terminal_set_title(String title)
{
#if defined(_WIN32)
int wlen = MultiByteToWideChar(CP_UTF8, 0, title.data(), -1, NULL, 0);
if (wlen <= 0) {
return false;
}
auto *wstr = new WCHAR[wlen];
MultiByteToWideChar(CP_UTF8, 0, title.data(), -1, wstr, wlen);
BOOL ok = SetConsoleTitleW(wstr);
delete[] wstr;
return ok != 0;
#else
printf("\033]0;%s\007", title.data());
fflush(stdout);
return true;
#endif
}

Bool php_terminal_supports_color()
{
const char *term = std::getenv("TERM");
if (term != nullptr && std::strcmp(term, "dumb") == 0) {
return false;
}
const char *no_color = std::getenv("NO_COLOR");
if (no_color != nullptr && no_color[0] != '\0') {
return false;
}
return true;
}
Loading
Loading