From 8bcec85da0fb0bd1dba68d6db86d099d70f8c353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 30 Aug 2026 20:25:44 -0300 Subject: [PATCH] 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 --- premake4.lua | 14 + premake5.lua | 12 + .../eterm/terminal/terminalcontroller.hpp | 248 ++++++++ .../eterm/terminal/terminaldisplay.hpp | 104 ++-- .../eterm/terminal/terminalemulator.hpp | 9 +- .../eterm/include/eterm/ui/uiterminal.hpp | 5 +- .../eterm/src/eterm/system/process.cpp | 157 +++-- .../src/eterm/terminal/pseudoterminal.cpp | 14 +- .../src/eterm/terminal/terminalcontroller.cpp | 555 ++++++++++++++++++ .../src/eterm/terminal/terminaldisplay.cpp | 516 +++++++++------- .../src/eterm/terminal/terminalemulator.cpp | 38 +- src/modules/eterm/src/eterm/ui/uiterminal.cpp | 111 ++-- src/tests/unit_tests/eterm_tests.cpp | 315 +++++++++- src/tools/ecode/ecode.cpp | 4 +- .../plugins/aiassistant/acp/agentsession.cpp | 34 +- .../plugins/aiassistant/acp/agentsession.hpp | 2 - .../ecode/plugins/debugger/debuggerplugin.cpp | 5 +- src/tools/ecode/statusterminalcontroller.cpp | 10 +- src/tools/ecode/terminalmanager.cpp | 2 +- src/tools/eterm/eterm.cpp | 5 +- src/tools/eterm_tabs/eterm_tabs.cpp | 516 ++++++++++++++++ 21 files changed, 2274 insertions(+), 402 deletions(-) create mode 100644 src/modules/eterm/include/eterm/terminal/terminalcontroller.hpp create mode 100644 src/modules/eterm/src/eterm/terminal/terminalcontroller.cpp create mode 100644 src/tools/eterm_tabs/eterm_tabs.cpp diff --git a/premake4.lua b/premake4.lua index 05b45b1ae..48da164d1 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1928,6 +1928,20 @@ solution "eepp" end build_link_configuration( "eterm", false ) + project "eterm-tabs" + set_kind() + language "C++" + files { "src/tools/eterm_tabs/**.cpp" } + links { "eterm-static" } + includedirs { "src/modules/eterm/include/", "src/thirdparty" } + if os.is_real("linux") then + links { "util" } + end + if os.is("haiku") then + links { "bsd" } + end + build_link_configuration( "eterm-tabs", false ) + project "eepp-texturepacker" kind "ConsoleApp" language "C++" diff --git a/premake5.lua b/premake5.lua index ebca58aa2..e0fe748d2 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1958,6 +1958,18 @@ workspace "eepp" filter "system:haiku" links { "bsd" } + project "eterm-tabs" + set_kind() + language "C++" + incdirs { "src/modules/eterm/include/", "src/thirdparty" } + files { "src/tools/eterm_tabs/**.cpp" } + links { "eterm-static" } + build_link_configuration( "eterm-tabs", false ) + filter "system:linux or system:bsd" + links { "util" } + filter "system:haiku" + links { "bsd" } + project "eepp-texturepacker" kind "ConsoleApp" language "C++" diff --git a/src/modules/eterm/include/eterm/terminal/terminalcontroller.hpp b/src/modules/eterm/include/eterm/terminal/terminalcontroller.hpp new file mode 100644 index 000000000..9701da3fb --- /dev/null +++ b/src/modules/eterm/include/eterm/terminal/terminalcontroller.hpp @@ -0,0 +1,248 @@ +#ifndef ETERM_TERMINALCONTROLLER_HPP +#define ETERM_TERMINALCONTROLLER_HPP + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 cells; + std::vector 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 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 { + public: + using PtyPtr = std::unique_ptr; + using ProcPtr = std::unique_ptr; + + 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 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 snapshot() const; + std::vector drainEvents(); + + /** Bounded exact-selection request. Returns no value on timeout or during shutdown. */ + std::optional + 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 response; + }; + struct SelectionClearCommand {}; + struct ResetCommand {}; + struct TerminateCommand {}; + struct AllowTrimCommand : BoolCommand {}; + struct DataEventsCommand : BoolCommand {}; + struct PromptEventsCommand : BoolCommand {}; + struct FocusCommand : BoolCommand {}; + + using Command = + std::variant; + + 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 snapshot ); + + std::shared_ptr mWorkerDisplay; + std::unique_ptr mEmulator; + std::thread mWorker; + std::mutex mShutdownMutex; + mutable std::mutex mCommandMutex; + std::condition_variable mCommandCondition; + std::deque mCommands; + std::mutex mEventMutex; + std::deque mEvents; + std::atomic> mPublishedSnapshot; + std::atomic mShutdownRequested{ false }; + std::atomic mNextScrollCommand{ 0 }; +}; + +}} // namespace eterm::Terminal + +#endif diff --git a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp index 266df855a..f5a64041a 100644 --- a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp @@ -11,9 +11,8 @@ #include #include #include -#include #include -#include +#include #include #include @@ -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 EventFunc; - - static std::shared_ptr - create( EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize, - std::shared_ptr&& terminalEmulator, - const bool& useFrameBuffer = false ); + using DataFunc = std::function; + using PromptStateChangedFunc = std::function; static std::shared_ptr 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& 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& getTerminal() const; + const std::shared_ptr& 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 mBuffer; std::vector mColors; - std::shared_ptr mTerminal; + std::shared_ptr mController; + std::shared_ptr mSnapshot; mutable std::string mClipboardUtf8; - Uint32 mNumCallBacks; + Uint32 mNumCallBacks{ 0 }; std::map 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 ); diff --git a/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp b/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp index 0d7a8e9dd..e99d4197e 100644 --- a/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp @@ -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; diff --git a/src/modules/eterm/include/eterm/ui/uiterminal.hpp b/src/modules/eterm/include/eterm/ui/uiterminal.hpp index ad7e2fc3e..c43f28e4b 100644 --- a/src/modules/eterm/include/eterm/ui/uiterminal.hpp +++ b/src/modules/eterm/include/eterm/ui/uiterminal.hpp @@ -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 mTerm; + Uint32 mTerminalEventCallbackId{ 0 }; UITerminal( const std::shared_ptr& terminalDisplay ); diff --git a/src/modules/eterm/src/eterm/system/process.cpp b/src/modules/eterm/src/eterm/system/process.cpp index fdaf270bb..af83c8c8e 100644 --- a/src/modules/eterm/src/eterm/system/process.cpp +++ b/src/modules/eterm/src/eterm/system/process.cpp @@ -23,6 +23,7 @@ #ifndef _WIN32 #include #include +#include #include #include #include @@ -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& env ) { - const struct passwd* pw; - const char* sh; +struct ProcessLaunchData { + std::string executable; + std::string workingDirectory; + std::vector arguments; + std::vector environmentStorage; + std::vector 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& args, + const std::string& workingDirectory, + const std::unordered_map& env, + ProcessLaunchData& launch ) { + long passwordBufferSize = sysconf( _SC_GETPW_R_SIZE_MAX ); + if ( passwordBufferSize < 0 ) + passwordBufferSize = 16384; + std::vector passwordBuffer( static_cast( 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( program.c_str() ) ); + for ( const auto& argument : args ) + launch.arguments.emplace_back( const_cast( 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::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& 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 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( new Process( pid ) ); diff --git a/src/modules/eterm/src/eterm/terminal/pseudoterminal.cpp b/src/modules/eterm/src/eterm/terminal/pseudoterminal.cpp index eb00c75df..1c20ab538 100644 --- a/src/modules/eterm/src/eterm/terminal/pseudoterminal.cpp +++ b/src/modules/eterm/src/eterm/terminal/pseudoterminal.cpp @@ -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::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 ); diff --git a/src/modules/eterm/src/eterm/terminal/terminalcontroller.cpp b/src/modules/eterm/src/eterm/terminal/terminalcontroller.cpp new file mode 100644 index 000000000..95a73f0db --- /dev/null +++ b/src/modules/eterm/src/eterm/terminal/terminalcontroller.cpp @@ -0,0 +1,555 @@ +#include + +#include +#include + +#include +#include + +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( columns ) * rows; + if ( columns != static_cast( mColumns ) || rows != static_cast( 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( y ) * mColumns + x1; + std::memcpy( destination, line + x1, static_cast( x2 - x1 ) * sizeof( *line ) ); + if ( mEmulator ) { + for ( int column = x1; column < x2; ++column ) { + if ( mEmulator->isSelected( column, y ) ) + mCells[static_cast( 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(); + 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( 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 mCells; + std::vector 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::create( PtyPtr&& pty, ProcPtr&& process, + size_t historySize, + TerminalColorPalette palette ) { + if ( !pty || !process ) + return nullptr; + auto controller = std::shared_ptr( 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( *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 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 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 TerminalController::snapshot() const { + return mPublishedSnapshot.load( std::memory_order_acquire ); +} + +std::optional +TerminalController::requestSelection( std::chrono::milliseconds timeout ) { + if ( mShutdownRequested.load( std::memory_order_acquire ) ) + return std::nullopt; + auto response = std::make_shared(); + enqueue( SelectionRequestCommand{ response } ); + std::unique_lock 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::drainEvents() { + std::vector events; + std::lock_guard 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 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 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 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 commands; + { + std::lock_guard 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; + if constexpr ( std::is_same_v ) { + mEmulator->ttywrite( value.data.data(), value.data.size(), value.mayEcho ); + } else if constexpr ( std::is_same_v ) { + mEmulator->write( value.data.data(), value.data.size() ); + } else if constexpr ( std::is_same_v ) { + mEmulator->resize( value.columns, value.rows ); + } else if constexpr ( std::is_same_v ) { + 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 ) { + mEmulator->selstart( value.column, value.row, value.snap ); + mEmulator->redraw(); + } else if constexpr ( std::is_same_v ) { + mEmulator->selextend( value.column, value.row, value.type, value.done ); + mEmulator->redraw(); + } else if constexpr ( std::is_same_v ) { + mEmulator->selclear(); + mEmulator->redraw(); + } else if constexpr ( std::is_same_v ) { + mEmulator->mousereport( value.type, value.position, value.flags, value.modifiers ); + } else if constexpr ( std::is_same_v ) { + 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 ) { + mWorkerDisplay->setCursorMode( value.mode ); + mEmulator->redraw(); + } else if constexpr ( std::is_same_v ) { + mWorkerDisplay->setPalette( std::move( value.palette ) ); + mEmulator->redraw(); + } else if constexpr ( std::is_same_v ) { + const Uint32 framesPerSecond = eeclamp( value.framesPerSecond, 1, 1000 ); + mEmulator->setPresentationInterval( + Microseconds( 1000000.0 / static_cast( framesPerSecond ) ) ); + mWorkerDisplay->setPresentationRate( framesPerSecond ); + mEmulator->redraw(); + } else if constexpr ( std::is_same_v ) { + mEmulator->setAllowMemoryTrimnming( value.value ); + } else if constexpr ( std::is_same_v ) { + 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 ) { + 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 ) { + mEmulator->terminate(); + mEmulator->redraw(); + } else if constexpr ( std::is_same_v ) { + mEmulator->reset(); + } else if constexpr ( std::is_same_v ) { + mEmulator->clearHistory(); + mEmulator->setPtyAndProcess( std::move( value.pty ), std::move( value.process ) ); + mEmulator->redraw(); + } else if constexpr ( std::is_same_v ) { + std::lock_guard lock( value.response->mutex ); + value.response->selection = mEmulator->getSelection(); + value.response->ready = true; + value.response->condition.notify_one(); + } + }, + std::move( command ) ); +} + +}} // namespace eterm::Terminal diff --git a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp index 8699188a9..734b27fa2 100644 --- a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp @@ -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::create( - EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize, - std::shared_ptr&& terminalEmulator, const bool& useFrameBuffer ) { - std::shared_ptr terminal = std::shared_ptr( - 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::create( EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize, std::string program, std::vector args, const std::string& workingDir, @@ -446,8 +450,15 @@ std::shared_ptr TerminalDisplay::create( std::shared_ptr terminal = std::shared_ptr( 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::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& TerminalDisplay::getTerminal() const { - return mTerminal; -} - -void TerminalDisplay::attach( TerminalEmulator* terminal ) { - ITerminalDisplay::attach( terminal ); - onSizeChange(); +const std::shared_ptr& 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 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( mColumns ) || + snapshot->rows != static_cast( 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( mDataCallback ) ); +} + +void TerminalDisplay::setPromptStateChangedCallback( PromptStateChangedFunc callback ) { + mPromptStateChangedCallback = std::move( callback ); + if ( mController ) + mController->setPromptEventsEnabled( static_cast( 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& 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(); } diff --git a/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp b/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp index 22ef55cd0..f2c41b860 100644 --- a/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp @@ -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 ); } diff --git a/src/modules/eterm/src/eterm/ui/uiterminal.cpp b/src/modules/eterm/src/eterm/ui/uiterminal.cpp index ec7f44e67..4d753b370 100644 --- a/src/modules/eterm/src/eterm/ui/uiterminal.cpp +++ b/src/modules/eterm/src/eterm/ui/uiterminal.cpp @@ -31,7 +31,10 @@ UITerminal* UITerminal::New( const std::shared_ptr& 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 ) : @@ -109,13 +113,13 @@ UITerminal::UITerminal( const std::shared_ptr& 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& 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(); } diff --git a/src/tests/unit_tests/eterm_tests.cpp b/src/tests/unit_tests/eterm_tests.cpp index 0082bfba8..f76099bc0 100644 --- a/src/tests/unit_tests/eterm_tests.cpp +++ b/src/tests/unit_tests/eterm_tests.cpp @@ -1,9 +1,13 @@ #include "utest.hpp" +#include +#include #include #include #include +#include #include #include +#include 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::max() }; + size_t mReadOffset{ 0 }; + std::atomic 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 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 +waitForSnapshot( const std::shared_ptr& controller, + const std::function& 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(); + auto process = std::make_unique(); + 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( '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(); + pty->mBuffer = "ordered selection"; + auto process = std::make_unique(); + 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(); + pty->mBuffer.assign( 32 * 1024 * 1024, 'L' ); + pty->mMaxRead = 64; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + 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(); + auto process = std::make_unique(); + 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( 40 * 12 ), snapshot->cells.size() ); +} + +UTEST( eterm_controller, scroll_snapshots_acknowledge_the_latest_ordered_command ) { + auto pty = std::make_unique(); + for ( int line = 0; line < 80; ++line ) + pty->mBuffer += "history " + std::to_string( line ) + "\r\n"; + auto process = std::make_unique(); + 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(); + auto process = std::make_unique(); + 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(); + pty->mBuffer = "\033[?1004h"; + pty->mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + 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(); + auto process = std::make_unique(); + 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(); + auto process = std::make_unique(); + 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 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( 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(); + pty->mBuffer = "content"; + auto process = std::make_unique(); + 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(); + pty->mMaxRead = 1; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + 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( 3 * 1024 ), bytesRead ); + EXPECT_TRUE( sawExit ); +} + +UTEST( eterm_controller, repeated_create_destroy_and_concurrent_workers ) { + for ( int iteration = 0; iteration < 16; ++iteration ) { + std::vector> controllers; + for ( int terminal = 0; terminal < 4; ++terminal ) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + 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(); + auto process = std::make_unique(); + auto controller = TerminalController::create( std::move( pty ), std::move( process ), 100 ); + std::vector 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 ); } diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index e39f23b20..858f456ac 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -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()->getTerm()->getTerminal()->getProcess()->pid(); + ProcessID pid = widget->asType()->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(); - 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 ) ) diff --git a/src/tools/ecode/plugins/aiassistant/acp/agentsession.cpp b/src/tools/ecode/plugins/aiassistant/acp/agentsession.cpp index 5038f5cab..6cbd53dfb 100644 --- a/src/tools/ecode/plugins/aiassistant/acp/agentsession.cpp +++ b/src/tools/ecode/plugins/aiassistant/acp/agentsession.cpp @@ -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 ); diff --git a/src/tools/ecode/plugins/aiassistant/acp/agentsession.hpp b/src/tools/ecode/plugins/aiassistant/acp/agentsession.hpp index d87a13131..5486b40c6 100644 --- a/src/tools/ecode/plugins/aiassistant/acp/agentsession.hpp +++ b/src/tools/ecode/plugins/aiassistant/acp/agentsession.hpp @@ -2,7 +2,6 @@ #include "acpclient.hpp" #include -#include #include #include #include @@ -60,7 +59,6 @@ class AgentSession { struct TermData { std::shared_ptr display; - std::shared_ptr emulator; UITerminal* uiTerm{ nullptr }; std::string outputBuffer; Uint32 eventCbId{ 0 }; diff --git a/src/tools/ecode/plugins/debugger/debuggerplugin.cpp b/src/tools/ecode/plugins/debugger/debuggerplugin.cpp index e333309d0..72ad64f28 100644 --- a/src/tools/ecode/plugins/debugger/debuggerplugin.cpp +++ b/src/tools/ecode/plugins/debugger/debuggerplugin.cpp @@ -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( diff --git a/src/tools/ecode/statusterminalcontroller.cpp b/src/tools/ecode/statusterminalcontroller.cpp index a34f84a7e..9251a6dc2 100644 --- a/src/tools/ecode/statusterminalcontroller.cpp +++ b/src/tools/ecode/statusterminalcontroller.cpp @@ -27,7 +27,7 @@ bool StatusTerminalController::tryTabClose( UITab* tab ) { if ( mContext->getConfig().term.warnBeforeClosingTab && widget->isType( UI_TYPE_TERMINAL ) ) { UITerminal* term = widget->asType(); - 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() ); } ); diff --git a/src/tools/ecode/terminalmanager.cpp b/src/tools/ecode/terminalmanager.cpp index 17b68bfae..e3a501bd1 100644 --- a/src/tools/ecode/terminalmanager.cpp +++ b/src/tools/ecode/terminalmanager.cpp @@ -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 && diff --git a/src/tools/eterm/eterm.cpp b/src/tools/eterm/eterm.cpp index f365da77c..84f25cd0b 100644 --- a/src/tools/eterm/eterm.cpp +++ b/src/tools/eterm/eterm.cpp @@ -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 ) { diff --git a/src/tools/eterm_tabs/eterm_tabs.cpp b/src/tools/eterm_tabs/eterm_tabs.cpp new file mode 100644 index 000000000..78b264a41 --- /dev/null +++ b/src/tools/eterm_tabs/eterm_tabs.cpp @@ -0,0 +1,516 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +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 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 terminalColorSchemes; +const TerminalColorScheme* selectedColorScheme{ nullptr }; +Float terminalFontSize{ 12 }; +bool warnBeforeClose{ false }; +bool closeApproved{ false }; +bool benchmarkMode{ false }; +Clock secondsCounter; +SmallVector 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() + : 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 shell( parser, "shell", "Shell name or path", { 's', "shell" }, + "" ); + args::ValueFlag shellArgs( parser, "shell-args", "Shell command line arguments", + { "shell-args" }, "" ); + args::ValueFlag 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 fontPath( parser, "fontpath", "Font path", { 'f', "font" } ); + args::ValueFlag fallbackFontPath( parser, "fallback-fontpath", + "Fallback Font path", { "fallback-font" } ); + args::ValueFlag fontSize( parser, "fontsize", "Font size (in dp)", { "fontsize" }, 11 ); + const std::unordered_map fontHintingMap{ + { "none", FontHinting::None }, + { "slight", FontHinting::Slight }, + { "full", FontHinting::Full }, + }; + args::MapFlag fontHinting( + parser, "font-hinting", "Font hinting mode (accepted values: none, slight, full)", + { "font-hinting" }, fontHintingMap, FontHinting::Full ); + const std::unordered_map fontAntialiasingMap{ + { "none", FontAntialiasing::None }, + { "grayscale", FontAntialiasing::Grayscale }, + { "subpixel", FontAntialiasing::Subpixel }, + }; + args::MapFlag fontAntialiasing( + parser, "font-antialiasing", + "Font antialiasing mode (accepted values: none, grayscale, subpixel)", + { "font-antialiasing" }, fontAntialiasingMap, FontAntialiasing::Grayscale ); + args::ValueFlag width( parser, "winwidth", "Window width (in dp)", { "width" }, 1280 ); + args::ValueFlag height( parser, "winheight", "Window height (in dp)", { "height" }, + 720 ); + args::ValueFlag pixelDensity( parser, "pixel-density", + "Set default application pixel density", + { 'd', "pixel-density" } ); + args::Positional 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 executeInShell( + parser, "execute-in-shell", "execute program in shell", { 'e', "execute" }, "" ); + args::Flag vsync( parser, "vsync", "Enable vsync", { "vsync" } ); + args::ValueFlag colorScheme( parser, "color-scheme", "Load color scheme", + { "color-scheme" }, "" ); + args::Flag listColorSchemes( parser, "color-schemes", "Lists color schemes", + { "list-color-schemes" } ); + args::ValueFlag 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 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 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( displaySize.getWidth() * 0.8f ) ); + if ( displaySize.getHeight() > 0 && windowSize.getHeight() >= displaySize.getHeight() ) + windowSize.setHeight( static_cast( 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( 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{}; + 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( 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( 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; +}