Frontends (terminals)
AppCUI draws every control into an in-memory screen; a frontend displays that screen and produces the keyboard /
mouse events. The frontend is selected with InitializationData::Frontend or with the Frontend key of the
settings file (see Initialization):
Frontend |
Systems |
Description |
|---|---|---|
|
Windows |
the Windows console (the default on Windows) |
|
Linux, macOS |
any terminal emulator, through ncurses (the default on Linux and macOS) |
|
all |
a separate window rendered with SDL2 and an embedded font (JuliaMono) |
|
web browsers |
an HTML5 canvas inside a web page ( |
|
all |
implemented by the application (headless, remote or recorded screens) - see below |
|
all |
an in-memory screen driven by a test script ( |
Frontends linked into libAppCUI
Each native frontend is a separate CMake component, linked statically into libAppCUI. AppCUI::Core holds
everything else (including the Tests and Custom frontends) and depends on none of the UI libraries:
CMake target |
Option (default) |
Dependencies |
|---|---|---|
|
always built |
libpng, libjpeg-turbo (only with |
|
|
SDL2, SDL2_ttf, FreeType (+ X11 on Linux) |
|
|
ncursesw |
|
|
none |
|
|
none (a JavaScript host, |
By default every frontend available on the platform is linked. Turning an option OFF (for example
-DAPPCUI_FRONTEND_SDL=OFF for a console-only application) removes that frontend and its libraries from
libAppCUI; requesting it afterwards makes Application::Init fail (the error is logged).
FrontendType::Default selects the native frontend of the platform (Windows console, ncurses or, in a web browser,
the browser frontend) or, when that one is not linked, SDL.
Supported features
Feature |
Windows console |
SDL |
ncurses |
Browser |
|---|---|---|---|---|
16 colors |
Yes |
Yes |
Yes (8-color terminals show bright colors as normal ones; no colors below 8) |
Yes |
Unicode special characters (box drawing, arrows, …) |
Yes |
Yes |
Yes (not on the Linux text console, |
Yes |
FPS mode ( |
Yes |
Yes |
Yes |
Yes |
Text cursor (caret) |
Yes |
No |
Yes |
Yes |
Mouse: left button |
Yes |
Yes |
Yes |
Yes |
Mouse: right / middle button |
Right only |
Yes |
No |
Yes |
Mouse: double click |
Yes |
No |
No |
Yes |
Mouse: move and drag |
Yes |
Yes |
Depends on the terminal ( |
Yes |
Mouse: wheel |
Yes |
Yes |
No |
Yes |
|
Yes |
Yes |
Partial (see below) |
Yes, except the shortcuts reserved by the browser ( |
Clipboard |
Yes |
Windows only |
No |
Private to the page |
Resize events |
Yes |
Yes |
Yes |
Yes (the size of the page element) |
Maximized / full screen / fixed size window |
Yes |
Yes |
No (the terminal decides) |
No (the page decides) |
Character size ( |
Yes |
Yes |
No (the terminal decides) |
No ( |
Font name ( |
Yes |
No (embedded font) |
No (the terminal decides) |
No ( |
Browser (WebAssembly)
An AppCUI application compiled with Emscripten runs inside a web page: the browser frontend
renders the screen into an HTML5 canvas (with the JuliaMono font) and translates the keyboard / mouse events of the
page. The application code does not change - Application::Init() selects the browser frontend by default:
emcmake cmake -S . -B build-wasm -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm --target MessageBox
Every example becomes a small static site in bin/wasm: <Example>.html + <Example>.mjs + <Example>.wasm,
next to appcui-host.js and JuliaMono-Regular.ttf. Browsers do not load WebAssembly from file:// URLs, so
the pages must be served over HTTP; bin/wasm/serve.py does that and opens an example in the browser:
python bin/wasm/serve.py MessageBox
Any other web server works too, as long as it serves .mjs as text/javascript and .wasm as
application/wasm. A page of your own boots an application with:
import createAppCUIModule from './MyApp.mjs';
import { startAppCUI } from './appcui-host.js';
await startAppCUI(createAppCUIModule, canvasElement, containerElement,
{ fontFamily: '"JuliaMono", monospace', fontSizeCss: 16 });
The size of containerElement decides the number of columns and rows (resizing it resizes the application).
Modal windows (
Window::Show,MessageBox,FileDialog, …) keep their synchronous API: while a modal window waits for input, the C++ stack is suspended and the browser keeps running. The default build suspends with Asyncify (every browser);-DAPPCUI_WASM_JSPI=ONuses JSPI instead (smaller and faster.wasm, requires a browser with JavaScript Promise Integration).With Asyncify,
Application::Run()does not return: the browser drives the event loop until the application closes and code placed afterRun()inmainis never executed. With JSPI,Run()returns normally.When the application exits (for example after its last window is closed), the page keeps the last frame, dimmed, under an “The application has exited” message with a Restart button. Pass
onExit: (host) => { ... }in the options ofstartAppCUIto handle the exit differently.The file system is Emscripten’s in-memory file system, separate from the user’s machine, and its files do not persist across page reloads. The application starts in the home folder (
/home/web_user).
Files of the user
The browser frontend moves files between the in-memory file system and the user’s machine:
FileDialoglists only real locations (Home, Temp,/) and adds two buttons:Upload opens the file picker of the browser. The picker suggests the extensions of the current file type filter. The picked files are copied into the folder shown by the dialog, and the first one is selected, so Enter opens it. The limit is 256 MB per file.
Download sends the selected file to the user as a browser download.
When a
ShowSaveFileWindowdialog returns a path, the file is downloaded automatically once the application has written it, typically in the same event handler. Existing applications therefore save to the user’s machine without any change.OS::IsHostFileTransferAvailable()tells whether the frontend works this way, andOS::DownloadFile(path)downloads any file explicitly, for example a log. On the other frontends the first returnsfalseand the second does nothing.Pass
onDownload: (name, data) => { ... }tostartAppCUIto handle the downloaded bytes in the page instead of starting a browser download.PNG / JPG images are not supported (
APPCUI_IMAGE_CODECSisOFF); BMP images are.
Keyboard on ncurses terminals
Terminals send many key combinations as escape sequences and some combinations are not sent at all. AppCUI recognizes:
Ctrl+<letter>,Ctrl+Space,Shift+Tab, the function keys and their Shift / Ctrl / Alt variants, and the modified navigation keys (arrows,Home,End,PageUp,PageDown,Insert,Delete) as described by the terminfo entry of the terminal (xterm-compatible terminals report all of them);Alt+<key>when the terminal sends Alt (Meta) as anEscapeprefix (on macOS enable “Use Option as Meta key” in Terminal / iTerm2);combo mode, for combinations the terminal can not send: press ` (backtick), toggle the modifiers with a (Alt), c (Ctrl) and s (Shift), then press a letter or a digit (
1-9,0are the function keysF1-F10). Space keeps combo mode active for several keys and Escape leaves it. The selected modifiers are shown on the screen while combo mode is active.
The Keyboard.Ctrl / Keyboard.Alt keys of the settings file (see Initialization) remap the modifiers
when a terminal or a keyboard layout makes some of them hard to use.
Custom frontends
An application can implement its own frontend (FrontendType::Custom): AppCUI then calls an object that implements
Application::CustomFrontendInterface instead of a terminal. This is how an application can run without a terminal
(tests, screenshots), stream its screen over the network or record a session.
class CustomFrontendInterface
{
public:
virtual bool OnInit(uint32& width, uint32& height) = 0; // choose the screen size
virtual void OnUnInit() = 0; // the application stops
virtual void OnFlushToScreen(const Graphics::Character* characters, uint32 width, uint32 height) = 0;
virtual void OnUpdateCursor(uint32 x, uint32 y, bool visible) = 0;
virtual bool WaitForEvent(FrontendEvent& evnt, uint32 timeoutMs) = 0; // false = no event (timeout)
virtual bool HasSupportFor(SpecialCharacterSetType type); // default: every set
};
AppCUI calls every method from its own (UI) thread.
WaitForEventmay be fed by other threads (for example a network thread that pushes events into a queue): it must return withintimeoutMsmilliseconds so that the frame updates ofEnableFPSModekeep running.OnFlushToScreenreceives the whole screen after every repaint (width * heightcharacters, row major); the pointer is only valid during the call.Events are
FrontendEventobjects: key presses (key code + Unicode character), shift state changes, mouse down / up / move / wheel, resize, close and redraw requests.The object is not owned by AppCUI and must outlive
Application::Run().The
FrontendandSizekeys of the settings file are ignored, and the clipboard is private to the process (a custom frontend never reads or writes the clipboard of the operating system).
A complete headless frontend that renders an application into memory and prints the screen:
// A headless frontend: AppCUI draws into memory instead of a terminal (useful for tests, screenshots or for
// streaming the screen somewhere else).
class HeadlessScreen : public CustomFrontendInterface
{
std::vector<Graphics::Character> screen;
uint32 width{ 0 };
uint32 height{ 0 };
bool closed{ false };
public:
bool OnInit(uint32& w, uint32& h) override
{
w = 60; // the screen size is decided by the frontend
h = 12;
return true;
}
void OnUnInit() override
{
}
void OnFlushToScreen(const Graphics::Character* characters, uint32 w, uint32 h) override
{
screen.assign(characters, characters + static_cast<size_t>(w) * h);
width = w;
height = h;
}
void OnUpdateCursor(uint32, uint32, bool) override
{
}
bool WaitForEvent(FrontendEvent& e, uint32) override
{
// a real frontend waits here for input (network, test script, ...); this one closes the application as
// soon as the first screen has been drawn
if (screen.empty() || closed)
return false;
e = FrontendEvent{};
e.Type = FrontendEventType::Closed;
closed = true;
return true;
}
std::string ToText() const
{
std::string text;
for (uint32 y = 0; y < height; y++)
{
for (uint32 x = 0; x < width; x++)
{
const char16 ch = screen[static_cast<size_t>(y) * width + x].Code;
if (ch < 0x80)
text.push_back(ch >= 0x20 ? static_cast<char>(ch) : ' ');
else if (ch < 0x800)
{
text.push_back(static_cast<char>(0xC0 | (ch >> 6)));
text.push_back(static_cast<char>(0x80 | (ch & 0x3F)));
}
else
{
text.push_back(static_cast<char>(0xE0 | (ch >> 12)));
text.push_back(static_cast<char>(0x80 | ((ch >> 6) & 0x3F)));
text.push_back(static_cast<char>(0x80 | (ch & 0x3F)));
}
}
text.push_back('\n');
}
return text;
}
};
HeadlessScreen headless; // must outlive the application
InitializationData initData;
initData.Frontend = FrontendType::Custom;
initData.CustomFrontend = &headless;
initData.SpecialCharacterSet = SpecialCharacterSetType::Ascii;
if (!Application::Init(initData))
return 1;
auto window = Controls::Factory::Window::Create("Headless", "d:c,w:40,h:6");
Controls::Factory::Label::Create(*window, "Rendered without a terminal", "x:1,y:1,w:36");
Application::AddWindow(std::move(window));
Application::Run(); // returns after the frontend sent FrontendEventType::Closed
std::cout << headless.ToText();
Output:
############################################################
############################################################
############################################################
##########+============ Headless =============[x]+##########
##########| |##########
##########| Rendered without a terminal |##########
##########| |##########
##########| |##########
##########+======================================+##########
############################################################
############################################################
############################################################