Move eterm emulation off the UI thread and add tabbed frontend

- introduce a per-terminal worker controller with ordered command queues,
    coalesced events, immutable snapshots, and deterministic shutdown
  - move PTY reads, parsing, history, selection, resize, reflow, and process
    status handling onto the terminal worker
  - publish terminal snapshots at the host refresh rate while preserving
    immediate idle, resize, interaction, and final-exit presentation
  - migrate TerminalDisplay, UITerminal, eterm, ecode, debugger, and ACP
    integrations away from direct mutable emulator access
  - synchronize scrollbar updates with acknowledged scroll commands
  - preserve title, clipboard, bell, prompt, color, restart, keep-alive,
    selection, input, and buffered process-exit behavior
  - add bounded synchronous selection requests and idempotent worker shutdown
  - make POSIX process launch safe after fork in multithreaded hosts and mark
    original PTY descriptors close-on-exec
  - add an UIApplication-based tabbed eterm with legacy CLI parity, tab
    lifecycle handling, close warnings, and default navigation shortcuts
  - add premake targets and deterministic controller, snapshot, lifecycle,
    concurrency, presentation, and exit-drain tests
This commit is contained in:
Martín Lucas Golini
2026-08-30 20:25:44 -03:00
parent 7c92b59e00
commit 8bcec85da0
21 changed files with 2274 additions and 402 deletions
@@ -0,0 +1,248 @@
#ifndef ETERM_TERMINALCONTROLLER_HPP
#define ETERM_TERMINALCONTROLLER_HPP
#include <eepp/math/vector2.hpp>
#include <eepp/window/keycodes.hpp>
#include <eterm/system/iprocess.hpp>
#include <eterm/terminal/ipseudoterminal.hpp>
#include <eterm/terminal/terminalemulator.hpp>
#include <eterm/terminal/terminaltypes.hpp>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <variant>
#include <vector>
using namespace EE;
using namespace EE::Math;
using namespace EE::Window;
using namespace eterm::System;
namespace eterm { namespace Terminal {
/** Immutable worker-to-UI presentation state. Cell selection is already applied as ATTR_REVERSE. */
struct TerminalSnapshot {
std::vector<TerminalGlyph> cells;
std::vector<Uint8> dirtyRows;
std::string title;
std::string currentWorkingDirectory;
std::string selection;
Uint64 generation{ 0 };
Uint64 lastAppliedScrollCommand{ 0 };
Vector2i cursor;
TerminalGlyph cursorGlyph;
int columns{ 0 };
int rows{ 0 };
int historyLength{ 0 };
int scrollPosition{ 0 };
int windowMode{ MODE_VISIBLE | MODE_FOCUSED };
int processId{ 0 };
int exitCode{ 0 };
Uint32 presentationRate{ 60 };
TerminalCursorMode cursorMode{ SteadyUnderline };
TerminalSelectionMode selectionMode{ SEL_IDLE };
PromptState promptState{ PromptState::Unknown };
bool cursorVisible{ false };
bool cursorSelected{ false };
bool hasSelection{ false };
bool altScreen{ false };
bool processExited{ false };
/** Dirty rows describe only the transition from the immediately preceding generation. */
bool dirtyRowsFollow( Uint64 previousGeneration ) const {
return generation == previousGeneration + 1;
}
};
struct TerminalColorPalette {
std::vector<Uint32> colors;
Uint32 cursor{ 0 };
Uint32 foreground{ 0 };
Uint32 background{ 0 };
};
/**
* Per-terminal worker/controller.
*
* After create() returns, the worker thread exclusively owns the emulator, PTY, process, parser,
* history, selection, and cursor. UI code may only enqueue commands, drain events, or retain an
* immutable snapshot returned by snapshot(). No controller callback is invoked by the worker.
*/
class TerminalController final : public std::enable_shared_from_this<TerminalController> {
public:
using PtyPtr = std::unique_ptr<IPseudoTerminal>;
using ProcPtr = std::unique_ptr<IProcess>;
enum class EventType : Uint8 {
Title,
IconTitle,
HistoryLength,
ScrollPosition,
Bell,
Clipboard,
ProcessExit,
RestartFailure,
SnapshotReady,
Data,
PromptState,
Color,
Error
};
struct Event {
EventType type{ EventType::Error };
std::string data;
Uint64 generation{ 0 };
int value{ 0 };
PromptState promptState{ PromptState::Unknown };
};
static std::shared_ptr<TerminalController> create( PtyPtr&& pty, ProcPtr&& process,
size_t historySize,
TerminalColorPalette palette = {} );
~TerminalController();
TerminalController( const TerminalController& ) = delete;
TerminalController( TerminalController&& ) = delete;
TerminalController& operator=( const TerminalController& ) = delete;
TerminalController& operator=( TerminalController&& ) = delete;
void write( std::string data, bool mayEcho = true );
void writeRaw( std::string data );
void resize( int columns, int rows );
void scrollUp( int amount );
void scrollDown( int amount );
/** Returns an ordered command id that is copied into snapshots after the scroll is applied. */
Uint64 scrollTo( int position );
void selectionStart( int column, int row, int snap );
void selectionExtend( int column, int row, int type, bool done );
void selectionClear();
void mouseReport( TerminalMouseEventType type, Vector2i position, Uint32 flags,
Uint32 modifiers );
void setFocus( bool focus );
void setCursorMode( TerminalCursorMode mode );
void setColorPalette( TerminalColorPalette palette );
void setAllowMemoryTrimming( bool allow );
void setPresentationRate( Uint32 framesPerSecond );
void setDataEventsEnabled( bool enabled );
void setPromptEventsEnabled( bool enabled );
void reset();
void terminate();
void restart( PtyPtr&& pty, ProcPtr&& process );
void shutdown();
std::shared_ptr<const TerminalSnapshot> snapshot() const;
std::vector<Event> drainEvents();
/** Bounded exact-selection request. Returns no value on timeout or during shutdown. */
std::optional<std::string>
requestSelection( std::chrono::milliseconds timeout = std::chrono::milliseconds( 50 ) );
private:
class WorkerDisplay;
struct SelectionResponse;
struct WriteCommand {
std::string data;
bool mayEcho{ true };
};
struct WriteRawCommand {
std::string data;
};
struct ResizeCommand {
int columns{ 0 };
int rows{ 0 };
};
struct ScrollCommand {
int amount{ 0 };
int direction{ 0 };
Uint64 commandId{ 0 };
};
struct SelectionStartCommand {
int column{ 0 };
int row{ 0 };
int snap{ 0 };
};
struct SelectionExtendCommand {
int column{ 0 };
int row{ 0 };
int type{ 0 };
bool done{ false };
};
struct MouseCommand {
TerminalMouseEventType type{ TerminalMouseEventType::MouseMotion };
Vector2i position;
Uint32 flags{ 0 };
Uint32 modifiers{ 0 };
};
struct BoolCommand {
bool value{ false };
};
struct CursorModeCommand {
TerminalCursorMode mode{ SteadyUnderline };
};
struct PaletteCommand {
TerminalColorPalette palette;
};
struct PresentationRateCommand {
Uint32 framesPerSecond{ 60 };
};
struct RestartCommand {
PtyPtr pty;
ProcPtr process;
};
struct SelectionRequestCommand {
std::shared_ptr<SelectionResponse> response;
};
struct SelectionClearCommand {};
struct ResetCommand {};
struct TerminateCommand {};
struct AllowTrimCommand : BoolCommand {};
struct DataEventsCommand : BoolCommand {};
struct PromptEventsCommand : BoolCommand {};
struct FocusCommand : BoolCommand {};
using Command =
std::variant<WriteCommand, WriteRawCommand, ResizeCommand, ScrollCommand,
SelectionStartCommand, SelectionExtendCommand, SelectionClearCommand,
MouseCommand, FocusCommand, CursorModeCommand, PaletteCommand,
PresentationRateCommand, AllowTrimCommand, DataEventsCommand,
PromptEventsCommand, TerminateCommand, RestartCommand, ResetCommand,
SelectionRequestCommand>;
TerminalController( PtyPtr&& pty, ProcPtr&& process, size_t historySize,
TerminalColorPalette palette );
void start();
void enqueue( Command&& command );
void workerLoop();
void processCommands();
void processCommand( Command&& command );
void enqueueEvent( Event event, bool coalescable );
void publishSnapshot( std::shared_ptr<const TerminalSnapshot> snapshot );
std::shared_ptr<WorkerDisplay> mWorkerDisplay;
std::unique_ptr<TerminalEmulator> mEmulator;
std::thread mWorker;
std::mutex mShutdownMutex;
mutable std::mutex mCommandMutex;
std::condition_variable mCommandCondition;
std::deque<Command> mCommands;
std::mutex mEventMutex;
std::deque<Event> mEvents;
std::atomic<std::shared_ptr<const TerminalSnapshot>> mPublishedSnapshot;
std::atomic<bool> mShutdownRequested{ false };
std::atomic<Uint64> mNextScrollCommand{ 0 };
};
}} // namespace eterm::Terminal
#endif
@@ -11,9 +11,8 @@
#include <eepp/window/keycodes.hpp>
#include <eepp/window/window.hpp>
#include <eterm/system/iprocessfactory.hpp>
#include <eterm/terminal/iterminaldisplay.hpp>
#include <eterm/terminal/terminalcolorscheme.hpp>
#include <eterm/terminal/terminalemulator.hpp>
#include <eterm/terminal/terminalcontroller.hpp>
#include <memory>
#include <unordered_map>
@@ -128,7 +127,7 @@ class TerminalKeyMap {
extern TerminalKeyMap terminalKeyMap;
class TerminalDisplay : public ITerminalDisplay {
class TerminalDisplay {
public:
enum class EventType {
TITLE,
@@ -136,6 +135,10 @@ class TerminalDisplay : public ITerminalDisplay {
SCROLL_HISTORY,
HISTORY_LENGTH_CHANGE,
PROCESS_EXIT,
BELL,
CLIPBOARD,
RESTART_FAILURE,
WORKER_ERROR,
UNKNOWN
};
@@ -145,11 +148,8 @@ class TerminalDisplay : public ITerminalDisplay {
};
typedef std::function<void( const TerminalDisplay::Event& event )> EventFunc;
static std::shared_ptr<TerminalDisplay>
create( EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize,
std::shared_ptr<TerminalEmulator>&& terminalEmulator,
const bool& useFrameBuffer = false );
using DataFunc = std::function<void( const char*, size_t )>;
using PromptStateChangedFunc = std::function<void( PromptState, std::string_view )>;
static std::shared_ptr<TerminalDisplay>
create( EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize,
@@ -158,25 +158,16 @@ class TerminalDisplay : public ITerminalDisplay {
IProcessFactory* processFactory = nullptr, bool useFrameBuffer = false,
bool keepAlive = true, const std::unordered_map<std::string, std::string>& env = {} );
virtual ~TerminalDisplay();
~TerminalDisplay();
virtual void resetColors();
virtual int resetColor( const Uint32& index, const char* name );
virtual bool getColor( const Uint32& index, unsigned char* r, unsigned char* g,
unsigned char* b );
void resetColors();
int resetColor( const Uint32& index, const char* name );
bool getColor( const Uint32& index, unsigned char* r, unsigned char* g, unsigned char* b );
virtual void setTitle( const char* title );
virtual void setIconTitle( const char* title );
void setClipboard( const char* text );
const char* getClipboard() const;
virtual void setClipboard( const char* text );
virtual const char* getClipboard() const;
virtual bool drawBegin( Uint32 columns, Uint32 rows );
virtual void drawLine( Line line, int x1, int y, int x2 );
virtual void drawCursor( int cx, int cy, TerminalGlyph g, int ox, int oy, TerminalGlyph og );
virtual void drawEnd();
virtual bool update( bool isMouseOverMe = true );
bool update( bool isMouseOverMe = true );
void executeFile( const std::string& cmd );
@@ -188,20 +179,20 @@ class TerminalDisplay : public ITerminalDisplay {
void draw();
virtual void onMouseDoubleClick( const Vector2i& pos, const Uint32& flags );
void onMouseDoubleClick( const Vector2i& pos, const Uint32& flags );
virtual void onMouseMove( const Vector2i& pos, const Uint32& flags );
void onMouseMove( const Vector2i& pos, const Uint32& flags );
virtual void onMouseDown( const Vector2i& pos, const Uint32& flags );
void onMouseDown( const Vector2i& pos, const Uint32& flags );
virtual void onMouseUp( const Vector2i& pos, const Uint32& flags );
void onMouseUp( const Vector2i& pos, const Uint32& flags );
virtual void onTextInput( const Uint32& chr );
void onTextInput( const Uint32& chr );
virtual void onTextEditing( const String& text, const Int32& start, const Int32& length );
void onTextEditing( const String& text, const Int32& start, const Int32& length );
virtual void onKeyDown( const Keycode& keyCode, const Uint32& chr, const Uint32& mod,
const Scancode& scancode );
void onKeyDown( const Keycode& keyCode, const Uint32& chr, const Uint32& mod,
const Scancode& scancode );
bool isRegisteredShortcut( const Keycode& keyCode, const Uint32& mod ) const;
@@ -251,14 +242,40 @@ class TerminalDisplay : public ITerminalDisplay {
void setPadding( const Rectf& padding );
const std::shared_ptr<TerminalEmulator>& getTerminal() const;
const std::shared_ptr<TerminalController>& getController() const;
virtual void attach( TerminalEmulator* terminal );
std::string getSelection();
bool hasSelection() const;
TerminalSelectionMode getSelectionMode() const;
int getProcessId() const;
int getExitCode() const;
void terminate();
void setAllowMemoryTrimming( bool allow );
void setDataCallback( DataFunc callback );
void setPromptStateChangedCallback( PromptStateChangedFunc callback );
void setCursorMode( TerminalCursorMode mode );
TerminalCursorMode getCursorMode() const;
int scrollSize() const;
int rowCount() const;
int scrollPosition() const;
Uint64 scrollTo( int position );
Uint64 lastAppliedScrollCommand() const;
Uint32 pushEventCallback( const EventFunc& func );
void popEventCallback( const Uint32& id );
@@ -295,12 +312,14 @@ class TerminalDisplay : public ITerminalDisplay {
protected:
EE::Window::Window* mWindow;
std::vector<TerminalGlyph> mBuffer;
std::vector<Color> mColors;
std::shared_ptr<TerminalEmulator> mTerminal;
std::shared_ptr<TerminalController> mController;
std::shared_ptr<const TerminalSnapshot> mSnapshot;
mutable std::string mClipboardUtf8;
Uint32 mNumCallBacks;
Uint32 mNumCallBacks{ 0 };
std::map<Uint32, EventFunc> mCallbacks;
DataFunc mDataCallback;
PromptStateChangedFunc mPromptStateChangedCallback;
Font* mFont{ nullptr };
Float mFontSize{ 12 };
@@ -322,11 +341,14 @@ class TerminalDisplay : public ITerminalDisplay {
bool mAlreadyClickedMButton{ false };
bool mKeepAlive{ true };
bool mDraggingSel{ false };
int mMode{ MODE_VISIBLE | MODE_FOCUSED };
TerminalCursorMode mCursorMode{ SteadyUnderline };
Clock mClock;
Clock mLastDoubleClick;
Uint32 mColumns{ 0 };
Uint32 mRows{ 0 };
Uint32 mClickStep{ 5 };
Uint64 mSnapshotGeneration{ 0 };
FontHinting mFontHinting{ FontHinting::Full };
FontAntialiasing mFontAntialiasing{ FontAntialiasing::Grayscale };
FrameBufferUniquePtr mFrameBuffer;
@@ -356,9 +378,13 @@ class TerminalDisplay : public ITerminalDisplay {
void onSizeChange();
virtual void onProcessExit( int exitCode );
void onProcessExit( int exitCode );
virtual void onScrollPositionChange();
void consumeSnapshot();
void drainControllerEvents();
TerminalColorPalette makeColorPalette() const;
void sendEvent( const TerminalDisplay::Event& event );
@@ -155,6 +155,9 @@ class TerminalEmulator final {
void redraw();
/** Worker-owned terminal state reset (RIS semantics without replacing the PTY/process). */
void reset();
void logError( const char* err );
/** @return If the tty read was completed or there's still buffer to read (true completed) */
@@ -245,6 +248,9 @@ class TerminalEmulator final {
void setAllowMemoryTrimnming( bool allowMemoryTrimnming );
/** Worker-only maximum interval between snapshots while PTY reads remain saturated. */
void setPresentationInterval( Time interval );
Vector2i getSize() const;
System::IProcess* getProcess() const;
@@ -277,8 +283,8 @@ class TerminalEmulator final {
bool mDirty{ true };
bool mAllDirty{ true };
uint8_t mDeferredPresentationBatches{ 0 };
Clock mPresentationClock;
Time mPresentationInterval{ Microseconds( 1000000.0 / 60.0 ) };
bool mAllowMemoryTrimnming{ false };
int mExitCode;
@@ -301,6 +307,7 @@ class TerminalEmulator final {
int mAllowWindowOps;
std::string mCurrentWorkingDirectory;
Vector2i mLastMousePosition{ -1, -1 };
PromptState mPromptState{ PromptState::Unknown };
PromptStateChangedCb mPromptStateChangedCb;
DataCb mDataCb;
@@ -128,10 +128,13 @@ class UITerminal : public UIWidget {
ScrollViewType mViewType{ ScrollViewType::Overlay };
ScrollBarMode mVScrollMode{ ScrollBarMode::Auto };
UIScrollBar* mVScroll{ nullptr };
int mScrollOffset;
int mScrollOffset{ 0 };
bool mScrollByBar{ false };
bool mPendingContentSizeChange{ false };
Uint64 mPendingScrollCommand{ 0 };
Clock mMouseClock;
std::shared_ptr<TerminalDisplay> mTerm;
Uint32 mTerminalEventCallbackId{ 0 };
UITerminal( const std::shared_ptr<TerminalDisplay>& terminalDisplay );
+96 -61
View File
@@ -23,6 +23,7 @@
#ifndef _WIN32
#include <eepp/system/filesystem.hpp>
#include <eepp/system/log.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/version.hpp>
#include <eterm/system/process.hpp>
#include <poll.h>
@@ -109,54 +110,83 @@ int Process::getExitCode() const {
Process::Process( int pid ) : mPID( pid ) {}
static void execshell( const char* cmd, const char* const* args, std::string workingDirectory,
const std::unordered_map<std::string, std::string>& env ) {
const struct passwd* pw;
const char* sh;
struct ProcessLaunchData {
std::string executable;
std::string workingDirectory;
std::vector<char*> arguments;
std::vector<std::string> environmentStorage;
std::vector<char*> environment;
};
errno = 0;
if ( ( pw = getpwuid( getuid() ) ) == NULL ) {
if ( errno ) {
fprintf( stderr, "getpwuid: %s\n", strerror( errno ) );
_exit( 1 );
} else {
static bool prepareProcessLaunch( const std::string& program, const std::vector<std::string>& args,
const std::string& workingDirectory,
const std::unordered_map<std::string, std::string>& env,
ProcessLaunchData& launch ) {
long passwordBufferSize = sysconf( _SC_GETPW_R_SIZE_MAX );
if ( passwordBufferSize < 0 )
passwordBufferSize = 16384;
std::vector<char> passwordBuffer( static_cast<size_t>( passwordBufferSize ) );
struct passwd passwordData;
struct passwd* passwordResult = nullptr;
const int passwordError = getpwuid_r( getuid(), &passwordData, passwordBuffer.data(),
passwordBuffer.size(), &passwordResult );
if ( passwordError || !passwordResult ) {
if ( passwordError )
fprintf( stderr, "getpwuid_r: %s\n", strerror( passwordError ) );
else
fprintf( stderr, "who are you?\n" );
_exit( 1 );
}
return false;
}
const struct passwd* pw = passwordResult;
if ( ( sh = getenv( "SHELL" ) ) == NULL )
sh = ( pw->pw_shell[0] ) ? pw->pw_shell : cmd;
launch.executable = Sys::which( program );
if ( launch.executable.empty() )
launch.executable = program;
launch.workingDirectory = workingDirectory.empty() ? pw->pw_dir : workingDirectory;
if ( workingDirectory.empty() )
workingDirectory = pw->pw_dir;
launch.arguments.reserve( args.size() + 2 );
launch.arguments.emplace_back( const_cast<char*>( program.c_str() ) );
for ( const auto& argument : args )
launch.arguments.emplace_back( const_cast<char*>( argument.c_str() ) );
launch.arguments.emplace_back( nullptr );
FileSystem::changeWorkingDirectory( workingDirectory );
auto environment = Sys::getEnvironmentVariables();
environment.erase( "COLUMNS" );
environment.erase( "LINES" );
environment.erase( "TERMCAP" );
environment["LOGNAME"] = pw->pw_name;
environment["USER"] = pw->pw_name;
const char* shell = getenv( "SHELL" );
environment["SHELL"] = shell ? shell : ( pw->pw_shell[0] ? pw->pw_shell : program );
environment["HOME"] = pw->pw_dir;
environment["TERM"] = "xterm-256color";
environment["TERM_PROGRAM"] = "eterm";
environment["TERM_PROGRAM_VERSION"] = EE::Version::getVersionName( false );
environment["COLORTERM"] = "24bit";
for ( const auto& entry : env )
environment[entry.first] = entry.second;
unsetenv( "COLUMNS" );
unsetenv( "LINES" );
unsetenv( "TERMCAP" );
setenv( "LOGNAME", pw->pw_name, 1 );
setenv( "USER", pw->pw_name, 1 );
setenv( "SHELL", sh, 1 );
setenv( "HOME", pw->pw_dir, 1 );
setenv( "TERM", "xterm-256color", 1 );
setenv( "TERM_PROGRAM", "eterm", 1 );
setenv( "TERM_PROGRAM_VERSION", EE::Version::getVersionName( false ).c_str(), 1 );
setenv( "COLORTERM", "24bit", 1 );
launch.environmentStorage.reserve( environment.size() );
for ( const auto& entry : environment )
launch.environmentStorage.emplace_back( entry.first + "=" + entry.second );
launch.environment.reserve( launch.environmentStorage.size() + 1 );
for ( auto& entry : launch.environmentStorage )
launch.environment.emplace_back( entry.data() );
launch.environment.emplace_back( nullptr );
return true;
}
for ( const auto& e : env )
setenv( e.first.c_str(), e.second.c_str(), 1 );
signal( SIGCHLD, SIG_DFL );
signal( SIGHUP, SIG_DFL );
signal( SIGINT, SIG_DFL );
signal( SIGQUIT, SIG_DFL );
signal( SIGTERM, SIG_DFL );
signal( SIGALRM, SIG_DFL );
execvp( cmd, (char* const*)args );
_exit( 1 );
static void resetSignalHandlers() {
struct sigaction action;
memset( &action, 0, sizeof( action ) );
action.sa_handler = SIG_DFL;
sigemptyset( &action.sa_mask );
sigaction( SIGCHLD, &action, nullptr );
sigaction( SIGHUP, &action, nullptr );
sigaction( SIGINT, &action, nullptr );
sigaction( SIGQUIT, &action, nullptr );
sigaction( SIGTERM, &action, nullptr );
sigaction( SIGALRM, &action, nullptr );
}
std::unique_ptr<Process> Process::createWithPipe( const std::string& /*program*/,
@@ -173,37 +203,42 @@ Process::createWithPseudoTerminal( const std::string& program, const std::vector
const std::string& workingDirectory,
Terminal::PseudoTerminal& pseudoTerminal,
const std::unordered_map<std::string, std::string>& env ) {
// The calling process may already contain other threads. Prepare every allocation and libc
// lookup before fork: the child may safely call only async-signal-safe operations until exec
// replaces the inherited multithreaded process image.
ProcessLaunchData launch;
if ( !prepareProcessLaunch( program, args, workingDirectory, env, launch ) )
return nullptr;
int pid = fork();
if ( pid == -1 ) {
fprintf( stderr, "Failed to fork process\n" );
return nullptr;
} else if ( pid == 0 ) {
setsid();
dup2( (int)pseudoTerminal.mSlave, 0 );
dup2( (int)pseudoTerminal.mSlave, 1 );
dup2( (int)pseudoTerminal.mSlave, 2 );
if ( ioctl( (int)pseudoTerminal.mSlave, TIOCSCTTY, NULL ) < 0 ) {
fprintf( stderr, "ioctl TIOCSCTTY failed: %s", strerror( errno ) );
exit( 1 );
const int master = (int)pseudoTerminal.mMaster;
const int slave = (int)pseudoTerminal.mSlave;
if ( setsid() < 0 || dup2( slave, STDIN_FILENO ) < 0 || dup2( slave, STDOUT_FILENO ) < 0 ||
dup2( slave, STDERR_FILENO ) < 0 ) {
_exit( 1 );
}
if ( (int)pseudoTerminal.mSlave > 2 )
close( (int)pseudoTerminal.mSlave );
if ( ioctl( slave, TIOCSCTTY, NULL ) < 0 )
_exit( 1 );
if ( slave > STDERR_FILENO )
close( slave );
if ( master > STDERR_FILENO )
close( master );
if ( chdir( launch.workingDirectory.c_str() ) < 0 )
_exit( 1 );
#ifdef __OpenBSD__
if ( pledge( "stdio getpw proc exec", NULL ) == -1 ) {
fprintf( stderr, "pledge\n" );
exit( 1 );
}
if ( pledge( "stdio proc exec", NULL ) == -1 )
_exit( 1 );
#endif
std::vector<const char*> argsV;
argsV.push_back( program.c_str() );
for ( auto& a : args ) {
argsV.push_back( a.c_str() );
}
argsV.push_back( nullptr );
execshell( program.c_str(), argsV.data(), workingDirectory, env );
resetSignalHandlers();
execve( launch.executable.c_str(), launch.arguments.data(), launch.environment.data() );
_exit( 1 );
} else {
pseudoTerminal.mSlave.release();
return std::unique_ptr<Process>( new Process( pid ) );
@@ -116,7 +116,8 @@ bool PseudoTerminal::resize( int columns, int rows ) {
w.ws_ypixel = 0;
bool masterResized = ioctl( (int)mMaster, TIOCSWINSZ, &w ) >= 0;
bool slaveResized = mSlave.handle() != -1 ? ioctl( mSlave.handle(), TIOCSWINSZ, &w ) >= 0 : false;
bool slaveResized =
mSlave.handle() != -1 ? ioctl( mSlave.handle(), TIOCSWINSZ, &w ) >= 0 : false;
if ( !masterResized && !slaveResized ) {
perror( "PseudoTerminal::Resize" );
@@ -226,6 +227,17 @@ std::unique_ptr<PseudoTerminal> PseudoTerminal::create( int columns, int rows )
return nullptr;
}
// The selected slave is duplicated onto standard input/output/error before exec. The original
// PTY descriptors must not otherwise survive exec or leak into unrelated child processes.
const int masterDescriptorFlags = fcntl( master, F_GETFD );
const int slaveDescriptorFlags = fcntl( slave, F_GETFD );
if ( masterDescriptorFlags < 0 || slaveDescriptorFlags < 0 ||
fcntl( master, F_SETFD, masterDescriptorFlags | FD_CLOEXEC ) < 0 ||
fcntl( slave, F_SETFD, slaveDescriptorFlags | FD_CLOEXEC ) < 0 ) {
perror( "PseudoTerminal::create(fcntl FD_CLOEXEC)" );
Log::error( "PseudoTerminal::create(fcntl FD_CLOEXEC)" );
return nullptr;
}
int flags = fcntl( master, F_GETFL, 0 );
fcntl( master, F_SETFL, flags | O_NONBLOCK );
@@ -0,0 +1,555 @@
#include <eterm/terminal/terminalcontroller.hpp>
#include <eepp/system/color.hpp>
#include <eepp/system/log.hpp>
#include <algorithm>
#include <cstring>
using namespace EE::System;
namespace eterm { namespace Terminal {
struct TerminalController::SelectionResponse {
std::mutex mutex;
std::condition_variable condition;
std::string selection;
bool ready{ false };
};
class TerminalController::WorkerDisplay final : public ITerminalDisplay {
public:
WorkerDisplay( TerminalController& controller, TerminalColorPalette palette ) :
mController( controller ),
mInitialPalette( std::move( palette ) ),
mPalette( mInitialPalette ) {
mMode |= MODE_FOCUSED;
}
bool drawBegin( Uint32 columns, Uint32 rows ) {
const size_t cellCount = static_cast<size_t>( columns ) * rows;
if ( columns != static_cast<Uint32>( mColumns ) || rows != static_cast<Uint32>( mRows ) ) {
mColumns = columns;
mRows = rows;
mCells.assign( cellCount, TerminalGlyph{} );
mDirtyRows.assign( rows, 1 );
} else {
mDirtyRows.assign( rows, 0 );
}
mCursorVisible = false;
return getMode( MODE_VISIBLE );
}
void drawLine( Line line, int x1, int y, int x2 ) {
if ( y < 0 || y >= mRows || x1 < 0 || x2 > mColumns || x1 >= x2 )
return;
TerminalGlyph* destination = mCells.data() + static_cast<size_t>( y ) * mColumns + x1;
std::memcpy( destination, line + x1, static_cast<size_t>( x2 - x1 ) * sizeof( *line ) );
if ( mEmulator ) {
for ( int column = x1; column < x2; ++column ) {
if ( mEmulator->isSelected( column, y ) )
mCells[static_cast<size_t>( y ) * mColumns + column].mode |= ATTR_REVERSE;
}
}
mDirtyRows[y] = 1;
}
void drawCursor( int cx, int cy, TerminalGlyph glyph, int, int, TerminalGlyph ) {
mCursor = { cx, cy };
mCursorGlyph = glyph;
mCursorVisible = true;
}
void drawEnd() {
auto snapshot = std::make_shared<TerminalSnapshot>();
snapshot->cells = mCells;
snapshot->dirtyRows = mDirtyRows;
snapshot->title = mTitle;
snapshot->generation = ++mGeneration;
snapshot->lastAppliedScrollCommand = mLastAppliedScrollCommand;
snapshot->cursor = mCursor;
snapshot->cursorGlyph = mCursorGlyph;
snapshot->columns = mColumns;
snapshot->rows = mRows;
snapshot->windowMode = mMode;
snapshot->presentationRate = mPresentationRate;
snapshot->cursorMode = mCursorMode;
snapshot->cursorVisible = mCursorVisible;
if ( mEmulator ) {
snapshot->historyLength = mEmulator->scrollSize();
snapshot->scrollPosition = mEmulator->scrollPos();
snapshot->hasSelection = mEmulator->hasSelection();
snapshot->selectionMode = mEmulator->getSelectionMode();
snapshot->cursorSelected =
snapshot->cursorVisible && mEmulator->isSelected( mCursor.x, mCursor.y );
if ( snapshot->hasSelection )
snapshot->selection = mEmulator->getSelection();
snapshot->altScreen = mEmulator->tisaltscr();
snapshot->processExited = mEmulator->hasExited();
snapshot->exitCode = mEmulator->getExitCode();
snapshot->currentWorkingDirectory = mEmulator->getCurrentWorkingDirectory();
snapshot->promptState = mEmulator->getPromptState();
if ( auto* process = mEmulator->getProcess() )
snapshot->processId = process->pid();
}
if ( snapshot->historyLength != mLastHistoryLength ) {
mLastHistoryLength = snapshot->historyLength;
Event event{ EventType::HistoryLength };
event.value = mLastHistoryLength;
mController.enqueueEvent( std::move( event ), true );
}
mController.publishSnapshot( std::move( snapshot ) );
}
void bell() { mController.enqueueEvent( { EventType::Bell }, false ); }
void resetColors() {
mPalette = mInitialPalette;
Event event{ EventType::Color };
event.value = -1;
mController.enqueueEvent( std::move( event ), false );
}
int resetColor( const Uint32& index, const char* name ) {
Uint32 color = 0;
bool parsed = false;
if ( name && String::startsWith( name, "rgb:" ) ) {
auto components = String::split( std::string( name + 4 ), '/' );
if ( components.size() == 3 ) {
char* ends[3]{};
long rgb[3]{};
for ( size_t i = 0; i < 3; ++i )
rgb[i] = std::strtol( components[i].c_str(), &ends[i], 16 );
if ( ends[0] && ends[1] && ends[2] ) {
color = Color( rgb[0], rgb[1], rgb[2] ).getValue();
parsed = true;
}
}
} else if ( name && Color::isColorString( std::string_view{ name }, true ) ) {
color = Color::fromString( name ).getValue();
parsed = true;
} else if ( !name || String::iequals( "default", name ) ) {
if ( index < mInitialPalette.colors.size() ) {
color = mInitialPalette.colors[index];
parsed = true;
} else if ( index == 256 || index == 257 ) {
color = mInitialPalette.cursor;
parsed = true;
} else if ( index == 258 ) {
color = mInitialPalette.foreground;
parsed = true;
} else if ( index == 259 ) {
color = mInitialPalette.background;
parsed = true;
}
}
if ( !parsed )
return 1;
setPaletteColor( index, color );
Event event{ EventType::Color };
event.data = name ? name : "";
event.value = static_cast<int>( index );
mController.enqueueEvent( std::move( event ), false );
return 0;
}
bool getColor( const Uint32& index, unsigned char* red, unsigned char* green,
unsigned char* blue ) {
Uint32 color = 0;
if ( index < mPalette.colors.size() )
color = mPalette.colors[index];
else if ( index == 256 || index == 257 )
color = mPalette.cursor;
else if ( index == 258 )
color = mPalette.foreground;
else if ( index == 259 )
color = mPalette.background;
else
return false;
*red = ( color >> 24 ) & 0xFF;
*green = ( color >> 16 ) & 0xFF;
*blue = ( color >> 8 ) & 0xFF;
return true;
}
void setTitle( const char* title ) {
mTitle = title ? title : "";
mController.enqueueEvent( { EventType::Title, mTitle }, true );
}
void setIconTitle( const char* title ) {
mController.enqueueEvent( { EventType::IconTitle, title ? title : "" }, true );
}
void setClipboard( const char* text ) {
if ( text )
mController.enqueueEvent( { EventType::Clipboard, text }, false );
}
void onProcessExit( int exitCode ) {
Event event{ EventType::ProcessExit };
event.value = exitCode;
mController.enqueueEvent( std::move( event ), false );
}
void onScrollPositionChange() {
mController.enqueueEvent( { EventType::ScrollPosition }, true );
}
void setPalette( TerminalColorPalette palette ) {
mInitialPalette = palette;
mPalette = std::move( palette );
}
void setFocused( bool focused ) {
if ( focused )
mMode |= MODE_FOCUSED;
else
mMode &= ~MODE_FOCUSED;
}
void setLastAppliedScrollCommand( Uint64 commandId ) { mLastAppliedScrollCommand = commandId; }
void setPresentationRate( Uint32 framesPerSecond ) { mPresentationRate = framesPerSecond; }
private:
void setPaletteColor( Uint32 index, Uint32 color ) {
if ( index < mPalette.colors.size() )
mPalette.colors[index] = color;
else if ( index == 256 || index == 257 )
mPalette.cursor = color;
else if ( index == 258 )
mPalette.foreground = color;
else if ( index == 259 )
mPalette.background = color;
}
TerminalController& mController;
TerminalColorPalette mInitialPalette;
TerminalColorPalette mPalette;
std::vector<TerminalGlyph> mCells;
std::vector<Uint8> mDirtyRows;
std::string mTitle;
Uint64 mGeneration{ 0 };
Uint64 mLastAppliedScrollCommand{ 0 };
Vector2i mCursor;
TerminalGlyph mCursorGlyph;
int mColumns{ 0 };
int mRows{ 0 };
int mLastHistoryLength{ -1 };
Uint32 mPresentationRate{ 60 };
bool mCursorVisible{ false };
};
std::shared_ptr<TerminalController> TerminalController::create( PtyPtr&& pty, ProcPtr&& process,
size_t historySize,
TerminalColorPalette palette ) {
if ( !pty || !process )
return nullptr;
auto controller = std::shared_ptr<TerminalController>( new TerminalController(
std::move( pty ), std::move( process ), historySize, std::move( palette ) ) );
controller->start();
return controller;
}
TerminalController::TerminalController( PtyPtr&& pty, ProcPtr&& process, size_t historySize,
TerminalColorPalette palette ) {
mWorkerDisplay = std::make_shared<WorkerDisplay>( *this, std::move( palette ) );
mEmulator = TerminalEmulator::create( std::move( pty ), std::move( process ), mWorkerDisplay,
historySize );
// Establish a complete generation before the controller becomes concurrently visible.
mEmulator->redraw();
}
TerminalController::~TerminalController() {
shutdown();
}
void TerminalController::start() {
mWorker = std::thread( [this] { workerLoop(); } );
}
void TerminalController::shutdown() {
std::lock_guard<std::mutex> shutdownLock( mShutdownMutex );
if ( !mShutdownRequested.exchange( true, std::memory_order_acq_rel ) )
mCommandCondition.notify_all();
if ( mWorker.joinable() && mWorker.get_id() != std::this_thread::get_id() )
mWorker.join();
}
void TerminalController::enqueue( Command&& command ) {
if ( mShutdownRequested.load( std::memory_order_acquire ) )
return;
{
std::lock_guard<std::mutex> lock( mCommandMutex );
if ( mShutdownRequested.load( std::memory_order_relaxed ) )
return;
mCommands.emplace_back( std::move( command ) );
}
mCommandCondition.notify_one();
}
void TerminalController::write( std::string data, bool mayEcho ) {
enqueue( WriteCommand{ std::move( data ), mayEcho } );
}
void TerminalController::writeRaw( std::string data ) {
enqueue( WriteRawCommand{ std::move( data ) } );
}
void TerminalController::resize( int columns, int rows ) {
enqueue( ResizeCommand{ columns, rows } );
}
void TerminalController::scrollUp( int amount ) {
enqueue( ScrollCommand{ amount, -1 } );
}
void TerminalController::scrollDown( int amount ) {
enqueue( ScrollCommand{ amount, 1 } );
}
Uint64 TerminalController::scrollTo( int position ) {
if ( mShutdownRequested.load( std::memory_order_acquire ) )
return 0;
const Uint64 commandId = mNextScrollCommand.fetch_add( 1, std::memory_order_relaxed ) + 1;
enqueue( ScrollCommand{ position, 0, commandId } );
return commandId;
}
void TerminalController::selectionStart( int column, int row, int snap ) {
enqueue( SelectionStartCommand{ column, row, snap } );
}
void TerminalController::selectionExtend( int column, int row, int type, bool done ) {
enqueue( SelectionExtendCommand{ column, row, type, done } );
}
void TerminalController::selectionClear() {
enqueue( SelectionClearCommand{} );
}
void TerminalController::mouseReport( TerminalMouseEventType type, Vector2i position, Uint32 flags,
Uint32 modifiers ) {
enqueue( MouseCommand{ type, position, flags, modifiers } );
}
void TerminalController::setFocus( bool focus ) {
enqueue( FocusCommand{ { focus } } );
}
void TerminalController::setCursorMode( TerminalCursorMode mode ) {
enqueue( CursorModeCommand{ mode } );
}
void TerminalController::setColorPalette( TerminalColorPalette palette ) {
enqueue( PaletteCommand{ std::move( palette ) } );
}
void TerminalController::setAllowMemoryTrimming( bool allow ) {
enqueue( AllowTrimCommand{ { allow } } );
}
void TerminalController::setPresentationRate( Uint32 framesPerSecond ) {
enqueue( PresentationRateCommand{ framesPerSecond } );
}
void TerminalController::setDataEventsEnabled( bool enabled ) {
enqueue( DataEventsCommand{ { enabled } } );
}
void TerminalController::setPromptEventsEnabled( bool enabled ) {
enqueue( PromptEventsCommand{ { enabled } } );
}
void TerminalController::reset() {
enqueue( ResetCommand{} );
}
void TerminalController::terminate() {
enqueue( TerminateCommand{} );
}
void TerminalController::restart( PtyPtr&& pty, ProcPtr&& process ) {
if ( !pty || !process ) {
enqueueEvent( { EventType::RestartFailure, "Invalid PTY or process" }, false );
return;
}
enqueue( RestartCommand{ std::move( pty ), std::move( process ) } );
}
std::shared_ptr<const TerminalSnapshot> TerminalController::snapshot() const {
return mPublishedSnapshot.load( std::memory_order_acquire );
}
std::optional<std::string>
TerminalController::requestSelection( std::chrono::milliseconds timeout ) {
if ( mShutdownRequested.load( std::memory_order_acquire ) )
return std::nullopt;
auto response = std::make_shared<SelectionResponse>();
enqueue( SelectionRequestCommand{ response } );
std::unique_lock<std::mutex> lock( response->mutex );
if ( !response->condition.wait_for( lock, timeout, [&response] { return response->ready; } ) )
return std::nullopt;
return std::move( response->selection );
}
std::vector<TerminalController::Event> TerminalController::drainEvents() {
std::vector<Event> events;
std::lock_guard<std::mutex> lock( mEventMutex );
events.reserve( mEvents.size() );
while ( !mEvents.empty() ) {
events.emplace_back( std::move( mEvents.front() ) );
mEvents.pop_front();
}
return events;
}
void TerminalController::enqueueEvent( Event event, bool coalescable ) {
std::lock_guard<std::mutex> lock( mEventMutex );
if ( coalescable ) {
for ( auto it = mEvents.rbegin(); it != mEvents.rend(); ++it ) {
const bool replaceable =
it->type == EventType::Title || it->type == EventType::IconTitle ||
it->type == EventType::HistoryLength || it->type == EventType::ScrollPosition ||
it->type == EventType::SnapshotReady;
if ( !replaceable )
break;
if ( it->type == event.type ) {
*it = std::move( event );
return;
}
}
}
mEvents.emplace_back( std::move( event ) );
}
void TerminalController::publishSnapshot( std::shared_ptr<const TerminalSnapshot> snapshot ) {
const Uint64 generation = snapshot->generation;
mPublishedSnapshot.store( std::move( snapshot ), std::memory_order_release );
Event event{ EventType::SnapshotReady };
event.generation = generation;
enqueueEvent( std::move( event ), true );
}
void TerminalController::workerLoop() {
while ( !mShutdownRequested.load( std::memory_order_acquire ) ) {
processCommands();
if ( mShutdownRequested.load( std::memory_order_acquire ) )
break;
const bool inputDrained = mEmulator->update();
if ( !inputDrained )
continue;
std::unique_lock<std::mutex> lock( mCommandMutex );
if ( mCommands.empty() && !mShutdownRequested.load( std::memory_order_relaxed ) )
mCommandCondition.wait_for( lock, std::chrono::milliseconds( 8 ) );
}
mShutdownRequested.store( true, std::memory_order_release );
mEmulator.reset();
mWorkerDisplay.reset();
}
void TerminalController::processCommands() {
std::deque<Command> commands;
{
std::lock_guard<std::mutex> lock( mCommandMutex );
commands.swap( mCommands );
}
while ( !commands.empty() && !mShutdownRequested.load( std::memory_order_relaxed ) ) {
processCommand( std::move( commands.front() ) );
commands.pop_front();
}
}
void TerminalController::processCommand( Command&& command ) {
std::visit(
[this]( auto&& value ) {
using T = std::decay_t<decltype( value )>;
if constexpr ( std::is_same_v<T, WriteCommand> ) {
mEmulator->ttywrite( value.data.data(), value.data.size(), value.mayEcho );
} else if constexpr ( std::is_same_v<T, WriteRawCommand> ) {
mEmulator->write( value.data.data(), value.data.size() );
} else if constexpr ( std::is_same_v<T, ResizeCommand> ) {
mEmulator->resize( value.columns, value.rows );
} else if constexpr ( std::is_same_v<T, ScrollCommand> ) {
TerminalArg argument( value.amount );
if ( value.direction < 0 )
mEmulator->kscrollup( &argument );
else if ( value.direction > 0 )
mEmulator->kscrolldown( &argument );
else {
mEmulator->kscrollto( &argument );
mWorkerDisplay->setLastAppliedScrollCommand( value.commandId );
}
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, SelectionStartCommand> ) {
mEmulator->selstart( value.column, value.row, value.snap );
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, SelectionExtendCommand> ) {
mEmulator->selextend( value.column, value.row, value.type, value.done );
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, SelectionClearCommand> ) {
mEmulator->selclear();
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, MouseCommand> ) {
mEmulator->mousereport( value.type, value.position, value.flags, value.modifiers );
} else if constexpr ( std::is_same_v<T, FocusCommand> ) {
if ( mWorkerDisplay->getMode( MODE_FOCUS ) )
mEmulator->ttywrite( value.value ? "\033[I" : "\033[O", 3, false );
mWorkerDisplay->setFocused( value.value );
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, CursorModeCommand> ) {
mWorkerDisplay->setCursorMode( value.mode );
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, PaletteCommand> ) {
mWorkerDisplay->setPalette( std::move( value.palette ) );
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, PresentationRateCommand> ) {
const Uint32 framesPerSecond = eeclamp<Uint32>( value.framesPerSecond, 1, 1000 );
mEmulator->setPresentationInterval(
Microseconds( 1000000.0 / static_cast<double>( framesPerSecond ) ) );
mWorkerDisplay->setPresentationRate( framesPerSecond );
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, AllowTrimCommand> ) {
mEmulator->setAllowMemoryTrimnming( value.value );
} else if constexpr ( std::is_same_v<T, DataEventsCommand> ) {
if ( value.value ) {
mEmulator->setDataCb( [this]( const char* data, size_t size ) {
enqueueEvent( { EventType::Data, std::string( data, size ) }, false );
} );
} else {
mEmulator->setDataCb( {} );
}
} else if constexpr ( std::is_same_v<T, PromptEventsCommand> ) {
if ( value.value ) {
mEmulator->setPromptStateChangedCb(
[this]( PromptState state, std::string_view data ) {
Event event{ EventType::PromptState, std::string( data ) };
event.promptState = state;
enqueueEvent( std::move( event ), false );
} );
} else {
mEmulator->setPromptStateChangedCb( {} );
}
} else if constexpr ( std::is_same_v<T, TerminateCommand> ) {
mEmulator->terminate();
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, ResetCommand> ) {
mEmulator->reset();
} else if constexpr ( std::is_same_v<T, RestartCommand> ) {
mEmulator->clearHistory();
mEmulator->setPtyAndProcess( std::move( value.pty ), std::move( value.process ) );
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, SelectionRequestCommand> ) {
std::lock_guard<std::mutex> lock( value.response->mutex );
value.response->selection = mEmulator->getSelection();
value.response->ready = true;
value.response->condition.notify_one();
}
},
std::move( command ) );
}
}} // namespace eterm::Terminal
@@ -371,15 +371,6 @@ static const Color colormapped[256] = {
Color( 208, 208, 208 ), Color( 218, 218, 218 ), Color( 228, 228, 228 ),
Color( 238, 238, 238 ) };
std::shared_ptr<TerminalDisplay> TerminalDisplay::create(
EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize,
std::shared_ptr<TerminalEmulator>&& terminalEmulator, const bool& useFrameBuffer ) {
std::shared_ptr<TerminalDisplay> terminal = std::shared_ptr<TerminalDisplay>(
new TerminalDisplay( window, font, fontSize, pixelsSize, useFrameBuffer ) );
terminal->mTerminal = std::move( terminalEmulator );
return terminal;
}
static Sizei gridSizeFromTermDimensions( Font* font, const Float& fontSize,
const Sizef& pixelsSize ) {
auto fontHeight = (Float)font->getFontHeight( fontSize );
@@ -390,6 +381,19 @@ static Sizei gridSizeFromTermDimensions( Font* font, const Float& fontSize,
return { clipColumns, clipRows };
}
static Uint32 presentationRateForWindow( EE::Window::Window* window ) {
if ( !window )
return 60;
Uint32 presentationRate = window->getFrameRateLimit();
if ( presentationRate == 0 && Engine::existsSingleton() &&
Engine::instance()->getDisplayManager() ) {
if ( auto* display = Engine::instance()->getDisplayManager()->getDisplayIndex(
window->getCurrentDisplayIndex() ) )
presentationRate = display->getRefreshRate();
}
return presentationRate > 0 ? presentationRate : 60;
}
std::shared_ptr<TerminalDisplay> TerminalDisplay::create(
EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize,
std::string program, std::vector<std::string> args, const std::string& workingDir,
@@ -446,8 +450,15 @@ std::shared_ptr<TerminalDisplay> TerminalDisplay::create(
std::shared_ptr<TerminalDisplay> terminal = std::shared_ptr<TerminalDisplay>(
new TerminalDisplay( window, font, fontSize, pixelsSize, useFrameBuffer ) );
terminal->mTerminal = TerminalEmulator::create( std::move( pseudoTerminal ),
std::move( process ), terminal, historySize );
terminal->mController =
TerminalController::create( std::move( pseudoTerminal ), std::move( process ), historySize,
terminal->makeColorPalette() );
if ( !terminal->mController ) {
if ( freeProcessFactory )
eeSAFE_DELETE( processFactory );
return nullptr;
}
terminal->mController->setPresentationRate( presentationRateForWindow( window ) );
terminal->mProgram = program;
terminal->mArgs = args;
terminal->mEnv = env;
@@ -461,11 +472,13 @@ std::shared_ptr<TerminalDisplay> TerminalDisplay::create(
return terminal;
}
TerminalDisplay::~TerminalDisplay() = default;
TerminalDisplay::~TerminalDisplay() {
if ( mController )
mController->shutdown();
}
TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const Float& fontSize,
const Sizef& pixelsSize, const bool& useFrameBuffer ) :
ITerminalDisplay(),
mWindow( window ),
mFont( font ),
mFontSize( fontSize ),
@@ -476,8 +489,8 @@ TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const
defaultGlyph.mode = ATTR_INVISIBLE;
mCursorGlyph = defaultGlyph;
mColors.resize( eeARRAY_SIZE( colormapped ), Color::Transparent );
mBuffer.resize( mColumns * mRows, defaultGlyph );
( (int&)mMode ) |= MODE_FOCUSED;
mMode |= MODE_FOCUSED;
resetColors();
Sizei gridSize( gridSizeFromTermDimensions( mFont, mFontSize, mSize - mPadding * 2.f ) );
mDirtyLines.resize( gridSize.getHeight(), 1 );
@@ -608,26 +621,40 @@ void TerminalDisplay::setPadding( const Rectf& padding ) {
}
}
const std::shared_ptr<TerminalEmulator>& TerminalDisplay::getTerminal() const {
return mTerminal;
}
void TerminalDisplay::attach( TerminalEmulator* terminal ) {
ITerminalDisplay::attach( terminal );
onSizeChange();
const std::shared_ptr<TerminalController>& TerminalDisplay::getController() const {
return mController;
}
int TerminalDisplay::scrollSize() const {
return mEmulator ? mEmulator->scrollSize() : 0;
return mSnapshot ? mSnapshot->historyLength : 0;
}
int TerminalDisplay::rowCount() const {
return mEmulator ? mEmulator->rowCount() : 0;
return mSnapshot ? mSnapshot->rows : 0;
}
int TerminalDisplay::scrollPosition() const {
return mSnapshot ? mSnapshot->scrollPosition : 0;
}
Uint64 TerminalDisplay::scrollTo( int position ) {
return mController ? mController->scrollTo( position ) : 0;
}
Uint64 TerminalDisplay::lastAppliedScrollCommand() const {
return mSnapshot ? mSnapshot->lastAppliedScrollCommand : 0;
}
void TerminalDisplay::sendEvent( const Event& event ) {
for ( auto it : mCallbacks )
it.second( event );
std::vector<Uint32> callbacks;
callbacks.reserve( mCallbacks.size() );
for ( const auto& callback : mCallbacks )
callbacks.emplace_back( callback.first );
for ( const Uint32 callbackId : callbacks ) {
auto callback = mCallbacks.find( callbackId );
if ( callback != mCallbacks.end() )
callback->second( event );
}
}
Uint32 TerminalDisplay::pushEventCallback( const EventFunc& func ) {
@@ -652,16 +679,18 @@ const TerminalColorScheme& TerminalDisplay::getColorScheme() const {
void TerminalDisplay::setColorScheme( const TerminalColorScheme& colorScheme ) {
mColorScheme = colorScheme;
resetColors();
if ( mController )
mController->setColorPalette( makeColorPalette() );
invalidateLines();
}
bool TerminalDisplay::isAppCapturingMouse() const {
return mTerminal &&
return mController &&
( mMode & ( MODE_MOUSEX10 | MODE_MOUSEBTN | MODE_MOUSEMOTION | MODE_MOUSEMANY ) );
}
bool TerminalDisplay::isAltScr() const {
return mEmulator && mEmulator->tisaltscr();
return mSnapshot && mSnapshot->altScreen;
}
const Uint32& TerminalDisplay::getClickStep() const {
@@ -681,18 +710,13 @@ void TerminalDisplay::setKeepAlive( bool keepAlive ) {
}
bool TerminalDisplay::update( bool isMouseOverMe ) {
bool ret = true;
consumeSnapshot();
drainControllerEvents();
if ( mFocus && isBlinkingCursor() && mClock.getElapsedTime().asSeconds() > 0.7 ) {
mMode ^= MODE_BLINK;
mClock.restart();
invalidateCursor();
}
if ( mTerminal ) {
int histi = mTerminal->getHistorySize();
ret = mTerminal->update();
if ( histi != mTerminal->getHistorySize() )
sendEvent( { EventType::HISTORY_LENGTH_CHANGE } );
}
if ( mAlreadyClickedLButton ) {
if ( !( mWindow->getInput()->getPressTrigger() & EE_BUTTON_LMASK ) ) {
mWindow->getInput()->captureMouse( false );
@@ -702,42 +726,222 @@ bool TerminalDisplay::update( bool isMouseOverMe ) {
mWindow->getInput()->getPressTrigger() );
}
}
return ret;
return true;
}
void TerminalDisplay::consumeSnapshot() {
if ( !mController )
return;
auto snapshot = mController->snapshot();
if ( !snapshot || snapshot->generation == mSnapshotGeneration )
return;
const Vector2i previousCursor = mCursor;
const bool dimensionsChanged = snapshot->columns != static_cast<int>( mColumns ) ||
snapshot->rows != static_cast<int>( mRows );
if ( dimensionsChanged ) {
mColumns = snapshot->columns;
mRows = snapshot->rows;
mDirtyLines.assign( mRows, true );
if ( !mUseFrameBuffer )
initVBOs();
mFullDirty = true;
} else if ( !snapshot->dirtyRowsFollow( mSnapshotGeneration ) ) {
// Atomic publication intentionally allows the worker to lap the renderer. The latest
// snapshot contains every cell, but its dirty rows only cover the immediately preceding
// generation, so a skipped generation requires rebuilding every visible row.
invalidateLines();
} else {
for ( size_t row = 0; row < snapshot->dirtyRows.size(); ++row ) {
if ( snapshot->dirtyRows[row] )
invalidateLine( row );
}
}
mSnapshot = std::move( snapshot );
mSnapshotGeneration = mSnapshot->generation;
mCursor = mSnapshot->cursor;
mCursorGlyph = mSnapshot->cursorGlyph;
mCursorMode = mSnapshot->cursorMode;
const int presentationBits = mMode & MODE_BLINK;
mMode = mSnapshot->windowMode | presentationBits;
if ( mFocus )
mMode |= MODE_FOCUSED;
else
mMode &= ~MODE_FOCUSED;
if ( previousCursor != mCursor ) {
invalidateLine( previousCursor.y );
invalidateCursor();
}
if ( isBlinkingCursor() ) {
mMode |= MODE_BLINK;
mClock.restart();
}
mDirty = true;
}
void TerminalDisplay::drainControllerEvents() {
if ( !mController )
return;
for ( auto& event : mController->drainEvents() ) {
switch ( event.type ) {
case TerminalController::EventType::Title:
sendEvent( { EventType::TITLE, std::move( event.data ) } );
break;
case TerminalController::EventType::IconTitle:
sendEvent( { EventType::ICON_TITLE, std::move( event.data ) } );
break;
case TerminalController::EventType::ScrollPosition:
sendEvent( { EventType::SCROLL_HISTORY } );
break;
case TerminalController::EventType::Bell:
sendEvent( { EventType::BELL } );
break;
case TerminalController::EventType::Clipboard:
setClipboard( event.data.c_str() );
sendEvent( { EventType::CLIPBOARD } );
break;
case TerminalController::EventType::ProcessExit:
onProcessExit( event.value );
break;
case TerminalController::EventType::RestartFailure:
sendEvent( { EventType::RESTART_FAILURE, std::move( event.data ) } );
break;
case TerminalController::EventType::Data:
if ( mDataCallback )
mDataCallback( event.data.data(), event.data.size() );
break;
case TerminalController::EventType::PromptState:
if ( mPromptStateChangedCallback )
mPromptStateChangedCallback( event.promptState, event.data );
break;
case TerminalController::EventType::Color:
if ( event.value < 0 )
resetColors();
else
resetColor( event.value, event.data.empty() ? nullptr : event.data.c_str() );
invalidateLines();
break;
case TerminalController::EventType::Error:
Log::error( "Terminal worker error: %s", event.data.c_str() );
sendEvent( { EventType::WORKER_ERROR, std::move( event.data ) } );
break;
case TerminalController::EventType::HistoryLength:
sendEvent( { EventType::HISTORY_LENGTH_CHANGE } );
break;
case TerminalController::EventType::SnapshotReady:
break;
}
}
}
TerminalColorPalette TerminalDisplay::makeColorPalette() const {
TerminalColorPalette palette;
palette.colors.reserve( mColors.size() );
for ( const auto& color : mColors )
palette.colors.emplace_back( color.getValue() );
palette.cursor = mColorScheme.getCursor().getValue();
palette.foreground = mColorScheme.getForeground().getValue();
palette.background = mColorScheme.getBackground().getValue();
return palette;
}
std::string TerminalDisplay::getSelection() {
if ( mController ) {
if ( auto selection = mController->requestSelection() )
return std::move( *selection );
}
return mSnapshot ? mSnapshot->selection : std::string{};
}
bool TerminalDisplay::hasSelection() const {
return mSnapshot && mSnapshot->hasSelection;
}
TerminalSelectionMode TerminalDisplay::getSelectionMode() const {
return mSnapshot ? mSnapshot->selectionMode : SEL_IDLE;
}
int TerminalDisplay::getProcessId() const {
return mSnapshot ? mSnapshot->processId : 0;
}
int TerminalDisplay::getExitCode() const {
return mSnapshot ? mSnapshot->exitCode : 0;
}
void TerminalDisplay::terminate() {
if ( mController )
mController->terminate();
}
void TerminalDisplay::setAllowMemoryTrimming( bool allow ) {
if ( mController )
mController->setAllowMemoryTrimming( allow );
}
void TerminalDisplay::setDataCallback( DataFunc callback ) {
mDataCallback = std::move( callback );
if ( mController )
mController->setDataEventsEnabled( static_cast<bool>( mDataCallback ) );
}
void TerminalDisplay::setPromptStateChangedCallback( PromptStateChangedFunc callback ) {
mPromptStateChangedCallback = std::move( callback );
if ( mController )
mController->setPromptEventsEnabled( static_cast<bool>( mPromptStateChangedCallback ) );
}
void TerminalDisplay::setCursorMode( TerminalCursorMode mode ) {
if ( mCursorMode == mode )
return;
mCursorMode = mode;
if ( mController )
mController->setCursorMode( mode );
invalidateCursor();
}
TerminalCursorMode TerminalDisplay::getCursorMode() const {
return mCursorMode;
}
void TerminalDisplay::executeFile( const std::string& cmd ) {
if ( mTerminal ) {
std::string rcmd( cmd + "\r" );
if ( mController ) {
std::string rcmd;
#if EE_PLATFORM != EE_PLATFORM_WIN
char clearLine = 0x15;
mTerminal->ttywrite( &clearLine, 1, 1 );
rcmd.push_back( 0x15 );
#endif
mTerminal->ttywrite( rcmd.c_str(), rcmd.size(), 1 );
rcmd.append( cmd ).push_back( '\r' );
mController->write( std::move( rcmd ) );
}
}
void TerminalDisplay::executeBinary( const std::string& binaryPath, const std::string& args ) {
if ( mTerminal ) {
std::string rcmd( "\"" + binaryPath + "\"" + " " + args + "\r" );
if ( mController ) {
std::string rcmd;
#if EE_PLATFORM != EE_PLATFORM_WIN
char clearLine = 0x15;
mTerminal->ttywrite( &clearLine, 1, 1 );
rcmd.push_back( 0x15 );
#endif
mTerminal->ttywrite( rcmd.c_str(), rcmd.size(), 1 );
rcmd.append( "\"" ).append( binaryPath ).append( "\" " ).append( args ).push_back( '\r' );
mController->write( std::move( rcmd ) );
}
}
void TerminalDisplay::action( TerminalShortcutAction action ) {
if ( !mController && action != TerminalShortcutAction::FONTSIZE_GROW &&
action != TerminalShortcutAction::FONTSIZE_SHRINK )
return;
switch ( action ) {
case TerminalShortcutAction::PASTE: {
getClipboard();
if ( !mClipboardUtf8.empty() ) {
if ( mMode & MODE_BRCKTPASTE ) {
mTerminal->write( "\033[200~", 6 );
mTerminal->write( mClipboardUtf8.c_str(), mClipboardUtf8.size() );
mTerminal->write( "\033[201~", 6 );
mController->writeRaw( "\033[200~" );
mController->writeRaw( std::move( mClipboardUtf8 ) );
mController->writeRaw( "\033[201~" );
} else {
mTerminal->write( mClipboardUtf8.c_str(), mClipboardUtf8.size() );
mController->writeRaw( std::move( mClipboardUtf8 ) );
}
}
break;
@@ -746,61 +950,48 @@ void TerminalDisplay::action( TerminalShortcutAction action ) {
std::string selection =
mWindow->getClipboard()->hasPrimarySelection()
? mWindow->getClipboard()->getPrimarySelectionText()
: ( mTerminal->hasSelection()
? mTerminal->getSelection()
: mWindow->getClipboard()->getPrimarySelectionText() );
: ( hasSelection() ? getSelection()
: mWindow->getClipboard()->getPrimarySelectionText() );
sanitizeInput( selection );
if ( !selection.empty() ) {
if ( mMode & MODE_BRCKTPASTE ) {
mTerminal->write( "\033[200~", 6 );
mTerminal->write( selection.c_str(), selection.size() );
mTerminal->write( "\033[201~", 6 );
mController->writeRaw( "\033[200~" );
mController->writeRaw( std::move( selection ) );
mController->writeRaw( "\033[201~" );
} else {
mTerminal->write( selection.c_str(), selection.size() );
mController->writeRaw( std::move( selection ) );
}
}
break;
}
case TerminalShortcutAction::COPY: {
auto selection = mTerminal->getSelection();
auto selection = getSelection();
if ( !selection.empty() )
setClipboard( selection.c_str() );
break;
}
case TerminalShortcutAction::SCROLLUP_SCREEN: {
TerminalArg arg( (int)-mClickStep );
mTerminal->kscrollup( &arg );
sendEvent( { EventType::SCROLL_HISTORY } );
mController->scrollUp( -(int)mClickStep );
break;
}
case TerminalShortcutAction::SCROLLDOWN_SCREEN: {
TerminalArg arg( (int)-mClickStep );
mTerminal->kscrolldown( &arg );
sendEvent( { EventType::SCROLL_HISTORY } );
mController->scrollDown( -(int)mClickStep );
break;
}
case TerminalShortcutAction::SCROLLUP_ROW: {
TerminalArg arg( (int)mClickStep );
mTerminal->kscrollup( &arg );
sendEvent( { EventType::SCROLL_HISTORY } );
mController->scrollUp( mClickStep );
break;
}
case TerminalShortcutAction::SCROLLDOWN_ROW: {
TerminalArg arg( (int)mClickStep );
mTerminal->kscrolldown( &arg );
sendEvent( { EventType::SCROLL_HISTORY } );
mController->scrollDown( mClickStep );
break;
}
case TerminalShortcutAction::SCROLLUP_HISTORY: {
TerminalArg arg( (int)INT_MAX );
mTerminal->kscrollup( &arg );
sendEvent( { EventType::SCROLL_HISTORY } );
mController->scrollUp( INT_MAX );
break;
}
case TerminalShortcutAction::SCROLLDOWN_HISTORY: {
TerminalArg arg( (int)INT_MAX );
mTerminal->kscrolldown( &arg );
sendEvent( { EventType::SCROLL_HISTORY } );
mController->scrollDown( INT_MAX );
break;
}
case TerminalShortcutAction::FONTSIZE_GROW: {
@@ -815,17 +1006,7 @@ void TerminalDisplay::action( TerminalShortcutAction action ) {
}
bool TerminalDisplay::hasTerminated() const {
return mTerminal->hasExited();
}
void TerminalDisplay::setTitle( const char* title ) {
if ( title )
sendEvent( { EventType::TITLE, std::string( title ) } );
}
void TerminalDisplay::setIconTitle( const char* title ) {
if ( title )
sendEvent( { EventType::ICON_TITLE, std::string( title ) } );
return mSnapshot && mSnapshot->processExited;
}
void TerminalDisplay::setClipboard( const char* text ) {
@@ -862,48 +1043,6 @@ void TerminalDisplay::sanitizeInput( std::string& input ) {
}
}
bool TerminalDisplay::drawBegin( Uint32 columns, Uint32 rows ) {
if ( columns != mColumns || rows != mRows ) {
TerminalGlyph defaultGlyph{};
mBuffer.resize( columns * rows, defaultGlyph );
mColumns = columns;
mRows = rows;
if ( !mUseFrameBuffer )
initVBOs();
invalidateLines();
invalidateCursor();
}
return ( ( mMode & MODE_VISIBLE ) != 0 );
}
void TerminalDisplay::drawLine( Line line, int x1, int y, int x2 ) {
memcpy( &mBuffer[y * mColumns + x1], line, ( x2 - x1 ) * sizeof( TerminalGlyph ) );
for ( int i = x1; i < x2; i++ ) {
if ( mTerminal->selected( i, y ) ) {
mBuffer[y * mColumns + i].mode |= ATTR_REVERSE;
}
}
invalidateLine( y );
}
void TerminalDisplay::drawCursor( int cx, int cy, TerminalGlyph g, int, int, TerminalGlyph ) {
if ( mCursor != Vector2i( cx, cy ) || mCursorGlyph != g ) {
mCursor.x = cx;
mCursor.y = cy;
if ( isBlinkingCursor() ) {
mMode |= MODE_BLINK;
mClock.restart();
}
mCursorGlyph = g;
invalidateCursor();
}
}
void TerminalDisplay::drawEnd() {}
void TerminalDisplay::draw() {
draw( nullptr != mFrameBuffer ? Vector2f( mPadding.Left, mPadding.Top )
: mPosition.floor() + Vector2f( mPadding.Left, mPadding.Top ) );
@@ -914,11 +1053,9 @@ void TerminalDisplay::onMouseDoubleClick( const Vector2i& pos, const Uint32& fla
mLastDoubleClick.restart();
if ( !isAppCapturingMouse() && ( flags & EE_BUTTON_LMASK ) &&
( mTerminal->getSelectionMode() == TerminalSelectionMode::SEL_EMPTY ||
mTerminal->getSelectionMode() == TerminalSelectionMode::SEL_IDLE ) ) {
( getSelectionMode() == SEL_EMPTY || getSelectionMode() == SEL_IDLE ) ) {
auto gridPos{ positionToGrid( pos ) };
mTerminal->selstart( gridPos.x, gridPos.y, SNAP_WORD );
invalidateLines();
mController->selectionStart( gridPos.x, gridPos.y, SNAP_WORD );
}
}
@@ -944,16 +1081,15 @@ void TerminalDisplay::onMouseMove( const Vector2i& pos, const Uint32& flags ) {
}
if ( !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) &&
( mTerminal->getSelectionMode() == TerminalSelectionMode::SEL_EMPTY ||
mTerminal->getSelectionMode() == TerminalSelectionMode::SEL_READY ) ) {
( mDraggingSel || getSelectionMode() == SEL_EMPTY || getSelectionMode() == SEL_READY ) ) {
auto gridPos{ positionToGrid( pos ) };
mTerminal->selextend(
mController->selectionExtend(
gridPos.x, gridPos.y,
mWindow->getInput()->getModState() & KEYMOD_SHIFT ? SEL_RECTANGULAR : SEL_REGULAR, 0 );
invalidateLines();
mWindow->getInput()->getModState() & KEYMOD_SHIFT ? SEL_RECTANGULAR : SEL_REGULAR,
false );
}
mTerminal->mousereport( TerminalMouseEventType::MouseMotion, positionToGrid( pos ), flags,
mWindow->getInput()->getModState() );
mController->mouseReport( TerminalMouseEventType::MouseMotion, positionToGrid( pos ), flags,
mWindow->getInput()->getModState() );
}
void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
@@ -967,10 +1103,10 @@ void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
if ( !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) &&
mLastDoubleClick.getElapsedTime() < Milliseconds( 300.f ) ) {
mTerminal->selstart( gridPos.x, gridPos.y, SNAP_LINE );
mController->selectionStart( gridPos.x, gridPos.y, SNAP_LINE );
} else if ( !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) ) {
if ( !mDraggingSel ) {
mTerminal->selstart( gridPos.x, gridPos.y, 0 );
mController->selectionStart( gridPos.x, gridPos.y, 0 );
mDraggingSel = true;
invalidateLines();
mWindow->getInput()->captureMouse( true );
@@ -993,8 +1129,8 @@ void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
}
}
mTerminal->mousereport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ), flags,
mWindow->getInput()->getModState() );
mController->mouseReport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ), flags,
mWindow->getInput()->getModState() );
}
void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
@@ -1003,7 +1139,7 @@ void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
}
if ( ( flags & EE_BUTTON_LMASK ) && mWindow->getClipboard()->hasPrimarySelection() ) {
mWindow->getClipboard()->setPrimarySelectionText( mTerminal->getSelection() );
mWindow->getClipboard()->setPrimarySelectionText( getSelection() );
}
Uint32 smod = sanitizeMod( mWindow->getInput()->getModState() );
@@ -1033,15 +1169,15 @@ void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
if ( IS_SET( MODE_APPCURSOR ) ? k.appcursor < 0 : k.appcursor > 0 )
continue;
if ( !k.altscrn || ( k.altscrn == ( mEmulator->tisaltscr() ? 1 : -1 ) ) ) {
if ( !k.altscrn || ( k.altscrn == ( isAltScr() ? 1 : -1 ) ) ) {
action( k.action );
return;
}
}
}
mTerminal->mousereport( TerminalMouseEventType::MouseButtonRelease, positionToGrid( pos ),
flags, mWindow->getInput()->getModState() );
mController->mouseReport( TerminalMouseEventType::MouseButtonRelease, positionToGrid( pos ),
flags, mWindow->getInput()->getModState() );
}
static inline Color termColor( unsigned int terminalColor, const std::vector<Color>& colors ) {
@@ -1256,7 +1392,7 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
for ( Uint32 i = 0; i < mColumns; i++ ) {
mCurGridPos = { i, j };
auto& glyph = mBuffer[j * mColumns + i];
const auto& glyph = mSnapshot->cells[j * mColumns + i];
auto fg = termColor( glyph.fg, mColors );
auto bg = termColor( glyph.bg, mColors );
@@ -1328,7 +1464,7 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
for ( Uint32 i = 0; i < mColumns; i++ ) {
mCurGridPos = { i, j };
auto& glyph = mBuffer[j * mColumns + i];
const auto& glyph = mSnapshot->cells[j * mColumns + i];
auto fg = termColor( glyph.fg, mColors );
auto bg = termColor( glyph.bg, mColors );
Color temp{ Color::Transparent };
@@ -1439,8 +1575,8 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
invalidateCursor();
}
bool redrawCursor =
!mEmulator->isScrolling() && !IS_SET( MODE_HIDE ) && ( !mUseFrameBuffer || mDirtyCursor );
bool redrawCursor = mSnapshot && mSnapshot->cursorVisible && !IS_SET( MODE_HIDE ) &&
( !mUseFrameBuffer || mDirtyCursor );
bool mustRenderUnderline = false;
if ( redrawCursor ) {
@@ -1448,14 +1584,14 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
Color drawcol;
if ( IS_SET( MODE_REVERSE ) ) {
if ( mEmulator->isSelected( mCursor.x, mCursor.y ) ) {
if ( mSnapshot->cursorSelected ) {
drawcol = mColorScheme.getCursor();
} else {
drawcol = mColorScheme.getBackground();
}
} else {
drawcol = mEmulator->isSelected( mCursor.x, mCursor.y ) ? mColorScheme.getBackground()
: mColorScheme.getCursor();
drawcol =
mSnapshot->cursorSelected ? mColorScheme.getBackground() : mColorScheme.getCursor();
}
mPrimitives.setColor( drawcol );
@@ -1564,7 +1700,7 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
}
void TerminalDisplay::drawBg( bool toFBO ) {
auto defaultBg = termColor( mEmulator->getDefaultBackground(), mColors );
auto defaultBg = mColorScheme.getBackground();
Primitives p;
p.setForceDraw( toFBO );
p.setColor( defaultBg );
@@ -1577,7 +1713,7 @@ void TerminalDisplay::drawBg( bool toFBO ) {
}
void TerminalDisplay::draw( const Vector2f& pos ) {
if ( !mEmulator || !mTerminal )
if ( !mController || !mSnapshot )
return;
mDrawing = true;
@@ -1625,10 +1761,10 @@ Vector2i TerminalDisplay::positionToGrid( const Vector2i& pos ) {
}
// All these checks are because there's a very rare bug I cannot find how it happens
auto termSize = mTerminal->getSize();
auto termSize = mSnapshot ? Vector2i( mSnapshot->columns, mSnapshot->rows ) : Vector2i::Zero;
eeASSERT( mouseX >= 0 && mouseX <= mTerminal->getSize().x );
eeASSERT( mouseY >= 0 && mouseY <= mTerminal->getSize().y );
eeASSERT( mouseX >= 0 && mouseX <= termSize.x );
eeASSERT( mouseY >= 0 && mouseY <= termSize.y );
mouseX = eeclamp( mouseX, 0, termSize.x );
mouseY = eeclamp( mouseY, 0, termSize.y );
@@ -1641,18 +1777,10 @@ void TerminalDisplay::onSizeChange() {
mFont, mFontSize,
mSize - Vector2f( mPadding.Left + mPadding.Right, mPadding.Top + mPadding.Bottom ) ) );
if ( mTerminal ) {
if ( gridSize.getWidth() != mTerminal->getNumColumns() ||
gridSize.getHeight() != mTerminal->getNumRows() ) {
mTerminal->resize( gridSize.getWidth(), gridSize.getHeight() );
mDirtyLines.resize( gridSize.getHeight(), 1 );
}
} else if ( mEmulator ) {
if ( gridSize.getWidth() != mEmulator->getNumColumns() ||
gridSize.getHeight() != mEmulator->getNumRows() ) {
mEmulator->resize( gridSize.getWidth(), gridSize.getHeight() );
mDirtyLines.resize( gridSize.getHeight(), 1 );
}
if ( mController && ( !mSnapshot || gridSize.getWidth() != mSnapshot->columns ||
gridSize.getHeight() != mSnapshot->rows ) ) {
mController->resize( gridSize.getWidth(), gridSize.getHeight() );
mDirtyLines.resize( gridSize.getHeight(), 1 );
}
if ( mFrameBuffer && ( mFrameBuffer->getWidth() < mSize.getWidth() ||
@@ -1670,7 +1798,7 @@ void TerminalDisplay::onSizeChange() {
void TerminalDisplay::onProcessExit( int exitCode ) {
sendEvent( { EventType::PROCESS_EXIT, String::toString( exitCode ) } );
if ( !mTerminal || mProgram.empty() || exitCode != 0 || !mKeepAlive )
if ( !mController || mProgram.empty() || exitCode != 0 || !mKeepAlive )
return;
auto processFactory = eeNew( ProcessFactory, () );
@@ -1685,35 +1813,34 @@ void TerminalDisplay::onProcessExit( int exitCode ) {
if ( !pseudoTerminal ) {
eeSAFE_DELETE( processFactory );
fprintf( stderr, "TerminalDisplay::onProcessExit: Failed to create pseudo terminal\n" );
sendEvent( { EventType::RESTART_FAILURE, "Failed to create pseudo terminal" } );
return;
}
if ( !process ) {
eeSAFE_DELETE( processFactory );
fprintf( stderr, "TerminalDisplay::onProcessExit: Failed to spawn process\n" );
sendEvent( { EventType::RESTART_FAILURE, "Failed to spawn process" } );
return;
}
mTerminal->clearHistory();
mTerminal->setPtyAndProcess( std::move( pseudoTerminal ), std::move( process ) );
mController->restart( std::move( pseudoTerminal ), std::move( process ) );
eeSAFE_DELETE( processFactory );
}
void TerminalDisplay::onScrollPositionChange() {
sendEvent( { EventType::SCROLL_HISTORY } );
}
void TerminalDisplay::onTextInput( const Uint32& chr ) {
if ( !mTerminal )
if ( !mController )
return;
String input;
input.push_back( chr );
std::string utf8Input( input.toUtf8() );
mTerminal->ttywrite( utf8Input.c_str(), utf8Input.size(), 1 );
mController->write( std::move( utf8Input ) );
mDirty = true;
}
void TerminalDisplay::onTextEditing( const String&, const Int32&, const Int32& ) {
if ( !mTerminal )
if ( !mController )
return;
invalidateCursor();
updateIMELocation();
@@ -1734,7 +1861,7 @@ bool TerminalDisplay::isRegisteredShortcut( const Keycode& keyCode, const Uint32
if ( IS_SET( MODE_APPCURSOR ) ? k.appcursor < 0 : k.appcursor > 0 )
continue;
if ( !k.altscrn || ( k.altscrn == ( mEmulator->tisaltscr() ? 1 : -1 ) ) ) {
if ( !k.altscrn || ( k.altscrn == ( isAltScr() ? 1 : -1 ) ) ) {
return true;
}
}
@@ -1762,7 +1889,7 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/,
if ( IS_SET( MODE_APPCURSOR ) ? k.appcursor < 0 : k.appcursor > 0 )
continue;
if ( !k.altscrn || ( k.altscrn == ( mEmulator->tisaltscr() ? 1 : -1 ) ) ) {
if ( !k.altscrn || ( k.altscrn == ( isAltScr() ? 1 : -1 ) ) ) {
action( k.action );
return;
}
@@ -1783,7 +1910,7 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/,
}
}
mTerminal->ttywrite( &tmp, 1, 1 );
mController->write( std::string( 1, tmp ) );
return;
}
}
@@ -1802,7 +1929,7 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/,
continue;
if ( k.string.size() > 0 ) {
mTerminal->ttywrite( k.string.c_str(), k.string.size(), 1 );
mController->write( k.string );
return;
}
break;
@@ -1824,7 +1951,7 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/,
continue;
if ( k.string.size() > 0 ) {
mTerminal->ttywrite( k.string.c_str(), k.string.size(), 1 );
mController->write( k.string );
return;
}
break;
@@ -1936,17 +2063,14 @@ void TerminalDisplay::setFocus( bool focus ) {
}
mFocus = focus;
bool modeFocus = mMode & MODE_FOCUSED;
if ( mFocus != modeFocus ) {
if ( mFocus ) {
mMode |= MODE_FOCUSED | MODE_FOCUS;
mWindow->startTextInput();
} else {
mMode ^= MODE_FOCUS | MODE_FOCUSED;
}
if ( mFocus ) {
mMode |= MODE_FOCUSED;
mWindow->startTextInput();
} else {
mMode ^= MODE_FOCUS;
mMode &= ~MODE_FOCUSED;
}
if ( mController )
mController->setFocus( focus );
invalidateCursor();
}
@@ -844,6 +844,10 @@ void TerminalEmulator::setAllowMemoryTrimnming( bool allowMemoryTrimnming ) {
mAllowMemoryTrimnming = allowMemoryTrimnming;
}
void TerminalEmulator::setPresentationInterval( Time interval ) {
mPresentationInterval = interval > Time::Zero ? interval : Microseconds( 1000000.0 / 60.0 );
}
Vector2i TerminalEmulator::getSize() const {
return { mTerm.col, mTerm.row };
}
@@ -3232,7 +3236,6 @@ void TerminalEmulator::draw() {
mTerm.ocy = mTerm.c.y;
dpy->drawEnd();
mDeferredPresentationBatches = 0;
mPresentationClock.restart();
}
@@ -3245,6 +3248,11 @@ void TerminalEmulator::redraw() {
draw();
}
void TerminalEmulator::reset() {
treset();
redraw();
}
int TerminalEmulator::xsetcolorname( int x, const char* name ) {
return resetColor( x, name );
}
@@ -3316,8 +3324,6 @@ void TerminalEmulator::mousereport( const TerminalMouseEventType& type, const Ve
int len, btn, code;
char buf[40];
static int ox, oy;
for ( btn = 1; btn <= 31 && !( flags & ( 1 << ( btn - 1 ) ) ); btn++ )
;
@@ -3343,7 +3349,7 @@ void TerminalEmulator::mousereport( const TerminalMouseEventType& type, const Ve
}
if ( type == TerminalMouseEventType::MouseMotion ) {
if ( pos.x == ox && pos.y == oy )
if ( pos == mLastMousePosition )
return;
if ( !xgetmode( MODE_MOUSEMOTION ) && !xgetmode( MODE_MOUSEMANY ) )
return;
@@ -3371,8 +3377,7 @@ void TerminalEmulator::mousereport( const TerminalMouseEventType& type, const Ve
code = 0;
}
ox = pos.x;
oy = pos.y;
mLastMousePosition = pos;
/* Encode btn into code. If no button is pressed for a motion event in
* MODE_MOUSEMANY, then encode it as a release. */
@@ -3598,23 +3603,23 @@ bool TerminalEmulator::update() {
int reads = 0;
Clock readBudgetClock;
bool presentationDeadlineReached = false;
while ( reads < MAX_TTY_READS && ttyread() > 0 ) {
++reads;
if ( mPresentationClock.getElapsedTime() >= mPresentationInterval ) {
presentationDeadlineReached = true;
break;
}
if ( readBudgetClock.getElapsedTime() >= Milliseconds( 4 ) )
break;
}
bool readBudgetSaturated =
reads == MAX_TTY_READS ||
reads == MAX_TTY_READS || presentationDeadlineReached ||
( reads > 0 && readBudgetClock.getElapsedTime() >= Milliseconds( 4 ) );
/* Keep presentation decoupled from every PTY read batch, but bound the
* deferral so sustained output remains visibly live. The time limit handles
* expensive batches; the batch limit guarantees progress when updates are
* individually very fast. */
if ( readBudgetSaturated )
++mDeferredPresentationBatches;
bool presentationDue = !readBudgetSaturated || mDeferredPresentationBatches >= 32 ||
mPresentationClock.getElapsedTime() >= Milliseconds( 75 );
/* Keep presentation decoupled from every PTY read batch. Sustained output publishes on the
* host frame deadline, while a drained/idle burst still publishes immediately. */
bool presentationDue = !readBudgetSaturated || presentationDeadlineReached;
if ( presentationDue && ( reads > 0 || mDirty ) )
draw();
@@ -3626,6 +3631,9 @@ bool TerminalEmulator::update() {
if ( mProcess->hasExited() && !readBudgetSaturated ) {
mExitCode = mProcess->getExitCode();
mStatus = TERMINATED;
// Publish process state together with the final drained frame before the ordered exit
// event.
redraw();
onProcessExit( mExitCode );
}
+68 -43
View File
@@ -31,7 +31,10 @@ UITerminal* UITerminal::New( const std::shared_ptr<TerminalDisplay>& terminalDis
return eeNew( UITerminal, ( terminalDisplay ) );
}
UITerminal::~UITerminal() {}
UITerminal::~UITerminal() {
if ( mTerm && mTerminalEventCallbackId )
mTerm->popEventCallback( mTerminalEventCallbackId );
}
Uint32 UITerminal::getType() const {
return UI_TYPE_TERMINAL;
@@ -51,28 +54,29 @@ void UITerminal::draw() {
void UITerminal::registerNewTerminal() {
if ( !mTerm )
return;
mTerm->pushEventCallback( [this]( const TerminalDisplay::Event& event ) {
switch ( event.type ) {
case TerminalDisplay::EventType::TITLE: {
if ( !mIsCustomTitle && mTitle != event.eventData ) {
mTitle = event.eventData;
sendTextEvent( Event::OnTitleChange, mTitle );
mTerminalEventCallbackId =
mTerm->pushEventCallback( [this]( const TerminalDisplay::Event& event ) {
switch ( event.type ) {
case TerminalDisplay::EventType::TITLE: {
if ( !mIsCustomTitle && mTitle != event.eventData ) {
mTitle = event.eventData;
sendTextEvent( Event::OnTitleChange, mTitle );
}
break;
}
case TerminalDisplay::EventType::HISTORY_LENGTH_CHANGE: {
if ( !mTerm->isAltScr() )
onContentSizeChange();
break;
}
case TerminalDisplay::EventType::SCROLL_HISTORY: {
updateScrollPosition();
break;
}
default: {
}
break;
}
case TerminalDisplay::EventType::HISTORY_LENGTH_CHANGE: {
if ( !mTerm->getTerminal()->tisaltscr() )
onContentSizeChange();
break;
}
case TerminalDisplay::EventType::SCROLL_HISTORY: {
updateScrollPosition();
break;
}
default: {
}
}
} );
} );
}
UITerminal::UITerminal( const std::shared_ptr<TerminalDisplay>& terminalDisplay ) :
@@ -109,13 +113,13 @@ UITerminal::UITerminal( const std::shared_ptr<TerminalDisplay>& terminalDisplay
[this] { mTerm->action( TerminalShortcutAction::PASTE_SELECTION ); } );
setCommand( "terminal-copy", [this] { mTerm->action( TerminalShortcutAction::COPY ); } );
setCommand( "terminal-open-link",
[this] { Engine::instance()->openURI( mTerm->getTerminal()->getSelection() ); } );
[this] { Engine::instance()->openURI( mTerm->getSelection() ); } );
subscribeScheduledUpdate();
}
int UITerminal::getContentSize() const {
if ( mTerm && mTerm->getTerminal() )
return mTerm->getTerminal()->getHistorySize() + mTerm->getTerminal()->getNumRows();
if ( mTerm )
return mTerm->scrollSize() + mTerm->rowCount();
return 0;
}
@@ -139,6 +143,13 @@ void UITerminal::onContentSizeChange() {
mVScroll->setPixelsSize( mVScroll->getPixelsSize().getWidth(),
getPixelsSize().getHeight() - mPaddingPx.Top - mPaddingPx.Bottom );
// Changing the page step resizes the thumb and therefore changes the mouse-to-value mapping.
// Keep both the thumb and its range stable while an asynchronous drag scroll is in flight.
if ( mVScroll->isDragging() || mScrollByBar ) {
mPendingContentSizeChange = true;
return;
}
mPendingContentSizeChange = false;
updateScrollPosition();
mVScroll->setPageStep( contentSize > 0 ? ( visibleArea / (Float)contentSize ) : 1.f );
updateScroll();
@@ -179,17 +190,22 @@ void UITerminal::onPaddingChange() {
}
int UITerminal::getVisibleArea() const {
return ( mTerm && mTerm->getTerminal() ) ? mTerm->getTerminal()->getNumRows() : 0;
return mTerm ? mTerm->rowCount() : 0;
}
void UITerminal::updateScrollPosition() {
if ( mTerm && mTerm->getTerminal() ) {
int historySize = mTerm->getTerminal()->getHistorySize();
Float val = historySize > 0
? ( 1.f - mTerm->getTerminal()->scrollPos() / (Float)historySize )
: 1.f;
mVScroll->setValue( val, false );
if ( !mTerm || mVScroll->isDragging() )
return;
if ( mScrollByBar ) {
if ( mPendingScrollCommand != 0 &&
mTerm->lastAppliedScrollCommand() < mPendingScrollCommand )
return;
mScrollByBar = false;
mPendingScrollCommand = 0;
}
int historySize = mTerm->scrollSize();
Float val = historySize > 0 ? ( 1.f - mTerm->scrollPosition() / (Float)historySize ) : 1.f;
mVScroll->setValue( val, false );
}
int UITerminal::getScrollableArea() const {
@@ -211,11 +227,11 @@ void UITerminal::updateScroll() {
}
void UITerminal::onScrollChange() {
if ( !mTerm || !mTerm->getTerminal() )
if ( !mTerm )
return;
int scrollTo = ( getScrollableArea() - mScrollOffset );
TerminalArg arg( scrollTo );
mTerm->getTerminal()->kscrollto( &arg );
mPendingScrollCommand = mTerm->scrollTo( scrollTo );
mScrollByBar = mPendingScrollCommand != 0;
}
void UITerminal::setVerticalScrollMode( const ScrollBarMode& Mode ) {
@@ -321,18 +337,23 @@ const std::shared_ptr<TerminalDisplay>& UITerminal::getTerm() const {
void UITerminal::scheduledUpdate( const Time& ) {
if ( !mTerm )
return;
auto terminal = mTerm;
auto mousePos = getInput()->getRelativeMousePos();
bool mouseOutsideBounds =
mousePos.y < 0 || mousePos.y > getUISceneNode()->getWindow()->getSize().getHeight();
mTerm->update( isMouseOverMeOrChildren() && !mouseOutsideBounds );
terminal->update( isMouseOverMeOrChildren() && !mouseOutsideBounds );
if ( !mVScroll->isDragging() && ( mScrollByBar || mPendingContentSizeChange ) ) {
updateScrollPosition();
if ( !mScrollByBar && mPendingContentSizeChange )
onContentSizeChange();
}
if ( mTerm->isDirty() && isVisible() )
if ( terminal->isDirty() && isVisible() )
invalidateDraw();
if ( ScrollBarMode::AlwaysOn == mVScrollMode ) {
mVScroll->setVisible( !mTerm->getTerminal()->tisaltscr() )
->setEnabled( !mTerm->getTerminal()->tisaltscr() );
mVScroll->setVisible( !terminal->isAltScr() )->setEnabled( !terminal->isAltScr() );
} else if ( ScrollBarMode::Auto == mVScrollMode ) {
if ( mViewType == ScrollViewType::Overlay && mMouseClock.getElapsedTime() > Seconds( 1 ) &&
!mVScroll->isDragging() )
@@ -493,8 +514,8 @@ Uint32 UITerminal::onKeyUp( const KeyEvent& ) {
Uint32 UITerminal::onMouseMove( const Vector2i& position, const Uint32& flags ) {
if ( mViewType == ScrollViewType::Overlay && ScrollBarMode::Auto == mVScrollMode ) {
mMouseClock.restart();
bool visible = !mTerm->getTerminal()->tisaltscr() && getContentSize() > getVisibleArea() &&
!mTerm->getTerminal()->hasSelection();
bool visible =
!mTerm->isAltScr() && getContentSize() > getVisibleArea() && !mTerm->hasSelection();
mVScroll->setVisible( visible )->setEnabled( visible );
}
@@ -563,8 +584,8 @@ void UITerminal::createDefaultContextMenuOptions( UIPopUpMenu* menu ) {
if ( !mCreateDefaultContextMenuOptions )
return;
if ( mTerm->getTerminal()->hasSelection() ) {
auto sel( mTerm->getTerminal()->getSelection() );
if ( mTerm->hasSelection() ) {
auto sel( mTerm->getSelection() );
if ( LuaPattern::hasMatches( sel, LuaPattern::getURIPattern() ) ) {
menuAdd( menu, i18n( "uiterminal_open_link", "Open Link" ), "earth",
@@ -573,7 +594,7 @@ void UITerminal::createDefaultContextMenuOptions( UIPopUpMenu* menu ) {
}
menuAdd( menu, i18n( "uiterminal_copy", "Copy" ), "copy", "terminal-copy" )
->setEnabled( mTerm->getTerminal() && mTerm->getTerminal()->hasSelection() );
->setEnabled( mTerm->hasSelection() );
menuAdd( menu, i18n( "uiterminal_paste", "Paste" ), "paste", "terminal-paste" )
->setEnabled( !getUISceneNode()->getWindow()->getClipboard()->getText().empty() );
}
@@ -628,10 +649,14 @@ bool UITerminal::onCreateContextMenu( const Vector2i& position, const Uint32& fl
void UITerminal::restart() {
auto win = SceneManager::instance()->getUISceneNode()->getWindow();
if ( mTerm && mTerminalEventCallbackId )
mTerm->popEventCallback( mTerminalEventCallbackId );
mTerm = TerminalDisplay::create( win, mTerm->getFont(), mTerm->getFontSize(), mTerm->getSize(),
mTerm->getProgram(), mTerm->getArgs(), mTerm->getWorkingDir(),
mTerm->getHistorySize(), nullptr, mTerm->useFrameBuffer(),
mTerm->getKeepAlive(), mTerm->getEnv() );
mTerminalEventCallbackId = 0;
registerNewTerminal();
syncFontRenderingConfig();
}
+302 -13
View File
@@ -1,9 +1,13 @@
#include "utest.hpp"
#include <atomic>
#include <chrono>
#include <eterm/system/iprocess.hpp>
#include <eterm/terminal/ipseudoterminal.hpp>
#include <eterm/terminal/iterminaldisplay.hpp>
#include <eterm/terminal/terminalcontroller.hpp>
#include <eterm/terminal/terminalemulator.hpp>
#include <limits>
#include <thread>
using namespace eterm::Terminal;
using namespace eterm::System;
@@ -11,7 +15,11 @@ using namespace eterm::System;
class MockPty : public IPseudoTerminal {
public:
std::string mBuffer;
std::string mWrites;
bool mLoopWrites{ true };
size_t mMaxRead{ std::numeric_limits<size_t>::max() };
size_t mReadOffset{ 0 };
std::atomic<size_t> mBytesRead{ 0 };
int mCols = 80;
int mRows = 24;
int getNumColumns() const override { return mCols; }
@@ -23,30 +31,313 @@ class MockPty : public IPseudoTerminal {
}
bool isTTY() const override { return true; }
int write( const char* s, size_t n ) override {
mBuffer.append( s, n );
mWrites.append( s, n );
if ( mLoopWrites )
mBuffer.append( s, n );
return n;
}
int read( char* buf, size_t n, bool ) override {
if ( mBuffer.empty() )
if ( mReadOffset == mBuffer.size() )
return 0;
size_t toRead = std::min( { n, mBuffer.size(), mMaxRead } );
memcpy( buf, mBuffer.data(), toRead );
mBuffer.erase( 0, toRead );
size_t toRead = std::min( { n, mBuffer.size() - mReadOffset, mMaxRead } );
memcpy( buf, mBuffer.data() + mReadOffset, toRead );
mReadOffset += toRead;
mBytesRead.fetch_add( toRead, std::memory_order_relaxed );
return toRead;
}
};
class MockProcess : public IProcess {
public:
bool mExited{ false };
std::atomic<bool> mExited{ false };
void checkExitStatus() override {}
bool hasExited() const override { return mExited; }
bool hasExited() const override { return mExited.load(); }
int getExitCode() const override { return 0; }
void terminate() override {}
void waitForExit() override {}
int pid() override { return 123; }
};
static std::shared_ptr<const TerminalSnapshot>
waitForSnapshot( const std::shared_ptr<TerminalController>& controller,
const std::function<bool( const TerminalSnapshot& )>& predicate,
std::chrono::milliseconds timeout = std::chrono::milliseconds( 1000 ) ) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while ( std::chrono::steady_clock::now() < deadline ) {
auto snapshot = controller->snapshot();
if ( snapshot && predicate( *snapshot ) )
return snapshot;
std::this_thread::sleep_for( std::chrono::milliseconds( 1 ) );
}
return nullptr;
}
UTEST( eterm_controller, command_wakeup_and_snapshot_immutability ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto controller = TerminalController::create( std::move( pty ), std::move( process ), 100 );
ASSERT_TRUE( controller != nullptr );
controller->writeRaw( "ABC" );
auto first = waitForSnapshot( controller, []( const TerminalSnapshot& snapshot ) {
return snapshot.cells.size() >= 3 && snapshot.cells[0].u == 'A' &&
snapshot.cells[1].u == 'B' && snapshot.cells[2].u == 'C';
} );
ASSERT_TRUE( first != nullptr );
const Uint64 firstGeneration = first->generation;
controller->writeRaw( "\rXYZ" );
auto second =
waitForSnapshot( controller, [firstGeneration]( const TerminalSnapshot& snapshot ) {
return snapshot.generation > firstGeneration && snapshot.cells[0].u == 'X';
} );
ASSERT_TRUE( second != nullptr );
EXPECT_EQ( static_cast<Rune>( 'A' ), first->cells[0].u );
EXPECT_TRUE( second->generation > first->generation );
}
UTEST( eterm_controller, skipped_snapshot_generation_requires_full_redraw ) {
TerminalSnapshot snapshot;
snapshot.generation = 42;
EXPECT_TRUE( snapshot.dirtyRowsFollow( 41 ) );
EXPECT_FALSE( snapshot.dirtyRowsFollow( 40 ) );
}
UTEST( eterm_controller, ordered_selection_request ) {
auto pty = std::make_unique<MockPty>();
pty->mBuffer = "ordered selection";
auto process = std::make_unique<MockProcess>();
auto controller = TerminalController::create( std::move( pty ), std::move( process ), 100 );
ASSERT_TRUE( waitForSnapshot( controller, []( const TerminalSnapshot& snapshot ) {
return !snapshot.cells.empty() && snapshot.cells[0].u == 'o';
} ) != nullptr );
controller->selectionStart( 0, 0, 0 );
controller->selectionExtend( 6, 0, SEL_REGULAR, false );
auto selection = controller->requestSelection();
ASSERT_TRUE( selection.has_value() );
EXPECT_STDSTREQ( "ordered", *selection );
}
UTEST( eterm_controller, loaded_command_latency_stays_bounded ) {
auto pty = std::make_unique<MockPty>();
pty->mBuffer.assign( 32 * 1024 * 1024, 'L' );
pty->mMaxRead = 64;
MockPty* ptyPtr = pty.get();
auto process = std::make_unique<MockProcess>();
auto controller = TerminalController::create( std::move( pty ), std::move( process ), 100 );
const auto readDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds( 100 );
while ( ptyPtr->mBytesRead.load( std::memory_order_relaxed ) == 0 &&
std::chrono::steady_clock::now() < readDeadline )
std::this_thread::yield();
ASSERT_TRUE( ptyPtr->mBytesRead.load( std::memory_order_relaxed ) > 0 );
const auto start = std::chrono::steady_clock::now();
auto selection = controller->requestSelection();
const auto latency = std::chrono::steady_clock::now() - start;
EXPECT_TRUE( selection.has_value() );
EXPECT_TRUE( latency < std::chrono::milliseconds( 50 ) );
}
UTEST( eterm_controller, resize_and_output_are_serialized ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto controller = TerminalController::create( std::move( pty ), std::move( process ), 100 );
controller->resize( 40, 12 );
controller->writeRaw( "after resize" );
auto snapshot = waitForSnapshot( controller, []( const TerminalSnapshot& value ) {
return value.columns == 40 && value.rows == 12 && !value.cells.empty() &&
value.cells[0].u == 'a';
} );
ASSERT_TRUE( snapshot != nullptr );
EXPECT_EQ( static_cast<size_t>( 40 * 12 ), snapshot->cells.size() );
}
UTEST( eterm_controller, scroll_snapshots_acknowledge_the_latest_ordered_command ) {
auto pty = std::make_unique<MockPty>();
for ( int line = 0; line < 80; ++line )
pty->mBuffer += "history " + std::to_string( line ) + "\r\n";
auto process = std::make_unique<MockProcess>();
auto controller = TerminalController::create( std::move( pty ), std::move( process ), 100 );
ASSERT_TRUE( waitForSnapshot( controller, []( const TerminalSnapshot& snapshot ) {
return snapshot.historyLength >= 25;
} ) != nullptr );
const Uint64 firstCommand = controller->scrollTo( 10 );
const Uint64 secondCommand = controller->scrollTo( 25 );
EXPECT_EQ( firstCommand + 1, secondCommand );
auto acknowledged =
waitForSnapshot( controller, [secondCommand]( const TerminalSnapshot& snapshot ) {
return snapshot.lastAppliedScrollCommand == secondCommand;
} );
ASSERT_TRUE( acknowledged != nullptr );
EXPECT_EQ( 25, acknowledged->scrollPosition );
}
UTEST( eterm_controller, presentation_rate_is_applied_on_the_worker ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto controller = TerminalController::create( std::move( pty ), std::move( process ), 100 );
controller->setPresentationRate( 120 );
ASSERT_TRUE( waitForSnapshot( controller, []( const TerminalSnapshot& snapshot ) {
return snapshot.presentationRate == 120;
} ) != nullptr );
}
UTEST( eterm_controller, focus_reporting_is_ordered_on_worker ) {
auto pty = std::make_unique<MockPty>();
pty->mBuffer = "\033[?1004h";
pty->mLoopWrites = false;
MockPty* ptyPtr = pty.get();
auto process = std::make_unique<MockProcess>();
auto controller = TerminalController::create( std::move( pty ), std::move( process ), 100 );
auto enabled = waitForSnapshot( controller, []( const TerminalSnapshot& snapshot ) {
return snapshot.windowMode & MODE_FOCUS;
} );
ASSERT_TRUE( enabled != nullptr );
controller->setFocus( false );
auto unfocused = waitForSnapshot( controller, [enabled]( const TerminalSnapshot& snapshot ) {
return snapshot.generation > enabled->generation && !( snapshot.windowMode & MODE_FOCUSED );
} );
ASSERT_TRUE( unfocused != nullptr );
ASSERT_TRUE( ptyPtr->mWrites.size() >= 3 );
EXPECT_STDSTREQ( "\033[O", ptyPtr->mWrites.substr( ptyPtr->mWrites.size() - 3 ) );
controller->setFocus( true );
ASSERT_TRUE( waitForSnapshot( controller, [unfocused]( const TerminalSnapshot& snapshot ) {
return snapshot.generation > unfocused->generation &&
snapshot.windowMode & MODE_FOCUSED;
} ) != nullptr );
ASSERT_TRUE( ptyPtr->mWrites.size() >= 3 );
EXPECT_STDSTREQ( "\033[I", ptyPtr->mWrites.substr( ptyPtr->mWrites.size() - 3 ) );
}
UTEST( eterm_controller, replaceable_events_coalesce_without_losing_ordered_events ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto controller = TerminalController::create( std::move( pty ), std::move( process ), 100 );
controller->drainEvents();
controller->writeRaw( "\033]0;first\a\033]0;second\a" );
ASSERT_TRUE( waitForSnapshot( controller, []( const TerminalSnapshot& snapshot ) {
return snapshot.title == "second";
} ) != nullptr );
int titleEvents = 0;
std::string title;
for ( auto& event : controller->drainEvents() ) {
if ( event.type == TerminalController::EventType::Title ) {
++titleEvents;
title = std::move( event.data );
}
}
EXPECT_EQ( 1, titleEvents );
EXPECT_STDSTREQ( "second", title );
}
UTEST( eterm_controller, ordered_events_are_coalescing_barriers ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto controller = TerminalController::create( std::move( pty ), std::move( process ), 100 );
controller->drainEvents();
controller->writeRaw( "\033]0;before\a\a\033]0;after\a" );
ASSERT_TRUE( waitForSnapshot( controller, []( const TerminalSnapshot& snapshot ) {
return snapshot.title == "after";
} ) != nullptr );
std::vector<TerminalController::EventType> semanticEvents;
for ( const auto& event : controller->drainEvents() ) {
if ( event.type == TerminalController::EventType::Title ||
event.type == TerminalController::EventType::Bell )
semanticEvents.emplace_back( event.type );
}
ASSERT_EQ( static_cast<size_t>( 3 ), semanticEvents.size() );
EXPECT_EQ( TerminalController::EventType::Title, semanticEvents[0] );
EXPECT_EQ( TerminalController::EventType::Bell, semanticEvents[1] );
EXPECT_EQ( TerminalController::EventType::Title, semanticEvents[2] );
}
UTEST( eterm_controller, reset_is_ordered_and_publishes_immediately ) {
auto pty = std::make_unique<MockPty>();
pty->mBuffer = "content";
auto process = std::make_unique<MockProcess>();
auto controller = TerminalController::create( std::move( pty ), std::move( process ), 100 );
auto populated = waitForSnapshot( controller, []( const TerminalSnapshot& snapshot ) {
return !snapshot.cells.empty() && snapshot.cells[0].u == 'c';
} );
ASSERT_TRUE( populated != nullptr );
controller->reset();
auto reset = waitForSnapshot( controller, [populated]( const TerminalSnapshot& snapshot ) {
return snapshot.generation > populated->generation && !snapshot.cells.empty() &&
snapshot.cells[0].u == ' ';
} );
ASSERT_TRUE( reset != nullptr );
}
UTEST( eterm_controller, process_exit_follows_buffered_output_and_final_snapshot ) {
auto pty = std::make_unique<MockPty>();
pty->mMaxRead = 1;
MockPty* ptyPtr = pty.get();
auto process = std::make_unique<MockProcess>();
MockProcess* processPtr = process.get();
auto controller = TerminalController::create( std::move( pty ), std::move( process ), 100 );
controller->setDataEventsEnabled( true );
controller->writeRaw( std::string( 3 * 1024, 'Q' ) );
const auto readDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds( 100 );
while ( ptyPtr->mBytesRead.load( std::memory_order_relaxed ) == 0 &&
std::chrono::steady_clock::now() < readDeadline )
std::this_thread::yield();
ASSERT_TRUE( ptyPtr->mBytesRead.load( std::memory_order_relaxed ) > 0 );
processPtr->mExited.store( true );
auto finalSnapshot = waitForSnapshot(
controller, []( const TerminalSnapshot& snapshot ) { return snapshot.processExited; } );
ASSERT_TRUE( finalSnapshot != nullptr );
EXPECT_EQ( 0, finalSnapshot->exitCode );
size_t bytesRead = 0;
bool sawExit = false;
for ( auto& event : controller->drainEvents() ) {
if ( event.type == TerminalController::EventType::Data )
bytesRead += event.data.size();
else if ( event.type == TerminalController::EventType::ProcessExit )
sawExit = true;
}
EXPECT_EQ( static_cast<size_t>( 3 * 1024 ), bytesRead );
EXPECT_TRUE( sawExit );
}
UTEST( eterm_controller, repeated_create_destroy_and_concurrent_workers ) {
for ( int iteration = 0; iteration < 16; ++iteration ) {
std::vector<std::shared_ptr<TerminalController>> controllers;
for ( int terminal = 0; terminal < 4; ++terminal ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto controller =
TerminalController::create( std::move( pty ), std::move( process ), 100 );
controller->writeRaw( "worker" + std::to_string( terminal ) );
controllers.emplace_back( std::move( controller ) );
}
for ( const auto& controller : controllers ) {
EXPECT_TRUE( waitForSnapshot( controller, []( const TerminalSnapshot& snapshot ) {
return !snapshot.cells.empty() && snapshot.cells[0].u == 'w';
} ) != nullptr );
}
}
}
UTEST( eterm_controller, concurrent_shutdown_is_idempotent ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto controller = TerminalController::create( std::move( pty ), std::move( process ), 100 );
std::vector<std::thread> shutdownThreads;
for ( int thread = 0; thread < 4; ++thread )
shutdownThreads.emplace_back( [controller] { controller->shutdown(); } );
for ( auto& thread : shutdownThreads )
thread.join();
controller->shutdown();
}
class MockDisplay : public ITerminalDisplay {
public:
int mDrawLines{ 0 };
@@ -120,14 +411,12 @@ UTEST( eterm, sustained_saturated_reads_present_periodically ) {
term->update();
display->mDrawLines = 0;
std::string output( 33 * 1024, 'A' );
std::string output( 1024 * 1024, 'A' );
term->write( output.data(), output.size() );
for ( int batch = 0; batch < 31; ++batch ) {
EXPECT_FALSE( term->update() );
EXPECT_EQ( 0, display->mDrawLines );
}
EXPECT_FALSE( term->update() );
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds( 100 );
while ( display->mDrawLines == 0 && std::chrono::steady_clock::now() < deadline )
term->update();
EXPECT_TRUE( display->mDrawLines > 0 );
}
+2 -2
View File
@@ -77,7 +77,7 @@ App* App::instance() {
bool App::isAnyTerminalDirty() const {
bool dirty = false;
mSplitter->forEachWidgetTypeStoppable( UI_TYPE_TERMINAL, [&dirty]( UIWidget* widget ) -> bool {
ProcessID pid = widget->asType<UITerminal>()->getTerm()->getTerminal()->getProcess()->pid();
ProcessID pid = widget->asType<UITerminal>()->getTerm()->getProcessId();
if ( Sys::processHasChildren( pid ) ) {
dirty = true;
return true;
@@ -5063,7 +5063,7 @@ void App::init( InitParameters& params ) {
focusTabBehavior, onMsgBoxCloseCb );
} else if ( mConfig.term.warnBeforeClosingTab && widget->isType( UI_TYPE_TERMINAL ) ) {
UITerminal* term = widget->asType<UITerminal>();
ProcessID pid = term->getTerm()->getTerminal()->getProcess()->pid();
ProcessID pid = term->getTerm()->getProcessId();
std::string msgBoxId = String::format( "msgbox_%p", this );
if ( Sys::processHasChildren( pid ) ) {
if ( nullptr != getUISceneNode()->find( msgBoxId ) )
@@ -126,8 +126,11 @@ void AgentSession::listSessions(
void AgentSession::stop() {
for ( auto& term : mTerminals ) {
if ( term.second.display && term.second.eventCbId )
term.second.display->popEventCallback( term.second.eventCbId );
if ( term.second.display ) {
if ( term.second.eventCbId )
term.second.display->popEventCallback( term.second.eventCbId );
term.second.display->setDataCallback( {} );
}
}
mTerminals.clear();
@@ -171,10 +174,9 @@ void AgentSession::cancel() {
void AgentSession::setTerminalData( const std::string& terminalId, UITerminal* uiTerm ) {
auto& termData = mTerminals[terminalId];
termData.display = uiTerm->getTerm();
termData.emulator = uiTerm->getTerm()->getTerminal();
termData.uiTerm = uiTerm;
if ( termData.emulator ) {
termData.emulator->setDataCb( [this, terminalId]( const char* data, size_t size ) {
if ( termData.display ) {
termData.display->setDataCallback( [this, terminalId]( const char* data, size_t size ) {
auto it = mTerminals.find( terminalId );
if ( it != mTerminals.end() ) {
it->second.outputBuffer.append( data, size );
@@ -188,7 +190,7 @@ void AgentSession::setTerminalData( const std::string& terminalId, UITerminal* u
auto it = mTerminals.find( terminalId );
if ( it != mTerminals.end() ) {
WaitForTerminalExitResponse res;
res.exitCode = it->second.emulator ? it->second.emulator->getExitCode() : 0;
res.exitCode = it->second.display ? it->second.display->getExitCode() : 0;
auto callbacks = std::move( it->second.exitCallbacks );
it->second.exitCallbacks.clear();
for ( const auto& cb : callbacks ) {
@@ -268,9 +270,9 @@ void AgentSession::setupClient() {
res.truncated = false;
}
if ( it->second.emulator && it->second.emulator->hasExited() ) {
if ( it->second.display && it->second.display->hasTerminated() ) {
TerminalExitStatus status;
status.exitCode = it->second.emulator->getExitCode();
status.exitCode = it->second.display->getExitCode();
res.exitStatus = status;
}
}
@@ -279,8 +281,8 @@ void AgentSession::setupClient() {
mClient->onKillTerminal = [this]( const KillTerminalRequest& req, auto cb ) {
auto it = mTerminals.find( req.terminalId );
if ( it != mTerminals.end() && it->second.emulator ) {
it->second.emulator->terminate();
if ( it != mTerminals.end() && it->second.display ) {
it->second.display->terminate();
}
cb( KillTerminalResponse() );
};
@@ -290,8 +292,10 @@ void AgentSession::setupClient() {
if ( it != mTerminals.end() ) {
if ( it->second.display && it->second.eventCbId )
it->second.display->popEventCallback( it->second.eventCbId );
if ( it->second.emulator )
it->second.emulator->terminate();
if ( it->second.display ) {
it->second.display->setDataCallback( {} );
it->second.display->terminate();
}
if ( it->second.uiTerm )
it->second.uiTerm->close();
mTerminals.erase( it );
@@ -301,10 +305,10 @@ void AgentSession::setupClient() {
mClient->onWaitForTerminalExit = [this]( const WaitForTerminalExitRequest& req, auto cb ) {
auto it = mTerminals.find( req.terminalId );
if ( it != mTerminals.end() && it->second.emulator ) {
if ( it->second.emulator->hasExited() ) {
if ( it != mTerminals.end() && it->second.display ) {
if ( it->second.display->hasTerminated() ) {
WaitForTerminalExitResponse res;
res.exitCode = it->second.emulator->getExitCode();
res.exitCode = it->second.display->getExitCode();
cb( res );
} else {
it->second.exitCallbacks.push_back( cb );
@@ -2,7 +2,6 @@
#include "acpclient.hpp"
#include <eepp/system/threadpool.hpp>
#include <eterm/terminal/terminalemulator.hpp>
#include <eterm/ui/uiterminal.hpp>
#include <functional>
#include <memory>
@@ -60,7 +59,6 @@ class AgentSession {
struct TermData {
std::shared_ptr<TerminalDisplay> display;
std::shared_ptr<TerminalEmulator> emulator;
UITerminal* uiTerm{ nullptr };
std::string outputBuffer;
Uint32 eventCbId{ 0 };
@@ -2360,10 +2360,7 @@ void DebuggerPlugin::run( const std::string& debugger, ProtocolSettings&& protoc
plugin->getPluginContext()->getTerminalManager()->createTerminalInSplitter(
cwd, cmd, args, env, false, false );
doneFn( term && term->getTerm() && term->getTerm()->getTerminal() &&
term->getTerm()->getTerminal()->getProcess()
? term->getTerm()->getTerminal()->getProcess()->pid()
: 0 );
doneFn( term && term->getTerm() ? term->getTerm()->getProcessId() : 0 );
} else {
std::string fcmd = cmd + ( !args.empty() ? " " : "" ) + String::join( args, ' ' );
doneFn( plugin->getPluginContext()->getTerminalManager()->openInExternalTerminal(
+5 -5
View File
@@ -27,7 +27,7 @@ bool StatusTerminalController::tryTabClose( UITab* tab ) {
if ( mContext->getConfig().term.warnBeforeClosingTab && widget->isType( UI_TYPE_TERMINAL ) ) {
UITerminal* term = widget->asType<UITerminal>();
ProcessID pid = term->getTerm()->getTerminal()->getProcess()->pid();
ProcessID pid = term->getTerm()->getProcessId();
if ( Sys::processHasChildren( pid ) ) {
UIMessageBox* msgBox =
UIMessageBox::New( UIMessageBox::OK_CANCEL,
@@ -141,8 +141,7 @@ UITerminal* StatusTerminalController::createTerminal(
mContext->getTerminalFont() ? mContext->getTerminalFont() : mContext->getFontMono(),
mContext->termConfig().fontSize.asPixels( 0, Sizef(), mContext->getDisplayDPI() ),
initialSize, program, args, env,
!workingDir.empty() ? workingDir
: mContext->getTerminalManager()->getSelectedWorkingDir(),
!workingDir.empty() ? workingDir : mContext->getTerminalManager()->getSelectedWorkingDir(),
10000, nullptr, false );
if ( term == nullptr || term->getTerm() == nullptr ) {
@@ -154,7 +153,7 @@ UITerminal* StatusTerminalController::createTerminal(
const auto& currentTerminalColorScheme =
mContext->getTerminalManager()->getTerminalCurrentColorScheme();
auto csIt = terminalColorSchemes.find( currentTerminalColorScheme );
term->getTerm()->getTerminal()->setAllowMemoryTrimnming( true );
term->getTerm()->setAllowMemoryTrimming( true );
term->getTerm()->setCursorMode( mContext->termConfig().cursorStyle );
term->setExclusiveMode( mContext->termConfig().exclusiveMode );
term->setScrollViewType( mContext->termConfig().scrollBarType );
@@ -242,7 +241,8 @@ UITerminal* StatusTerminalController::createTerminal(
UIIcon* icon = mUISceneNode->findIcon( "terminal" );
auto tab = mTabWidget->add(
program, term, icon != nullptr ? icon->createDrawable( PixelDensity::dpToPxI( 12 ) ) : nullptr );
program, term,
icon != nullptr ? icon->createDrawable( PixelDensity::dpToPxI( 12 ) ) : nullptr );
term->setData( (UintPtr)tab );
term->on( Event::OnTitleChange, [tab, term]( auto ) { tab->setText( term->getTitle() ); } );
+1 -1
View File
@@ -658,7 +658,7 @@ UITerminal* TerminalManager::createNewTerminal(
} );
term->setTitle( title );
auto csIt = mTerminalColorSchemes.find( mTerminalCurrentColorScheme );
term->getTerm()->getTerminal()->setAllowMemoryTrimnming( true );
term->getTerm()->setAllowMemoryTrimming( true );
term->getTerm()->setKeepAlive( !mApp->getConfig().term.closeTerminalTabOnExit );
term->getTerm()->pushEventCallback( [this, term]( const TerminalDisplay::Event& event ) {
if ( event.type == TerminalDisplay::EventType::PROCESS_EXIT &&
+2 -3
View File
@@ -149,8 +149,7 @@ void inputCallback( InputEvent* event ) {
}
bool onCloseRequestCallback( EE::Window::Window* ) {
if ( warnBeforeClose &&
Sys::processHasChildren( terminal->getTerminal()->getProcess()->pid() ) ) {
if ( warnBeforeClose && Sys::processHasChildren( terminal->getProcessId() ) ) {
displayingWarnBeforeClose = true;
needsRedraw = true;
return false;
@@ -366,7 +365,7 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) {
terminal->setFontHinting( fontHinting.Get() );
terminal->setFontAntialiasing( fontAntialiasing.Get() );
terminal->getTerminal()->setAllowMemoryTrimnming( true );
terminal->setAllowMemoryTrimming( true );
terminal->setCursorMode( cursorStyle.Get() );
terminal->pushEventCallback( [&closeOnExit]( const TerminalDisplay::Event& event ) {
if ( event.type == TerminalDisplay::EventType::TITLE ) {
+516
View File
@@ -0,0 +1,516 @@
#include <args/args.hxx>
#include <eepp/core/small_vector.hpp>
#include <eepp/ee.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uilinearlayout.hpp>
#include <eepp/ui/uimessagebox.hpp>
#include <eepp/ui/uitabwidget.hpp>
#include <eterm/ui/uiterminal.hpp>
#include <algorithm>
#include <iostream>
#include <iterator>
#include <map>
#include <unordered_map>
using namespace EE;
using namespace EE::Graphics;
using namespace EE::Scene;
using namespace EE::System;
using namespace EE::UI;
using namespace EE::Window;
using namespace eterm::Terminal;
using namespace eterm::UI;
namespace {
struct TerminalLaunchConfig {
std::string program;
std::vector<std::string> arguments;
std::string workingDirectory;
std::string executeInShell;
size_t historySize{ 10000 };
TerminalCursorMode cursorStyle{ TerminalCursorMode::SteadyUnderline };
FontHinting fontHinting{ FontHinting::Full };
FontAntialiasing fontAntialiasing{ FontAntialiasing::Grayscale };
bool useFrameBuffer{ false };
bool keepAlive{ true };
bool closeOnExit{ false };
};
EE::Window::Window* appWindow{ nullptr };
UISceneNode* scene{ nullptr };
UILinearLayout* mainLayout{ nullptr };
UITabWidget* tabs{ nullptr };
FontTrueType* terminalFont{ nullptr };
UIMessageBox* closeDialog{ nullptr };
UITab* closeDialogTab{ nullptr };
TerminalLaunchConfig terminalConfig;
std::map<std::string, TerminalColorScheme> terminalColorSchemes;
const TerminalColorScheme* selectedColorScheme{ nullptr };
Float terminalFontSize{ 12 };
bool warnBeforeClose{ false };
bool closeApproved{ false };
bool benchmarkMode{ false };
Clock secondsCounter;
SmallVector<UITab*, 8> pendingExitCloseTabs;
std::string getResourcePath() {
std::string resPath = Sys::getProcessPath();
#if EE_PLATFORM == EE_PLATFORM_MACOS
if ( String::contains( resPath, "ecode.app" ) ) {
resPath = FileSystem::getCurrentWorkingDirectory();
FileSystem::dirAddSlashAtEnd( resPath );
}
#elif EE_PLATFORM == EE_PLATFORM_LINUX
if ( String::contains( resPath, ".mount_" ) ) {
resPath = FileSystem::getCurrentWorkingDirectory();
FileSystem::dirAddSlashAtEnd( resPath );
}
#elif EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN
resPath += "eterm/";
#endif
resPath += "assets";
FileSystem::dirAddSlashAtEnd( resPath );
return resPath;
}
void loadColorSchemes( const std::string& resPath ) {
auto colorSchemes =
TerminalColorScheme::loadFromFile( resPath + "colorschemes/terminalcolorschemes.conf" );
const std::string configColorSchemesPath =
Sys::getConfigPath( "eterm" ) + FileSystem::getOSSlash() + "colorschemes";
if ( FileSystem::isDirectory( configColorSchemesPath ) ) {
for ( const auto& file : FileSystem::filesGetInPath( configColorSchemesPath ) ) {
auto fileColorSchemes = TerminalColorScheme::loadFromFile( file );
colorSchemes.insert( colorSchemes.end(),
std::make_move_iterator( fileColorSchemes.begin() ),
std::make_move_iterator( fileColorSchemes.end() ) );
}
}
for ( auto& colorScheme : colorSchemes ) {
std::string name = colorScheme.getName();
terminalColorSchemes.emplace( std::move( name ), std::move( colorScheme ) );
}
}
UITerminal* terminalFromTab( UITab* tab ) {
return tab && tab->getOwnedWidget() && tab->getOwnedWidget()->isType( UI_TYPE_TERMINAL )
? tab->getOwnedWidget()->asType<UITerminal>()
: nullptr;
}
void updateWindowTitle() {
if ( !appWindow )
return;
std::string title{ "eterm" };
if ( tabs ) {
if ( auto* terminal = terminalFromTab( tabs->getTabSelected() );
terminal && !terminal->getTitle().empty() ) {
title += " - ";
title += terminal->getTitle();
}
}
if ( benchmarkMode ) {
title += " - ";
title += String::toString( appWindow->getFPS() );
title += " FPS";
}
appWindow->setTitle( title );
}
bool hasRunningChildren( UITab* tab ) {
auto* terminal = terminalFromTab( tab );
return terminal && terminal->getTerm() &&
Sys::processHasChildren( terminal->getTerm()->getProcessId() );
}
void closeTab( UITab* tab ) {
if ( !tabs || !tab || tabs->getTabIndex( tab ) == eeINDEX_NOT_FOUND )
return;
tabs->removeTab( tab );
}
void queueExitCloseTab( UITab* tab ) {
if ( tab && std::find( pendingExitCloseTabs.begin(), pendingExitCloseTabs.end(), tab ) ==
pendingExitCloseTabs.end() ) {
pendingExitCloseTabs.emplace_back( tab );
}
}
void queueExitedTabs() {
if ( !terminalConfig.closeOnExit )
return;
tabs->forEachTab( []( UITab* tab ) {
auto* terminal = terminalFromTab( tab );
if ( !terminal || !terminal->getTerm() )
return;
const auto& controller = terminal->getTerm()->getController();
auto snapshot = controller ? controller->snapshot() : nullptr;
if ( snapshot && snapshot->processExited )
queueExitCloseTab( tab );
} );
}
void requestCloseTab( UITab* tab ) {
if ( !warnBeforeClose || !hasRunningChildren( tab ) ) {
closeTab( tab );
return;
}
if ( closeDialog )
return;
closeDialog = UIMessageBox::New(
UIMessageBox::OK_CANCEL,
"Are you sure you want to close this terminal?\nIt is still running a process." );
closeDialogTab = tab;
closeDialog->setTitle( "eterm" );
closeDialog->on( Event::OnConfirm, []( const Event* ) { closeTab( closeDialogTab ); } );
closeDialog->on( Event::OnClose, []( const Event* ) {
closeDialog = nullptr;
closeDialogTab = nullptr;
} );
closeDialog->center();
closeDialog->showWhenReady();
}
void addTabKeyBindings( UITerminal* terminal, UITab* tab );
UITerminal* createTerminal() {
Sizef initialSize{ 16, 16 };
if ( tabs && tabs->getContainerNode() &&
tabs->getContainerNode()->getPixelsSize() != Sizef::Zero ) {
initialSize = tabs->getContainerNode()->getPixelsSize();
}
auto* terminal = UITerminal::New(
terminalFont, terminalFontSize, initialSize, terminalConfig.program,
terminalConfig.arguments, {}, terminalConfig.workingDirectory, terminalConfig.historySize,
nullptr, terminalConfig.useFrameBuffer, terminalConfig.keepAlive );
if ( !terminal || !terminal->getTerm() ) {
eeSAFE_DELETE( terminal );
return nullptr;
}
terminal->getTerm()->setAllowMemoryTrimming( true );
terminal->getTerm()->setCursorMode( terminalConfig.cursorStyle );
terminal->getTerm()->setFontHinting( terminalConfig.fontHinting );
terminal->getTerm()->setFontAntialiasing( terminalConfig.fontAntialiasing );
if ( selectedColorScheme )
terminal->setColorScheme( *selectedColorScheme );
auto* tab = tabs->add( "Terminal", terminal );
addTabKeyBindings( terminal, tab );
terminal->on( Event::OnTitleChange, [tab, terminal]( const Event* ) {
tab->setText( terminal->getTitle().empty() ? "Terminal" : terminal->getTitle() );
if ( tabs->getTabSelected() == tab )
updateWindowTitle();
} );
terminal->getTerm()->pushEventCallback( [tab]( const TerminalDisplay::Event& event ) {
if ( terminalConfig.closeOnExit && event.type == TerminalDisplay::EventType::PROCESS_EXIT )
queueExitCloseTab( tab );
} );
tabs->setTabSelected( tab );
if ( !terminalConfig.executeInShell.empty() )
terminal->executeFile( terminalConfig.executeInShell );
terminal->setFocus();
updateWindowTitle();
return terminal;
}
void addTabKeyBindings( UITerminal* terminal, UITab* tab ) {
terminal->setCommand( "create-new-terminal", [] { createTerminal(); } );
terminal->setCommand( "close-tab", [tab] { requestCloseTab( tab ); } );
terminal->setCommand( "next-tab", [] { tabs->focusNextTab(); } );
terminal->setCommand( "previous-tab", [] { tabs->focusPreviousTab(); } );
terminal->addKeyBinding( { KEY_T, KeyMod::getDefaultModifier() | KEYMOD_SHIFT },
"create-new-terminal" );
terminal->addKeyBinding( { KEY_W, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "close-tab" );
terminal->addKeyBinding( { KEY_PAGEDOWN, KEYMOD_CTRL }, "next-tab" );
terminal->addKeyBinding( { KEY_PAGEUP, KEYMOD_CTRL }, "previous-tab" );
terminal->addKeyBinding( { KEY_TAB, KEYMOD_CTRL }, "next-tab" );
terminal->addKeyBinding( { KEY_TAB, KEYMOD_CTRL | KEYMOD_SHIFT }, "previous-tab" );
}
bool closeWindow( EE::Window::Window* ) {
if ( closeApproved || !warnBeforeClose )
return true;
bool running = false;
tabs->forEachTab( [&running]( UITab* tab ) { running |= hasRunningChildren( tab ); } );
if ( !running )
return true;
if ( closeDialog )
return false;
closeDialog = UIMessageBox::New(
UIMessageBox::OK_CANCEL,
"Are you sure you want to close this window? It is still running a process." );
closeDialog->setTitle( "eterm" );
closeDialog->on( Event::OnConfirm, []( const Event* ) {
closeApproved = true;
appWindow->close();
} );
closeDialog->on( Event::OnClose, []( const Event* ) {
closeDialog = nullptr;
closeDialogTab = nullptr;
} );
closeDialog->center();
closeDialog->showWhenReady();
return false;
}
} // namespace
EE_MAIN_FUNC int main( int argc, char* argv[] ) {
#ifdef EE_DEBUG
Log::instance()->setLogToStdOut( true );
Log::instance()->setLiveWrite( true );
#endif
args::ArgumentParser parser( "eterm" );
args::HelpFlag help( parser, "help", "Display this help menu", { 'h', "help" } );
args::ValueFlag<std::string> shell( parser, "shell", "Shell name or path", { 's', "shell" },
"" );
args::ValueFlag<std::string> shellArgs( parser, "shell-args", "Shell command line arguments",
{ "shell-args" }, "" );
args::ValueFlag<size_t> historySize( parser, "scrollback", "Maximum history size (lines)",
{ 'l', "scrollback" }, 10000 );
args::Flag fb( parser, "framebuffer", "Use frame buffer (more memory usage, less CPU usage)",
{ "fb", "framebuffer" } );
args::ValueFlag<std::string> fontPath( parser, "fontpath", "Font path", { 'f', "font" } );
args::ValueFlag<std::string> fallbackFontPath( parser, "fallback-fontpath",
"Fallback Font path", { "fallback-font" } );
args::ValueFlag<Float> fontSize( parser, "fontsize", "Font size (in dp)", { "fontsize" }, 11 );
const std::unordered_map<std::string, FontHinting> fontHintingMap{
{ "none", FontHinting::None },
{ "slight", FontHinting::Slight },
{ "full", FontHinting::Full },
};
args::MapFlag<std::string, FontHinting> fontHinting(
parser, "font-hinting", "Font hinting mode (accepted values: none, slight, full)",
{ "font-hinting" }, fontHintingMap, FontHinting::Full );
const std::unordered_map<std::string, FontAntialiasing> fontAntialiasingMap{
{ "none", FontAntialiasing::None },
{ "grayscale", FontAntialiasing::Grayscale },
{ "subpixel", FontAntialiasing::Subpixel },
};
args::MapFlag<std::string, FontAntialiasing> fontAntialiasing(
parser, "font-antialiasing",
"Font antialiasing mode (accepted values: none, grayscale, subpixel)",
{ "font-antialiasing" }, fontAntialiasingMap, FontAntialiasing::Grayscale );
args::ValueFlag<Float> width( parser, "winwidth", "Window width (in dp)", { "width" }, 1280 );
args::ValueFlag<Float> height( parser, "winheight", "Window height (in dp)", { "height" },
720 );
args::ValueFlag<Float> pixelDensity( parser, "pixel-density",
"Set default application pixel density",
{ 'd', "pixel-density" } );
args::Positional<std::string> wd( parser, "wording-dir", "Working Directory / executable" );
args::Flag closeOnExit( parser, "close-on-exit",
"close the application when the executable exits", { 'c', "close" } );
args::ValueFlag<std::string> executeInShell(
parser, "execute-in-shell", "execute program in shell", { 'e', "execute" }, "" );
args::Flag vsync( parser, "vsync", "Enable vsync", { "vsync" } );
args::ValueFlag<std::string> colorScheme( parser, "color-scheme", "Load color scheme",
{ "color-scheme" }, "" );
args::Flag listColorSchemes( parser, "color-schemes", "Lists color schemes",
{ "list-color-schemes" } );
args::ValueFlag<Uint32> maxFPS( parser, "max-fps",
"Maximum rendering frames per second of the terminal. Default "
"value will be the refresh rate of the screen.",
{ "max-fps" }, 0 );
args::MapFlag<std::string, TerminalCursorMode> cursorStyle(
parser, "cursor-style",
"Sets the cursor-style (accepted values: blinking_block, steady_block, blink_underline, "
"steady_underline, blink_bar, steady_bar)",
{ "cursor-style" }, TerminalCursorHelper::getTerminalCursorModeMap(),
TerminalCursorMode::SteadyUnderline );
args::Flag benchmarkModeFlag(
parser, "benchmark-mode",
"Render as much as possible to measure the rendering performance.", { "benchmark-mode" } );
args::Flag warnBeforeCloseFlag(
parser, "warn-before-closing",
"Prompts for confirmation if a program is still running when closing the terminal.",
{ "warn-before-closing" } );
args::ValueFlag<size_t> initialTabs( parser, "tabs", "Number of initial terminal tabs",
{ "tabs" }, 1 );
try {
parser.ParseCLI( argc, argv );
} catch ( const args::Help& ) {
std::cout << parser;
return EXIT_SUCCESS;
} catch ( const args::ParseError& error ) {
std::cerr << error.what() << std::endl;
std::cerr << parser;
return EXIT_FAILURE;
} catch ( args::ValidationError& error ) {
std::cerr << error.what() << std::endl;
std::cerr << parser;
return EXIT_FAILURE;
}
const std::string initialWorkingDirectory = FileSystem::getCurrentWorkingDirectory();
const std::string resPath = getResourcePath();
if ( listColorSchemes.Get() || colorScheme )
loadColorSchemes( resPath );
if ( listColorSchemes.Get() ) {
std::cout << "Color schemes:\n";
for ( const auto& colorSchemeEntry : terminalColorSchemes )
std::cout << "\t" << colorSchemeEntry.first << "\n";
return EXIT_SUCCESS;
}
if ( colorScheme ) {
auto colorSchemeIt = terminalColorSchemes.find( colorScheme.Get() );
if ( colorSchemeIt != terminalColorSchemes.end() )
selectedColorScheme = &colorSchemeIt->second;
}
DisplayManager* displayManager = Engine::instance()->getDisplayManager();
Display* currentDisplay = displayManager->getDisplayIndex( 0 );
if ( !currentDisplay ) {
std::cerr << "Display not found, exiting" << std::endl;
return EXIT_FAILURE;
}
Sizei windowSize( width.Get(), height.Get() );
const auto displaySize = currentDisplay->getUsableBounds().getSize();
if ( displaySize.getWidth() > 0 && windowSize.getWidth() >= displaySize.getWidth() )
windowSize.setWidth( static_cast<int>( displaySize.getWidth() * 0.8f ) );
if ( displaySize.getHeight() > 0 && windowSize.getHeight() >= displaySize.getHeight() )
windowSize.setHeight( static_cast<int>( displaySize.getHeight() * 0.75f ) );
UIApplication::Settings appSettings;
appSettings.basePath = FileSystem::removeLastFolderFromPath( resPath );
appSettings.pixelDensity =
pixelDensity ? pixelDensity.Get() : currentDisplay->getPixelDensity();
appSettings.fontHinting = fontHinting.Get();
appSettings.fontAntialiasing = fontAntialiasing.Get();
const Int32 frameRateLimit =
benchmarkModeFlag.Get()
? 0
: static_cast<Int32>( maxFPS.Get() ? maxFPS.Get() : currentDisplay->getRefreshRate() );
UIApplication app( WindowSettings( windowSize.getWidth(), windowSize.getHeight(), "eterm",
WindowStyle::Default, WindowBackend::Default, 32,
resPath + "icon/eterm.png",
appSettings.pixelDensity.value() ),
appSettings, ContextSettings( vsync.Get(), frameRateLimit ) );
appWindow = app.getWindow();
scene = app.getUI();
if ( !appWindow || !appWindow->isOpen() || !scene )
return EXIT_FAILURE;
FileSystem::changeWorkingDirectory( initialWorkingDirectory );
appWindow->setClearColor( RGB( 0, 0, 0 ) );
auto& resourceScope = *scene->getResourceScope();
if ( fontPath && FileSystem::fileExists( fontPath.Get() ) ) {
terminalFont = FontTrueType::New( "eterm-monospace", resourceScope ).get();
if ( terminalFont->loadFromFile( fontPath.Get() ) )
FontFamily::loadFromRegular( terminalFont );
else
terminalFont = nullptr;
}
if ( !terminalFont ) {
terminalFont = FontTrueType::New( "eterm-monospace", resourceScope ).get();
if ( !terminalFont->loadFromFile( resPath + "fonts/DejaVuSansMonoNerdFontComplete.ttf" ) ) {
std::cerr << "Could not load terminal font" << std::endl;
return EXIT_FAILURE;
}
FontFamily::loadFromRegular( terminalFont, "DejaVuSansMono" );
}
if ( fallbackFontPath ) {
if ( FileSystem::fileExists( fallbackFontPath.Get() ) ) {
auto fallback = FontTrueType::New( "eterm-fallback-font", resourceScope );
if ( fallback->loadFromFile( fallbackFontPath.Get() ) )
resourceScope.getFontService().addFallbackFont( std::move( fallback ) );
}
} else if ( auto fallback = resourceScope.findFont( "DroidSansFallbackFull" ) ) {
resourceScope.getFontService().addFallbackFont( std::move( fallback ) );
}
const std::string launchPath = wd ? wd.Get() : initialWorkingDirectory;
FileInfo launchFile( launchPath );
const bool launchExecutable = launchFile.isRegularFile() && launchFile.isExecutable();
terminalConfig.program = launchExecutable ? launchFile.getFilepath() : shell.Get();
terminalConfig.arguments =
shellArgs ? String::split( shellArgs.Get() ) : std::vector<std::string>{};
terminalConfig.workingDirectory = launchFile.getDirectoryPath();
terminalConfig.executeInShell = executeInShell.Get();
terminalConfig.historySize = historySize.Get();
terminalConfig.cursorStyle = cursorStyle.Get();
terminalConfig.fontHinting = fontHinting.Get();
terminalConfig.fontAntialiasing = fontAntialiasing.Get();
terminalConfig.useFrameBuffer = fb.Get();
terminalConfig.keepAlive = !launchExecutable && !shell;
terminalConfig.closeOnExit = closeOnExit.Get();
warnBeforeClose = warnBeforeCloseFlag.Get();
benchmarkMode = benchmarkModeFlag.Get();
terminalFontSize = PixelDensity::dpToPx( fontSize.Get() );
mainLayout = UILinearLayout::NewVertical();
mainLayout->setParent( scene->getRoot() );
mainLayout->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent );
mainLayout->setPixelsSize( appWindow->getSize().asFloat() );
tabs = UITabWidget::New();
tabs->setParent( mainLayout );
tabs->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent );
tabs->setTabsClosable( true );
tabs->setAllowRearrangeTabs( true );
tabs->setHideTabBarOnSingleTab( true );
tabs->setTabTryCloseCallback( []( UITab* tab, UITabWidget::FocusTabBehavior ) {
if ( warnBeforeClose && hasRunningChildren( tab ) ) {
requestCloseTab( tab );
return false;
}
return true;
} );
tabs->on( Event::OnTabSelected, []( const Event* ) { updateWindowTitle(); } );
tabs->on( Event::OnTabClosed, []( const Event* event ) {
auto* closedTab = static_cast<const TabEvent*>( event )->getTab();
pendingExitCloseTabs.erase(
std::remove( pendingExitCloseTabs.begin(), pendingExitCloseTabs.end(), closedTab ),
pendingExitCloseTabs.end() );
if ( closeDialogTab == closedTab )
closeDialogTab = nullptr;
if ( tabs->getTabCount() == 0 )
appWindow->close();
else
updateWindowTitle();
} );
mainLayout->updateLayout();
for ( size_t tab = 0; tab < eemax( static_cast<size_t>( 1 ), initialTabs.Get() ); ++tab ) {
if ( !createTerminal() ) {
appWindow->showMessageBox( EE::Window::Window::MessageBoxType::Error, "eterm",
"Operating System not supported." );
return EXIT_FAILURE;
}
}
appWindow->setCloseRequestCallback( &closeWindow );
app.setShowMemoryManagerResult( true );
appWindow->runMainLoop( [] {
appWindow->getInput()->update();
SceneManager::instance()->update();
queueExitedTabs();
// Process-exit events are drained from UITerminal scheduled updates. Removing a tab from
// that callback would mutate the scheduled-widget set while it is being traversed.
while ( !pendingExitCloseTabs.empty() ) {
auto* tab = pendingExitCloseTabs.back();
pendingExitCloseTabs.pop_back();
closeTab( tab );
}
if ( benchmarkMode || scene->invalidated() ) {
appWindow->clear();
SceneManager::instance()->draw();
appWindow->display();
} else {
#if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN
appWindow->getInput()->waitEvent( Milliseconds( appWindow->hasFocus() ? 16 : 100 ) );
#endif
}
if ( benchmarkMode && secondsCounter.getElapsedTime() >= Seconds( 1 ) ) {
updateWindowTitle();
secondsCounter.restart();
}
} );
return EXIT_SUCCESS;
}