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
75 changes: 75 additions & 0 deletions docs/NATIVE_TERMINAL_SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Real integrated terminal

## Decisions

| Decision | Source | Status |
| --- | --- | --- |
| Real terminal on macOS and Windows, no separate command entry field | USER | CONFIRMED |
| macOS zsh with PTY and a single terminal input/output surface | USER | CONFIRMED |
| Windows ConPTY, retain existing cmd.exe default | USER | CONFIRMED |
| VT screen/keyboard processing through SHA-256-pinned MIT libvterm 0.3.3 release archive | IMPLEMENTATION | CONFIRMED |
| One shell per workspace view, lazy startup on terminal activation | USER | CONFIRMED |
| Use selected workspace as startup directory; home if no workspace | USER | CONFIRMED |
| 2,000 scrollback lines, 1 MiB queued input | USER | CONFIRMED |
| Screen limit: 512 rows by 1024 columns | AGENT_PARAMETER | ASSUMED |
| Preserve build/debug/Git subprocess APIs and present their logs as output, not shell output | IMPLEMENTATION | CONFIRMED |
| Do not silently fall back to pipe input on unsupported ConPTY systems | USER | CONFIRMED |
| macOS production shell `/bin/zsh -l -i`; tests use `-f -i` | USER | CONFIRMED |
| Retain live shell while changing or hiding panels; resize never restarts | USER | CONFIRMED |
| Reactivating Terminal after shell exit starts a new shell; Start also restarts | DELEGATED | ASSUMED |
| Native wheel scroll and selection; Select All selects visible grid | DELEGATED | ASSUMED |
| Clipboard line endings normalize CRLF/LF to Return bytes | IMPLEMENTATION | CONFIRMED |
| Static underline cursor; IME composition text is drawn at the cursor | IMPLEMENTATION | CONFIRMED |
| No OSC52 clipboard writes or shell-triggered external actions | USER | CONFIRMED |

## Shared base and category

One feature category: real integrated terminal, branch `feature/native-terminal`.
The shared base is the public screen/session/key/snapshot contract in terminal.h,
the existing status/error conventions and reproducibly built libvterm target.
Validate this base before implementing its native consumers.

## Implementation units

1. Shared screen model: VT parsing, Unicode cells, colors, cursor, alternate
screen, bounded history, snapshots, key/paste encoding and regression tests.
2. Session transports: macOS PTY controlling terminal, Windows ConPTY, async
input/output, resize, lifecycle/termination and real shell tests.
3. Native terminal surfaces: direct text/key input, copy/paste, selection, resize,
scrollback, cursor and ANSI rendering; remove separate shell entry/send UI.
4. Cross-platform integration, lifetime/error tests, packaging licenses and
independent review; one PR targeting dev. No automatic merge is requested.

### Implementation state

| Unit | State | Evidence |
| --- | --- | --- |
| Screen model | COMPLETE | VT regression executable; C ABI preserved |
| PTY/ConPTY sessions | IMPLEMENTED; Windows runtime validation pending | macOS real-shell tests; Windows CI initially exposed redirected standard-handle inheritance, fixed with null `STARTF_USESTDHANDLES` |
| Native grid integration | IMPLEMENTED; final independent review pending | macOS grid/keyboard runtime and workspace/editor layout tests; Windows grid runtime test included for CI |
| Integration/license/audit | Local validation COMPLETE; parent reviews and Windows CI pending | full macOS build and 40/40 CTest pass; libvterm license verified in built app; one draft PR managed by parent |

Private macOS child cleanup obtains Mach audit tokens and signals through
`proc_signal_with_audittoken`, which validates PID generations in the kernel.
Stop first terminates captured descendants, sends shell SIGHUP, and escalates
the still-owned root after 200 ms if needed. Readers poll at most 50 ms per wait;
post-root-exit drain is capped at 500 ms for descendants retaining the slave.
Output EOF alone never kills a live shell: it waits for the actual exit status.
Windows owns a kill-on-close Job Object, cancels synchronous writer I/O, closes
ConPTY outside the model/pseudoconsole locks, and drains until actual pipe EOF.
The test-only `AXYNE_TERMINAL_TRACE` switch emits API status codes and byte counts,
never child output, environment contents or command strings.

## Required audit

Private terminal implementation uses C++17 standard mutexes, worker threads and
RAII behind the frozen C ABI; existing C17 modules and native C/Objective-C
adapters are unchanged. This is an IMPLEMENTATION choice. Screen bounds are
512 rows and 1024 columns (AGENT_PARAMETER / ASSUMED). Manual model operations
never notify; reader notifications run after releasing model and queue locks.

Prove isatty/prompt, direct input, Tab/history, Ctrl+C without killing the shell,
screen cursor/ANSI semantics, resizing, shell exit/stop/restart and teardown.
Keep actual terminal output free of `[stderr]` prefixes. Unsupported Windows
versions must report a clear error. Keep runtime UTF-8/CJK and clipboard input
separate from app/editor shortcuts and preserve ordinary build/Git/debug output.
90 changes: 90 additions & 0 deletions include/axyne/terminal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#ifndef AXYNE_TERMINAL_H
#define AXYNE_TERMINAL_H

#include <stddef.h>
#include <stdint.h>
#include "axyne/status.h"

#ifdef __cplusplus
extern "C" {
#endif

typedef struct AxyneTerminal AxyneTerminal;
typedef void (*AxyneTerminalChangedFn)(AxyneTerminal *, void *);
typedef void (*AxyneTerminalExitFn)(AxyneTerminal *, int, void *);

typedef struct AxyneTerminalSpec {
const char *executable;
const char *const *arguments;
size_t argument_count;
const char *working_directory;
AxyneTerminalChangedFn on_changed;
AxyneTerminalExitFn on_exit;
void *user_data;
} AxyneTerminalSpec;

typedef enum AxyneTerminalKey {
AXYNE_TERMINAL_ENTER = 1, AXYNE_TERMINAL_TAB,
AXYNE_TERMINAL_BACKSPACE, AXYNE_TERMINAL_ESCAPE,
AXYNE_TERMINAL_UP, AXYNE_TERMINAL_DOWN,
AXYNE_TERMINAL_LEFT, AXYNE_TERMINAL_RIGHT,
AXYNE_TERMINAL_INSERT, AXYNE_TERMINAL_DELETE,
AXYNE_TERMINAL_HOME, AXYNE_TERMINAL_END,
AXYNE_TERMINAL_PAGEUP, AXYNE_TERMINAL_PAGEDOWN,
AXYNE_TERMINAL_F1 = 257
} AxyneTerminalKey;
enum { AXYNE_TERMINAL_SHIFT = 1, AXYNE_TERMINAL_ALT = 2,
AXYNE_TERMINAL_CTRL = 4 };
enum { AXYNE_TERMINAL_BOLD = 1, AXYNE_TERMINAL_ITALIC = 2,
AXYNE_TERMINAL_UNDERLINE = 4, AXYNE_TERMINAL_REVERSE = 8 };

typedef struct AxyneTerminalCell {
uint32_t chars[6];
uint32_t foreground, background; /* RGB, default palette already resolved */
unsigned attributes;
int width; /* 0: wide-character continuation; 1 or 2: leading cell */
} AxyneTerminalCell;

typedef struct AxyneTerminalSnapshot {
int rows, columns;
int cursor_row, cursor_column, cursor_visible;
int alternate_screen;
int scroll_offset;
size_t history_lines;
AxyneTerminalCell *cells;
} AxyneTerminalSnapshot;

/* The model supports deterministic VT tests without starting a child.
* Feed/snapshot/key/resize are internally synchronized. Callbacks notify only;
* native adapters marshal repaint/exit to their UI thread. Session output
* changes and exit callbacks run serially on the session reader thread, with
* no model locks held. Exit is delivered once, after output is drained; no
* further session notifications follow it. Callbacks may begin before start
* returns, so create/initialize user_data before calling start. They may query
* snapshots or enqueue input but must not start or release a session. Manual
* model operations (feed without a child, resize, scroll, clear) do not deliver
* notifications: callers schedule their own repaint. Inputs are copied by
* start. user_data stays valid until release finishes. Release terminates the
* session and joins workers; no callbacks can arrive after it returns. */
AxyneStatus axyne_terminal_create(int rows, int columns, size_t history_limit,
AxyneTerminal **out, AxyneError *error);
AxyneStatus axyne_terminal_start(AxyneTerminal *, const AxyneTerminalSpec *, AxyneError *);
AxyneStatus axyne_terminal_write(AxyneTerminal *, const char *, size_t, AxyneError *);
AxyneStatus axyne_terminal_key(AxyneTerminal *, AxyneTerminalKey, unsigned, AxyneError *);
AxyneStatus axyne_terminal_unichar(AxyneTerminal *, uint32_t, unsigned, AxyneError *);
AxyneStatus axyne_terminal_paste(AxyneTerminal *, const char *, size_t, AxyneError *);
AxyneStatus axyne_terminal_resize(AxyneTerminal *, int rows, int columns, AxyneError *);
void axyne_terminal_feed(AxyneTerminal *, const char *, size_t);
void axyne_terminal_scroll(AxyneTerminal *, int lines);
void axyne_terminal_clear(AxyneTerminal *);
void axyne_terminal_set_colors(AxyneTerminal *, uint32_t foreground, uint32_t background);
int axyne_terminal_running(AxyneTerminal *);
AxyneStatus axyne_terminal_snapshot(AxyneTerminal *, AxyneTerminalSnapshot *, AxyneError *);
void axyne_terminal_snapshot_free(AxyneTerminalSnapshot *);
AxyneStatus axyne_terminal_terminate(AxyneTerminal *, AxyneError *);
void axyne_terminal_release(AxyneTerminal *);

#ifdef __cplusplus
}
#endif
#endif
50 changes: 50 additions & 0 deletions src/features/terminal/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Upstream 0.3.3 release includes generated encoding tables, unlike Git tags.
# Pin its complete archive so Windows needs no Perl/source-generation tools.
enable_language(CXX)
FetchContent_Declare(axyne_vterm_source
URL https://www.leonerd.org.uk/code/libvterm/libvterm-0.3.3.tar.gz
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
URL_HASH SHA256=09156f43dd2128bd347cbeebe50d9a571d32c64e0cf18d211197946aff7226e0)
FetchContent_GetProperties(axyne_vterm_source)
if(NOT axyne_vterm_source_POPULATED)
cmake_policy(PUSH)
if(POLICY CMP0169)
cmake_policy(SET CMP0169 OLD)
endif()
FetchContent_Populate(axyne_vterm_source)
cmake_policy(POP)
endif()
add_library(axyne_terminal STATIC "${CMAKE_CURRENT_LIST_DIR}/terminal.cpp")
target_include_directories(axyne_terminal PUBLIC "${PROJECT_SOURCE_DIR}/include")
target_link_libraries(axyne_terminal PRIVATE axyne_vterm)
set_target_properties(axyne_terminal PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON)
if(WIN32)
target_sources(axyne_terminal PRIVATE "${CMAKE_CURRENT_LIST_DIR}/terminal_windows.cpp")
else()
find_package(Threads REQUIRED)
target_sources(axyne_terminal PRIVATE "${CMAKE_CURRENT_LIST_DIR}/terminal_posix.cpp")
target_link_libraries(axyne_terminal PRIVATE Threads::Threads)
endif()
if(TARGET axyne_ui)
target_link_libraries(axyne_ui PRIVATE axyne_terminal)
endif()
file(GLOB AXYNE_VTERM_SOURCES CONFIGURE_DEPENDS "${axyne_vterm_source_SOURCE_DIR}/src/*.c")
add_library(axyne_vterm STATIC ${AXYNE_VTERM_SOURCES})
target_include_directories(axyne_vterm SYSTEM PUBLIC "${axyne_vterm_source_SOURCE_DIR}/include")
target_include_directories(axyne_vterm PRIVATE "${axyne_vterm_source_SOURCE_DIR}/src")
if(MSVC)
target_compile_options(axyne_vterm PRIVATE /utf-8)
endif()
if(APPLE)
add_custom_command(TARGET axyne POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${axyne_vterm_source_SOURCE_DIR}/LICENSE"
"$<TARGET_BUNDLE_CONTENT_DIR:axyne>/Resources/libvterm-LICENSE.txt")
install(FILES "${axyne_vterm_source_SOURCE_DIR}/LICENSE"
DESTINATION "${AXYNE_MACOS_BUNDLE_NAME}/Contents/Resources"
RENAME libvterm-LICENSE.txt CONFIGURATIONS Release)
elseif(WIN32)
add_custom_command(TARGET axyne POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${axyne_vterm_source_SOURCE_DIR}/LICENSE" "$<TARGET_FILE_DIR:axyne>/libvterm-LICENSE.txt")
install(FILES "${axyne_vterm_source_SOURCE_DIR}/LICENSE"
DESTINATION . RENAME libvterm-LICENSE.txt CONFIGURATIONS Release)
endif()
Loading
Loading