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 1/5] 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; +} From d0aa3103620cdc70a2d1e6e4d95609aabc2e2d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 30 Aug 2026 22:15:14 -0300 Subject: [PATCH 2/5] Fix compilation issues with older C++ versions. --- .../eterm/terminal/terminalcontroller.hpp | 5 ++-- .../src/eterm/terminal/terminalcontroller.cpp | 25 ++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/modules/eterm/include/eterm/terminal/terminalcontroller.hpp b/src/modules/eterm/include/eterm/terminal/terminalcontroller.hpp index 9701da3fb..2f7d1bd57 100644 --- a/src/modules/eterm/include/eterm/terminal/terminalcontroller.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminalcontroller.hpp @@ -222,7 +222,7 @@ class TerminalController final : public std::enable_shared_from_this mCommands; std::mutex mEventMutex; std::deque mEvents; - std::atomic> mPublishedSnapshot; + mutable std::mutex mPublishedSnapshotMutex; + std::shared_ptr mPublishedSnapshot; std::atomic mShutdownRequested{ false }; std::atomic mNextScrollCommand{ 0 }; }; diff --git a/src/modules/eterm/src/eterm/terminal/terminalcontroller.cpp b/src/modules/eterm/src/eterm/terminal/terminalcontroller.cpp index 95a73f0db..616616228 100644 --- a/src/modules/eterm/src/eterm/terminal/terminalcontroller.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminalcontroller.cpp @@ -277,16 +277,18 @@ void TerminalController::shutdown() { mWorker.join(); } -void TerminalController::enqueue( Command&& command ) { - if ( mShutdownRequested.load( std::memory_order_acquire ) ) - return; +bool TerminalController::enqueue( Command&& command ) { { std::lock_guard lock( mCommandMutex ); + if ( mShutdownRequested.load( std::memory_order_relaxed ) ) - return; + return false; + mCommands.emplace_back( std::move( command ) ); } + mCommandCondition.notify_one(); + return true; } void TerminalController::write( std::string data, bool mayEcho ) { @@ -310,10 +312,11 @@ void TerminalController::scrollDown( int amount ) { } 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 } ); + + if ( !enqueue( ScrollCommand{ position, 0, commandId } ) ) + return 0; + return commandId; } @@ -379,7 +382,8 @@ void TerminalController::restart( PtyPtr&& pty, ProcPtr&& process ) { } std::shared_ptr TerminalController::snapshot() const { - return mPublishedSnapshot.load( std::memory_order_acquire ); + std::lock_guard lock( mPublishedSnapshotMutex ); + return mPublishedSnapshot; } std::optional @@ -426,7 +430,10 @@ void TerminalController::enqueueEvent( Event event, bool coalescable ) { void TerminalController::publishSnapshot( std::shared_ptr snapshot ) { const Uint64 generation = snapshot->generation; - mPublishedSnapshot.store( std::move( snapshot ), std::memory_order_release ); + { + std::lock_guard lock( mPublishedSnapshotMutex ); + mPublishedSnapshot = std::move( snapshot ); + } Event event{ EventType::SnapshotReady }; event.generation = generation; enqueueEvent( std::move( event ), true ); From bfa9b6ab603f053f20bf2560030c01720723da4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 31 Aug 2026 01:15:28 -0300 Subject: [PATCH 3/5] Rename TerminalController to TerminalSession. --- .../eterm/terminal/terminaldisplay.hpp | 8 +- ...inalcontroller.hpp => terminalsession.hpp} | 32 ++-- .../src/eterm/terminal/terminaldisplay.cpp | 174 ++++++++--------- ...inalcontroller.cpp => terminalsession.cpp} | 123 ++++++------ src/tests/unit_tests/eterm_tests.cpp | 176 +++++++++--------- src/tools/eterm_tabs/eterm_tabs.cpp | 4 +- 6 files changed, 255 insertions(+), 262 deletions(-) rename src/modules/eterm/include/eterm/terminal/{terminalcontroller.hpp => terminalsession.hpp} (88%) rename src/modules/eterm/src/eterm/terminal/{terminalcontroller.cpp => terminalsession.cpp} (80%) diff --git a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp index f5a64041a..76904182d 100644 --- a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp @@ -12,7 +12,7 @@ #include #include #include -#include +#include #include #include @@ -242,7 +242,7 @@ class TerminalDisplay { void setPadding( const Rectf& padding ); - const std::shared_ptr& getController() const; + const std::shared_ptr& getSession() const; std::string getSelection(); @@ -313,7 +313,7 @@ class TerminalDisplay { protected: EE::Window::Window* mWindow; std::vector mColors; - std::shared_ptr mController; + std::shared_ptr mSession; std::shared_ptr mSnapshot; mutable std::string mClipboardUtf8; Uint32 mNumCallBacks{ 0 }; @@ -382,7 +382,7 @@ class TerminalDisplay { void consumeSnapshot(); - void drainControllerEvents(); + void drainSessionEvents(); TerminalColorPalette makeColorPalette() const; diff --git a/src/modules/eterm/include/eterm/terminal/terminalcontroller.hpp b/src/modules/eterm/include/eterm/terminal/terminalsession.hpp similarity index 88% rename from src/modules/eterm/include/eterm/terminal/terminalcontroller.hpp rename to src/modules/eterm/include/eterm/terminal/terminalsession.hpp index 2f7d1bd57..59be5bef2 100644 --- a/src/modules/eterm/include/eterm/terminal/terminalcontroller.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminalsession.hpp @@ -1,5 +1,5 @@ -#ifndef ETERM_TERMINALCONTROLLER_HPP -#define ETERM_TERMINALCONTROLLER_HPP +#ifndef ETERM_TERMINALSESSION_HPP +#define ETERM_TERMINALSESSION_HPP #include #include @@ -69,13 +69,13 @@ struct TerminalColorPalette { }; /** - * Per-terminal worker/controller. + * Per-terminal worker/session. * * 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. + * immutable snapshot returned by snapshot(). No session callback is invoked by the worker. */ -class TerminalController final : public std::enable_shared_from_this { +class TerminalSession final : public std::enable_shared_from_this { public: using PtyPtr = std::unique_ptr; using ProcPtr = std::unique_ptr; @@ -104,16 +104,16 @@ class TerminalController final : public std::enable_shared_from_this create( PtyPtr&& pty, ProcPtr&& process, - size_t historySize, - TerminalColorPalette palette = {} ); + static std::shared_ptr create( PtyPtr&& pty, ProcPtr&& process, + size_t historySize, + TerminalColorPalette palette = {} ); - ~TerminalController(); + ~TerminalSession(); - TerminalController( const TerminalController& ) = delete; - TerminalController( TerminalController&& ) = delete; - TerminalController& operator=( const TerminalController& ) = delete; - TerminalController& operator=( TerminalController&& ) = delete; + TerminalSession( const TerminalSession& ) = delete; + TerminalSession( TerminalSession&& ) = delete; + TerminalSession& operator=( const TerminalSession& ) = delete; + TerminalSession& operator=( TerminalSession&& ) = delete; void write( std::string data, bool mayEcho = true ); void writeRaw( std::string data ); @@ -218,8 +218,8 @@ class TerminalController final : public std::enable_shared_from_this; - TerminalController( PtyPtr&& pty, ProcPtr&& process, size_t historySize, - TerminalColorPalette palette ); + TerminalSession( PtyPtr&& pty, ProcPtr&& process, size_t historySize, + TerminalColorPalette palette ); void start(); bool enqueue( Command&& command ); @@ -246,4 +246,4 @@ class TerminalController final : public std::enable_shared_from_this TerminalDisplay::create( std::shared_ptr terminal = std::shared_ptr( new TerminalDisplay( window, font, fontSize, pixelsSize, useFrameBuffer ) ); - terminal->mController = - TerminalController::create( std::move( pseudoTerminal ), std::move( process ), historySize, - terminal->makeColorPalette() ); - if ( !terminal->mController ) { + terminal->mSession = + TerminalSession::create( std::move( pseudoTerminal ), std::move( process ), historySize, + terminal->makeColorPalette() ); + if ( !terminal->mSession ) { if ( freeProcessFactory ) eeSAFE_DELETE( processFactory ); return nullptr; } - terminal->mController->setPresentationRate( presentationRateForWindow( window ) ); + terminal->mSession->setPresentationRate( presentationRateForWindow( window ) ); terminal->mProgram = program; terminal->mArgs = args; terminal->mEnv = env; @@ -473,8 +473,8 @@ std::shared_ptr TerminalDisplay::create( } TerminalDisplay::~TerminalDisplay() { - if ( mController ) - mController->shutdown(); + if ( mSession ) + mSession->shutdown(); } TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const Float& fontSize, @@ -621,8 +621,8 @@ void TerminalDisplay::setPadding( const Rectf& padding ) { } } -const std::shared_ptr& TerminalDisplay::getController() const { - return mController; +const std::shared_ptr& TerminalDisplay::getSession() const { + return mSession; } int TerminalDisplay::scrollSize() const { @@ -638,7 +638,7 @@ int TerminalDisplay::scrollPosition() const { } Uint64 TerminalDisplay::scrollTo( int position ) { - return mController ? mController->scrollTo( position ) : 0; + return mSession ? mSession->scrollTo( position ) : 0; } Uint64 TerminalDisplay::lastAppliedScrollCommand() const { @@ -679,13 +679,13 @@ const TerminalColorScheme& TerminalDisplay::getColorScheme() const { void TerminalDisplay::setColorScheme( const TerminalColorScheme& colorScheme ) { mColorScheme = colorScheme; resetColors(); - if ( mController ) - mController->setColorPalette( makeColorPalette() ); + if ( mSession ) + mSession->setColorPalette( makeColorPalette() ); invalidateLines(); } bool TerminalDisplay::isAppCapturingMouse() const { - return mController && + return mSession && ( mMode & ( MODE_MOUSEX10 | MODE_MOUSEBTN | MODE_MOUSEMOTION | MODE_MOUSEMANY ) ); } @@ -711,7 +711,7 @@ void TerminalDisplay::setKeepAlive( bool keepAlive ) { bool TerminalDisplay::update( bool isMouseOverMe ) { consumeSnapshot(); - drainControllerEvents(); + drainSessionEvents(); if ( mFocus && isBlinkingCursor() && mClock.getElapsedTime().asSeconds() > 0.7 ) { mMode ^= MODE_BLINK; mClock.restart(); @@ -730,9 +730,9 @@ bool TerminalDisplay::update( bool isMouseOverMe ) { } void TerminalDisplay::consumeSnapshot() { - if ( !mController ) + if ( !mSession ) return; - auto snapshot = mController->snapshot(); + auto snapshot = mSession->snapshot(); if ( !snapshot || snapshot->generation == mSnapshotGeneration ) return; @@ -781,56 +781,56 @@ void TerminalDisplay::consumeSnapshot() { mDirty = true; } -void TerminalDisplay::drainControllerEvents() { - if ( !mController ) +void TerminalDisplay::drainSessionEvents() { + if ( !mSession ) return; - for ( auto& event : mController->drainEvents() ) { + for ( auto& event : mSession->drainEvents() ) { switch ( event.type ) { - case TerminalController::EventType::Title: + case TerminalSession::EventType::Title: sendEvent( { EventType::TITLE, std::move( event.data ) } ); break; - case TerminalController::EventType::IconTitle: + case TerminalSession::EventType::IconTitle: sendEvent( { EventType::ICON_TITLE, std::move( event.data ) } ); break; - case TerminalController::EventType::ScrollPosition: + case TerminalSession::EventType::ScrollPosition: sendEvent( { EventType::SCROLL_HISTORY } ); break; - case TerminalController::EventType::Bell: + case TerminalSession::EventType::Bell: sendEvent( { EventType::BELL } ); break; - case TerminalController::EventType::Clipboard: + case TerminalSession::EventType::Clipboard: setClipboard( event.data.c_str() ); sendEvent( { EventType::CLIPBOARD } ); break; - case TerminalController::EventType::ProcessExit: + case TerminalSession::EventType::ProcessExit: onProcessExit( event.value ); break; - case TerminalController::EventType::RestartFailure: + case TerminalSession::EventType::RestartFailure: sendEvent( { EventType::RESTART_FAILURE, std::move( event.data ) } ); break; - case TerminalController::EventType::Data: + case TerminalSession::EventType::Data: if ( mDataCallback ) mDataCallback( event.data.data(), event.data.size() ); break; - case TerminalController::EventType::PromptState: + case TerminalSession::EventType::PromptState: if ( mPromptStateChangedCallback ) mPromptStateChangedCallback( event.promptState, event.data ); break; - case TerminalController::EventType::Color: + case TerminalSession::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: + case TerminalSession::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: + case TerminalSession::EventType::HistoryLength: sendEvent( { EventType::HISTORY_LENGTH_CHANGE } ); break; - case TerminalController::EventType::SnapshotReady: + case TerminalSession::EventType::SnapshotReady: break; } } @@ -848,8 +848,8 @@ TerminalColorPalette TerminalDisplay::makeColorPalette() const { } std::string TerminalDisplay::getSelection() { - if ( mController ) { - if ( auto selection = mController->requestSelection() ) + if ( mSession ) { + if ( auto selection = mSession->requestSelection() ) return std::move( *selection ); } return mSnapshot ? mSnapshot->selection : std::string{}; @@ -872,33 +872,33 @@ int TerminalDisplay::getExitCode() const { } void TerminalDisplay::terminate() { - if ( mController ) - mController->terminate(); + if ( mSession ) + mSession->terminate(); } void TerminalDisplay::setAllowMemoryTrimming( bool allow ) { - if ( mController ) - mController->setAllowMemoryTrimming( allow ); + if ( mSession ) + mSession->setAllowMemoryTrimming( allow ); } void TerminalDisplay::setDataCallback( DataFunc callback ) { mDataCallback = std::move( callback ); - if ( mController ) - mController->setDataEventsEnabled( static_cast( mDataCallback ) ); + if ( mSession ) + mSession->setDataEventsEnabled( static_cast( mDataCallback ) ); } void TerminalDisplay::setPromptStateChangedCallback( PromptStateChangedFunc callback ) { mPromptStateChangedCallback = std::move( callback ); - if ( mController ) - mController->setPromptEventsEnabled( static_cast( mPromptStateChangedCallback ) ); + if ( mSession ) + mSession->setPromptEventsEnabled( static_cast( mPromptStateChangedCallback ) ); } void TerminalDisplay::setCursorMode( TerminalCursorMode mode ) { if ( mCursorMode == mode ) return; mCursorMode = mode; - if ( mController ) - mController->setCursorMode( mode ); + if ( mSession ) + mSession->setCursorMode( mode ); invalidateCursor(); } @@ -907,29 +907,29 @@ TerminalCursorMode TerminalDisplay::getCursorMode() const { } void TerminalDisplay::executeFile( const std::string& cmd ) { - if ( mController ) { + if ( mSession ) { std::string rcmd; #if EE_PLATFORM != EE_PLATFORM_WIN rcmd.push_back( 0x15 ); #endif rcmd.append( cmd ).push_back( '\r' ); - mController->write( std::move( rcmd ) ); + mSession->write( std::move( rcmd ) ); } } void TerminalDisplay::executeBinary( const std::string& binaryPath, const std::string& args ) { - if ( mController ) { + if ( mSession ) { std::string rcmd; #if EE_PLATFORM != EE_PLATFORM_WIN rcmd.push_back( 0x15 ); #endif rcmd.append( "\"" ).append( binaryPath ).append( "\" " ).append( args ).push_back( '\r' ); - mController->write( std::move( rcmd ) ); + mSession->write( std::move( rcmd ) ); } } void TerminalDisplay::action( TerminalShortcutAction action ) { - if ( !mController && action != TerminalShortcutAction::FONTSIZE_GROW && + if ( !mSession && action != TerminalShortcutAction::FONTSIZE_GROW && action != TerminalShortcutAction::FONTSIZE_SHRINK ) return; switch ( action ) { @@ -937,11 +937,11 @@ void TerminalDisplay::action( TerminalShortcutAction action ) { getClipboard(); if ( !mClipboardUtf8.empty() ) { if ( mMode & MODE_BRCKTPASTE ) { - mController->writeRaw( "\033[200~" ); - mController->writeRaw( std::move( mClipboardUtf8 ) ); - mController->writeRaw( "\033[201~" ); + mSession->writeRaw( "\033[200~" ); + mSession->writeRaw( std::move( mClipboardUtf8 ) ); + mSession->writeRaw( "\033[201~" ); } else { - mController->writeRaw( std::move( mClipboardUtf8 ) ); + mSession->writeRaw( std::move( mClipboardUtf8 ) ); } } break; @@ -955,11 +955,11 @@ void TerminalDisplay::action( TerminalShortcutAction action ) { sanitizeInput( selection ); if ( !selection.empty() ) { if ( mMode & MODE_BRCKTPASTE ) { - mController->writeRaw( "\033[200~" ); - mController->writeRaw( std::move( selection ) ); - mController->writeRaw( "\033[201~" ); + mSession->writeRaw( "\033[200~" ); + mSession->writeRaw( std::move( selection ) ); + mSession->writeRaw( "\033[201~" ); } else { - mController->writeRaw( std::move( selection ) ); + mSession->writeRaw( std::move( selection ) ); } } break; @@ -971,27 +971,27 @@ void TerminalDisplay::action( TerminalShortcutAction action ) { break; } case TerminalShortcutAction::SCROLLUP_SCREEN: { - mController->scrollUp( -(int)mClickStep ); + mSession->scrollUp( -(int)mClickStep ); break; } case TerminalShortcutAction::SCROLLDOWN_SCREEN: { - mController->scrollDown( -(int)mClickStep ); + mSession->scrollDown( -(int)mClickStep ); break; } case TerminalShortcutAction::SCROLLUP_ROW: { - mController->scrollUp( mClickStep ); + mSession->scrollUp( mClickStep ); break; } case TerminalShortcutAction::SCROLLDOWN_ROW: { - mController->scrollDown( mClickStep ); + mSession->scrollDown( mClickStep ); break; } case TerminalShortcutAction::SCROLLUP_HISTORY: { - mController->scrollUp( INT_MAX ); + mSession->scrollUp( INT_MAX ); break; } case TerminalShortcutAction::SCROLLDOWN_HISTORY: { - mController->scrollDown( INT_MAX ); + mSession->scrollDown( INT_MAX ); break; } case TerminalShortcutAction::FONTSIZE_GROW: { @@ -1055,7 +1055,7 @@ void TerminalDisplay::onMouseDoubleClick( const Vector2i& pos, const Uint32& fla if ( !isAppCapturingMouse() && ( flags & EE_BUTTON_LMASK ) && ( getSelectionMode() == SEL_EMPTY || getSelectionMode() == SEL_IDLE ) ) { auto gridPos{ positionToGrid( pos ) }; - mController->selectionStart( gridPos.x, gridPos.y, SNAP_WORD ); + mSession->selectionStart( gridPos.x, gridPos.y, SNAP_WORD ); } } @@ -1083,13 +1083,13 @@ void TerminalDisplay::onMouseMove( const Vector2i& pos, const Uint32& flags ) { if ( !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) && ( mDraggingSel || getSelectionMode() == SEL_EMPTY || getSelectionMode() == SEL_READY ) ) { auto gridPos{ positionToGrid( pos ) }; - mController->selectionExtend( + mSession->selectionExtend( gridPos.x, gridPos.y, mWindow->getInput()->getModState() & KEYMOD_SHIFT ? SEL_RECTANGULAR : SEL_REGULAR, false ); } - mController->mouseReport( TerminalMouseEventType::MouseMotion, positionToGrid( pos ), flags, - mWindow->getInput()->getModState() ); + mSession->mouseReport( TerminalMouseEventType::MouseMotion, positionToGrid( pos ), flags, + mWindow->getInput()->getModState() ); } void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) { @@ -1103,10 +1103,10 @@ void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) { if ( !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) && mLastDoubleClick.getElapsedTime() < Milliseconds( 300.f ) ) { - mController->selectionStart( gridPos.x, gridPos.y, SNAP_LINE ); + mSession->selectionStart( gridPos.x, gridPos.y, SNAP_LINE ); } else if ( !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) ) { if ( !mDraggingSel ) { - mController->selectionStart( gridPos.x, gridPos.y, 0 ); + mSession->selectionStart( gridPos.x, gridPos.y, 0 ); mDraggingSel = true; invalidateLines(); mWindow->getInput()->captureMouse( true ); @@ -1129,8 +1129,8 @@ void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) { } } - mController->mouseReport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ), flags, - mWindow->getInput()->getModState() ); + mSession->mouseReport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ), flags, + mWindow->getInput()->getModState() ); } void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) { @@ -1176,8 +1176,8 @@ void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) { } } - mController->mouseReport( TerminalMouseEventType::MouseButtonRelease, positionToGrid( pos ), - flags, mWindow->getInput()->getModState() ); + mSession->mouseReport( TerminalMouseEventType::MouseButtonRelease, positionToGrid( pos ), flags, + mWindow->getInput()->getModState() ); } static inline Color termColor( unsigned int terminalColor, const std::vector& colors ) { @@ -1713,7 +1713,7 @@ void TerminalDisplay::drawBg( bool toFBO ) { } void TerminalDisplay::draw( const Vector2f& pos ) { - if ( !mController || !mSnapshot ) + if ( !mSession || !mSnapshot ) return; mDrawing = true; @@ -1777,9 +1777,9 @@ void TerminalDisplay::onSizeChange() { mFont, mFontSize, mSize - Vector2f( mPadding.Left + mPadding.Right, mPadding.Top + mPadding.Bottom ) ) ); - if ( mController && ( !mSnapshot || gridSize.getWidth() != mSnapshot->columns || - gridSize.getHeight() != mSnapshot->rows ) ) { - mController->resize( gridSize.getWidth(), gridSize.getHeight() ); + if ( mSession && ( !mSnapshot || gridSize.getWidth() != mSnapshot->columns || + gridSize.getHeight() != mSnapshot->rows ) ) { + mSession->resize( gridSize.getWidth(), gridSize.getHeight() ); mDirtyLines.resize( gridSize.getHeight(), 1 ); } @@ -1798,7 +1798,7 @@ void TerminalDisplay::onSizeChange() { void TerminalDisplay::onProcessExit( int exitCode ) { sendEvent( { EventType::PROCESS_EXIT, String::toString( exitCode ) } ); - if ( !mController || mProgram.empty() || exitCode != 0 || !mKeepAlive ) + if ( !mSession || mProgram.empty() || exitCode != 0 || !mKeepAlive ) return; auto processFactory = eeNew( ProcessFactory, () ); @@ -1824,23 +1824,23 @@ void TerminalDisplay::onProcessExit( int exitCode ) { return; } - mController->restart( std::move( pseudoTerminal ), std::move( process ) ); + mSession->restart( std::move( pseudoTerminal ), std::move( process ) ); eeSAFE_DELETE( processFactory ); } void TerminalDisplay::onTextInput( const Uint32& chr ) { - if ( !mController ) + if ( !mSession ) return; String input; input.push_back( chr ); std::string utf8Input( input.toUtf8() ); - mController->write( std::move( utf8Input ) ); + mSession->write( std::move( utf8Input ) ); mDirty = true; } void TerminalDisplay::onTextEditing( const String&, const Int32&, const Int32& ) { - if ( !mController ) + if ( !mSession ) return; invalidateCursor(); updateIMELocation(); @@ -1910,7 +1910,7 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/, } } - mController->write( std::string( 1, tmp ) ); + mSession->write( std::string( 1, tmp ) ); return; } } @@ -1929,7 +1929,7 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/, continue; if ( k.string.size() > 0 ) { - mController->write( k.string ); + mSession->write( k.string ); return; } break; @@ -1951,7 +1951,7 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/, continue; if ( k.string.size() > 0 ) { - mController->write( k.string ); + mSession->write( k.string ); return; } break; @@ -2069,8 +2069,8 @@ void TerminalDisplay::setFocus( bool focus ) { } else { mMode &= ~MODE_FOCUSED; } - if ( mController ) - mController->setFocus( focus ); + if ( mSession ) + mSession->setFocus( focus ); invalidateCursor(); } diff --git a/src/modules/eterm/src/eterm/terminal/terminalcontroller.cpp b/src/modules/eterm/src/eterm/terminal/terminalsession.cpp similarity index 80% rename from src/modules/eterm/src/eterm/terminal/terminalcontroller.cpp rename to src/modules/eterm/src/eterm/terminal/terminalsession.cpp index 616616228..d09c8e97c 100644 --- a/src/modules/eterm/src/eterm/terminal/terminalcontroller.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminalsession.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include @@ -10,19 +10,17 @@ using namespace EE::System; namespace eterm { namespace Terminal { -struct TerminalController::SelectionResponse { +struct TerminalSession::SelectionResponse { std::mutex mutex; std::condition_variable condition; std::string selection; bool ready{ false }; }; -class TerminalController::WorkerDisplay final : public ITerminalDisplay { +class TerminalSession::WorkerDisplay final : public ITerminalDisplay { public: - WorkerDisplay( TerminalController& controller, TerminalColorPalette palette ) : - mController( controller ), - mInitialPalette( std::move( palette ) ), - mPalette( mInitialPalette ) { + WorkerDisplay( TerminalSession& session, TerminalColorPalette palette ) : + mSession( session ), mInitialPalette( std::move( palette ) ), mPalette( mInitialPalette ) { mMode |= MODE_FOCUSED; } @@ -96,18 +94,18 @@ class TerminalController::WorkerDisplay final : public ITerminalDisplay { mLastHistoryLength = snapshot->historyLength; Event event{ EventType::HistoryLength }; event.value = mLastHistoryLength; - mController.enqueueEvent( std::move( event ), true ); + mSession.enqueueEvent( std::move( event ), true ); } - mController.publishSnapshot( std::move( snapshot ) ); + mSession.publishSnapshot( std::move( snapshot ) ); } - void bell() { mController.enqueueEvent( { EventType::Bell }, false ); } + void bell() { mSession.enqueueEvent( { EventType::Bell }, false ); } void resetColors() { mPalette = mInitialPalette; Event event{ EventType::Color }; event.value = -1; - mController.enqueueEvent( std::move( event ), false ); + mSession.enqueueEvent( std::move( event ), false ); } int resetColor( const Uint32& index, const char* name ) { @@ -149,7 +147,7 @@ class TerminalController::WorkerDisplay final : public ITerminalDisplay { Event event{ EventType::Color }; event.data = name ? name : ""; event.value = static_cast( index ); - mController.enqueueEvent( std::move( event ), false ); + mSession.enqueueEvent( std::move( event ), false ); return 0; } @@ -174,27 +172,25 @@ class TerminalController::WorkerDisplay final : public ITerminalDisplay { void setTitle( const char* title ) { mTitle = title ? title : ""; - mController.enqueueEvent( { EventType::Title, mTitle }, true ); + mSession.enqueueEvent( { EventType::Title, mTitle }, true ); } void setIconTitle( const char* title ) { - mController.enqueueEvent( { EventType::IconTitle, title ? title : "" }, true ); + mSession.enqueueEvent( { EventType::IconTitle, title ? title : "" }, true ); } void setClipboard( const char* text ) { if ( text ) - mController.enqueueEvent( { EventType::Clipboard, text }, false ); + mSession.enqueueEvent( { EventType::Clipboard, text }, false ); } void onProcessExit( int exitCode ) { Event event{ EventType::ProcessExit }; event.value = exitCode; - mController.enqueueEvent( std::move( event ), false ); + mSession.enqueueEvent( std::move( event ), false ); } - void onScrollPositionChange() { - mController.enqueueEvent( { EventType::ScrollPosition }, true ); - } + void onScrollPositionChange() { mSession.enqueueEvent( { EventType::ScrollPosition }, true ); } void setPalette( TerminalColorPalette palette ) { mInitialPalette = palette; @@ -224,7 +220,7 @@ class TerminalController::WorkerDisplay final : public ITerminalDisplay { mPalette.background = color; } - TerminalController& mController; + TerminalSession& mSession; TerminalColorPalette mInitialPalette; TerminalColorPalette mPalette; std::vector mCells; @@ -241,35 +237,35 @@ class TerminalController::WorkerDisplay final : public ITerminalDisplay { bool mCursorVisible{ false }; }; -std::shared_ptr TerminalController::create( PtyPtr&& pty, ProcPtr&& process, - size_t historySize, - TerminalColorPalette palette ) { +std::shared_ptr TerminalSession::create( PtyPtr&& pty, ProcPtr&& process, + size_t historySize, + TerminalColorPalette palette ) { if ( !pty || !process ) return nullptr; - auto controller = std::shared_ptr( new TerminalController( + auto session = std::shared_ptr( new TerminalSession( std::move( pty ), std::move( process ), historySize, std::move( palette ) ) ); - controller->start(); - return controller; + session->start(); + return session; } -TerminalController::TerminalController( PtyPtr&& pty, ProcPtr&& process, size_t historySize, - TerminalColorPalette palette ) { +TerminalSession::TerminalSession( 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. + // Establish a complete generation before the session becomes concurrently visible. mEmulator->redraw(); } -TerminalController::~TerminalController() { +TerminalSession::~TerminalSession() { shutdown(); } -void TerminalController::start() { +void TerminalSession::start() { mWorker = std::thread( [this] { workerLoop(); } ); } -void TerminalController::shutdown() { +void TerminalSession::shutdown() { std::lock_guard shutdownLock( mShutdownMutex ); if ( !mShutdownRequested.exchange( true, std::memory_order_acq_rel ) ) mCommandCondition.notify_all(); @@ -277,7 +273,7 @@ void TerminalController::shutdown() { mWorker.join(); } -bool TerminalController::enqueue( Command&& command ) { +bool TerminalSession::enqueue( Command&& command ) { { std::lock_guard lock( mCommandMutex ); @@ -291,27 +287,27 @@ bool TerminalController::enqueue( Command&& command ) { return true; } -void TerminalController::write( std::string data, bool mayEcho ) { +void TerminalSession::write( std::string data, bool mayEcho ) { enqueue( WriteCommand{ std::move( data ), mayEcho } ); } -void TerminalController::writeRaw( std::string data ) { +void TerminalSession::writeRaw( std::string data ) { enqueue( WriteRawCommand{ std::move( data ) } ); } -void TerminalController::resize( int columns, int rows ) { +void TerminalSession::resize( int columns, int rows ) { enqueue( ResizeCommand{ columns, rows } ); } -void TerminalController::scrollUp( int amount ) { +void TerminalSession::scrollUp( int amount ) { enqueue( ScrollCommand{ amount, -1 } ); } -void TerminalController::scrollDown( int amount ) { +void TerminalSession::scrollDown( int amount ) { enqueue( ScrollCommand{ amount, 1 } ); } -Uint64 TerminalController::scrollTo( int position ) { +Uint64 TerminalSession::scrollTo( int position ) { const Uint64 commandId = mNextScrollCommand.fetch_add( 1, std::memory_order_relaxed ) + 1; if ( !enqueue( ScrollCommand{ position, 0, commandId } ) ) @@ -320,60 +316,60 @@ Uint64 TerminalController::scrollTo( int position ) { return commandId; } -void TerminalController::selectionStart( int column, int row, int snap ) { +void TerminalSession::selectionStart( int column, int row, int snap ) { enqueue( SelectionStartCommand{ column, row, snap } ); } -void TerminalController::selectionExtend( int column, int row, int type, bool done ) { +void TerminalSession::selectionExtend( int column, int row, int type, bool done ) { enqueue( SelectionExtendCommand{ column, row, type, done } ); } -void TerminalController::selectionClear() { +void TerminalSession::selectionClear() { enqueue( SelectionClearCommand{} ); } -void TerminalController::mouseReport( TerminalMouseEventType type, Vector2i position, Uint32 flags, - Uint32 modifiers ) { +void TerminalSession::mouseReport( TerminalMouseEventType type, Vector2i position, Uint32 flags, + Uint32 modifiers ) { enqueue( MouseCommand{ type, position, flags, modifiers } ); } -void TerminalController::setFocus( bool focus ) { +void TerminalSession::setFocus( bool focus ) { enqueue( FocusCommand{ { focus } } ); } -void TerminalController::setCursorMode( TerminalCursorMode mode ) { +void TerminalSession::setCursorMode( TerminalCursorMode mode ) { enqueue( CursorModeCommand{ mode } ); } -void TerminalController::setColorPalette( TerminalColorPalette palette ) { +void TerminalSession::setColorPalette( TerminalColorPalette palette ) { enqueue( PaletteCommand{ std::move( palette ) } ); } -void TerminalController::setAllowMemoryTrimming( bool allow ) { +void TerminalSession::setAllowMemoryTrimming( bool allow ) { enqueue( AllowTrimCommand{ { allow } } ); } -void TerminalController::setPresentationRate( Uint32 framesPerSecond ) { +void TerminalSession::setPresentationRate( Uint32 framesPerSecond ) { enqueue( PresentationRateCommand{ framesPerSecond } ); } -void TerminalController::setDataEventsEnabled( bool enabled ) { +void TerminalSession::setDataEventsEnabled( bool enabled ) { enqueue( DataEventsCommand{ { enabled } } ); } -void TerminalController::setPromptEventsEnabled( bool enabled ) { +void TerminalSession::setPromptEventsEnabled( bool enabled ) { enqueue( PromptEventsCommand{ { enabled } } ); } -void TerminalController::reset() { +void TerminalSession::reset() { enqueue( ResetCommand{} ); } -void TerminalController::terminate() { +void TerminalSession::terminate() { enqueue( TerminateCommand{} ); } -void TerminalController::restart( PtyPtr&& pty, ProcPtr&& process ) { +void TerminalSession::restart( PtyPtr&& pty, ProcPtr&& process ) { if ( !pty || !process ) { enqueueEvent( { EventType::RestartFailure, "Invalid PTY or process" }, false ); return; @@ -381,13 +377,12 @@ void TerminalController::restart( PtyPtr&& pty, ProcPtr&& process ) { enqueue( RestartCommand{ std::move( pty ), std::move( process ) } ); } -std::shared_ptr TerminalController::snapshot() const { +std::shared_ptr TerminalSession::snapshot() const { std::lock_guard lock( mPublishedSnapshotMutex ); return mPublishedSnapshot; } -std::optional -TerminalController::requestSelection( std::chrono::milliseconds timeout ) { +std::optional TerminalSession::requestSelection( std::chrono::milliseconds timeout ) { if ( mShutdownRequested.load( std::memory_order_acquire ) ) return std::nullopt; auto response = std::make_shared(); @@ -398,7 +393,7 @@ TerminalController::requestSelection( std::chrono::milliseconds timeout ) { return std::move( response->selection ); } -std::vector TerminalController::drainEvents() { +std::vector TerminalSession::drainEvents() { std::vector events; std::lock_guard lock( mEventMutex ); events.reserve( mEvents.size() ); @@ -409,7 +404,7 @@ std::vector TerminalController::drainEvents() { return events; } -void TerminalController::enqueueEvent( Event event, bool coalescable ) { +void TerminalSession::enqueueEvent( Event event, bool coalescable ) { std::lock_guard lock( mEventMutex ); if ( coalescable ) { for ( auto it = mEvents.rbegin(); it != mEvents.rend(); ++it ) { @@ -428,7 +423,7 @@ void TerminalController::enqueueEvent( Event event, bool coalescable ) { mEvents.emplace_back( std::move( event ) ); } -void TerminalController::publishSnapshot( std::shared_ptr snapshot ) { +void TerminalSession::publishSnapshot( std::shared_ptr snapshot ) { const Uint64 generation = snapshot->generation; { std::lock_guard lock( mPublishedSnapshotMutex ); @@ -439,7 +434,7 @@ void TerminalController::publishSnapshot( std::shared_ptr commands; { std::lock_guard lock( mCommandMutex ); @@ -470,7 +465,7 @@ void TerminalController::processCommands() { } } -void TerminalController::processCommand( Command&& command ) { +void TerminalSession::processCommand( Command&& command ) { std::visit( [this]( auto&& value ) { using T = std::decay_t; @@ -559,4 +554,4 @@ void TerminalController::processCommand( Command&& command ) { std::move( command ) ); } -}} // namespace eterm::Terminal +}} // namespace eterm::Terminal \ No newline at end of file diff --git a/src/tests/unit_tests/eterm_tests.cpp b/src/tests/unit_tests/eterm_tests.cpp index f76099bc0..270134ef4 100644 --- a/src/tests/unit_tests/eterm_tests.cpp +++ b/src/tests/unit_tests/eterm_tests.cpp @@ -4,8 +4,8 @@ #include #include #include -#include #include +#include #include #include @@ -59,12 +59,12 @@ class MockProcess : public IProcess { }; static std::shared_ptr -waitForSnapshot( const std::shared_ptr& controller, +waitForSnapshot( const std::shared_ptr& session, 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(); + auto snapshot = session->snapshot(); if ( snapshot && predicate( *snapshot ) ) return snapshot; std::this_thread::sleep_for( std::chrono::milliseconds( 1 ) ); @@ -72,60 +72,59 @@ waitForSnapshot( const std::shared_ptr& controller, return nullptr; } -UTEST( eterm_controller, command_wakeup_and_snapshot_immutability ) { +UTEST( eterm_session, 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 ); + auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 ); + ASSERT_TRUE( session != nullptr ); - controller->writeRaw( "ABC" ); - auto first = waitForSnapshot( controller, []( const TerminalSnapshot& snapshot ) { + session->writeRaw( "ABC" ); + auto first = waitForSnapshot( session, []( 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'; - } ); + session->writeRaw( "\rXYZ" ); + auto second = waitForSnapshot( session, [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 ) { +UTEST( eterm_session, 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 ) { +UTEST( eterm_session, 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 ) { + auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 ); + ASSERT_TRUE( waitForSnapshot( session, []( 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(); + session->selectionStart( 0, 0, 0 ); + session->selectionExtend( 6, 0, SEL_REGULAR, false ); + auto selection = session->requestSelection(); ASSERT_TRUE( selection.has_value() ); EXPECT_STDSTREQ( "ordered", *selection ); } -UTEST( eterm_controller, loaded_command_latency_stays_bounded ) { +UTEST( eterm_session, 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 ); + auto session = TerminalSession::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 ) @@ -133,19 +132,19 @@ UTEST( eterm_controller, loaded_command_latency_stays_bounded ) { ASSERT_TRUE( ptyPtr->mBytesRead.load( std::memory_order_relaxed ) > 0 ); const auto start = std::chrono::steady_clock::now(); - auto selection = controller->requestSelection(); + auto selection = session->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 ) { +UTEST( eterm_session, 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 ) { + auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 ); + session->resize( 40, 12 ); + session->writeRaw( "after resize" ); + auto snapshot = waitForSnapshot( session, []( const TerminalSnapshot& value ) { return value.columns == 40 && value.rows == 12 && !value.cells.empty() && value.cells[0].u == 'a'; } ); @@ -153,59 +152,59 @@ UTEST( eterm_controller, resize_and_output_are_serialized ) { EXPECT_EQ( static_cast( 40 * 12 ), snapshot->cells.size() ); } -UTEST( eterm_controller, scroll_snapshots_acknowledge_the_latest_ordered_command ) { +UTEST( eterm_session, 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 ) { + auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 ); + ASSERT_TRUE( waitForSnapshot( session, []( const TerminalSnapshot& snapshot ) { return snapshot.historyLength >= 25; } ) != nullptr ); - const Uint64 firstCommand = controller->scrollTo( 10 ); - const Uint64 secondCommand = controller->scrollTo( 25 ); + const Uint64 firstCommand = session->scrollTo( 10 ); + const Uint64 secondCommand = session->scrollTo( 25 ); EXPECT_EQ( firstCommand + 1, secondCommand ); auto acknowledged = - waitForSnapshot( controller, [secondCommand]( const TerminalSnapshot& snapshot ) { + waitForSnapshot( session, [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 ) { +UTEST( eterm_session, 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 ) { + auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 ); + session->setPresentationRate( 120 ); + ASSERT_TRUE( waitForSnapshot( session, []( const TerminalSnapshot& snapshot ) { return snapshot.presentationRate == 120; } ) != nullptr ); } -UTEST( eterm_controller, focus_reporting_is_ordered_on_worker ) { +UTEST( eterm_session, 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 ) { + auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 ); + auto enabled = waitForSnapshot( session, []( const TerminalSnapshot& snapshot ) { return snapshot.windowMode & MODE_FOCUS; } ); ASSERT_TRUE( enabled != nullptr ); - controller->setFocus( false ); - auto unfocused = waitForSnapshot( controller, [enabled]( const TerminalSnapshot& snapshot ) { + session->setFocus( false ); + auto unfocused = waitForSnapshot( session, [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 ) { + session->setFocus( true ); + ASSERT_TRUE( waitForSnapshot( session, [unfocused]( const TerminalSnapshot& snapshot ) { return snapshot.generation > unfocused->generation && snapshot.windowMode & MODE_FOCUSED; } ) != nullptr ); @@ -213,20 +212,20 @@ UTEST( eterm_controller, focus_reporting_is_ordered_on_worker ) { EXPECT_STDSTREQ( "\033[I", ptyPtr->mWrites.substr( ptyPtr->mWrites.size() - 3 ) ); } -UTEST( eterm_controller, replaceable_events_coalesce_without_losing_ordered_events ) { +UTEST( eterm_session, 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 ) { + auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 ); + session->drainEvents(); + session->writeRaw( "\033]0;first\a\033]0;second\a" ); + ASSERT_TRUE( waitForSnapshot( session, []( 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 ) { + for ( auto& event : session->drainEvents() ) { + if ( event.type == TerminalSession::EventType::Title ) { ++titleEvents; title = std::move( event.data ); } @@ -235,54 +234,54 @@ UTEST( eterm_controller, replaceable_events_coalesce_without_losing_ordered_even EXPECT_STDSTREQ( "second", title ); } -UTEST( eterm_controller, ordered_events_are_coalescing_barriers ) { +UTEST( eterm_session, 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 ) { + auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 ); + session->drainEvents(); + session->writeRaw( "\033]0;before\a\a\033]0;after\a" ); + ASSERT_TRUE( waitForSnapshot( session, []( 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 ) + std::vector semanticEvents; + for ( const auto& event : session->drainEvents() ) { + if ( event.type == TerminalSession::EventType::Title || + event.type == TerminalSession::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] ); + EXPECT_EQ( TerminalSession::EventType::Title, semanticEvents[0] ); + EXPECT_EQ( TerminalSession::EventType::Bell, semanticEvents[1] ); + EXPECT_EQ( TerminalSession::EventType::Title, semanticEvents[2] ); } -UTEST( eterm_controller, reset_is_ordered_and_publishes_immediately ) { +UTEST( eterm_session, 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 ) { + auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 ); + auto populated = waitForSnapshot( session, []( 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 ) { + session->reset(); + auto reset = waitForSnapshot( session, [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 ) { +UTEST( eterm_session, 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' ) ); + auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 ); + session->setDataEventsEnabled( true ); + session->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 ) @@ -291,51 +290,50 @@ UTEST( eterm_controller, process_exit_follows_buffered_output_and_final_snapshot processPtr->mExited.store( true ); auto finalSnapshot = waitForSnapshot( - controller, []( const TerminalSnapshot& snapshot ) { return snapshot.processExited; } ); + session, []( 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 ) + for ( auto& event : session->drainEvents() ) { + if ( event.type == TerminalSession::EventType::Data ) bytesRead += event.data.size(); - else if ( event.type == TerminalController::EventType::ProcessExit ) + else if ( event.type == TerminalSession::EventType::ProcessExit ) sawExit = true; } EXPECT_EQ( static_cast( 3 * 1024 ), bytesRead ); EXPECT_TRUE( sawExit ); } -UTEST( eterm_controller, repeated_create_destroy_and_concurrent_workers ) { +UTEST( eterm_session, repeated_create_destroy_and_concurrent_workers ) { for ( int iteration = 0; iteration < 16; ++iteration ) { - std::vector> controllers; + std::vector> sessions; 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 ) ); + auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 ); + session->writeRaw( "worker" + std::to_string( terminal ) ); + sessions.emplace_back( std::move( session ) ); } - for ( const auto& controller : controllers ) { - EXPECT_TRUE( waitForSnapshot( controller, []( const TerminalSnapshot& snapshot ) { + for ( const auto& session : sessions ) { + EXPECT_TRUE( waitForSnapshot( session, []( const TerminalSnapshot& snapshot ) { return !snapshot.cells.empty() && snapshot.cells[0].u == 'w'; } ) != nullptr ); } } } -UTEST( eterm_controller, concurrent_shutdown_is_idempotent ) { +UTEST( eterm_session, 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 ); + auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 ); std::vector shutdownThreads; for ( int thread = 0; thread < 4; ++thread ) - shutdownThreads.emplace_back( [controller] { controller->shutdown(); } ); + shutdownThreads.emplace_back( [session] { session->shutdown(); } ); for ( auto& thread : shutdownThreads ) thread.join(); - controller->shutdown(); + session->shutdown(); } class MockDisplay : public ITerminalDisplay { diff --git a/src/tools/eterm_tabs/eterm_tabs.cpp b/src/tools/eterm_tabs/eterm_tabs.cpp index 78b264a41..a30aeb9fb 100644 --- a/src/tools/eterm_tabs/eterm_tabs.cpp +++ b/src/tools/eterm_tabs/eterm_tabs.cpp @@ -145,8 +145,8 @@ void queueExitedTabs() { auto* terminal = terminalFromTab( tab ); if ( !terminal || !terminal->getTerm() ) return; - const auto& controller = terminal->getTerm()->getController(); - auto snapshot = controller ? controller->snapshot() : nullptr; + const auto& session = terminal->getTerm()->getSession(); + auto snapshot = session ? session->snapshot() : nullptr; if ( snapshot && snapshot->processExited ) queueExitCloseTab( tab ); } ); From c5c6350920831ac7b109c68230a5534e86256099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 31 Aug 2026 03:01:40 -0300 Subject: [PATCH 4/5] eterm: harden threaded presentation and terminal protocol handling - preserve CSI subparameter separators for modern SGR color sequences - fix synchronized updates and periodic presentation under sustained output - support OSC palette queries, resets, and color scheme notifications - implement alternate scroll, cursor style, and terminal version queries - preserve the configured cursor shape for default blinking cursor requests - recognize Kitty keyboard negotiation without falsely advertising support - recognize OSC 8 hyperlink markers without emitting parser errors - make release parser diagnostics opt-in through ETERM_LOG_ERRORS - update the Kitty keyboard protocol implementation plan for worker ownership - add regression coverage for rendering, colors, CSI modes, cursors, and OSC 8 --- ...y_keyboard_protocol_implementation_plan.md | 428 ++++++++++-------- .../eterm/terminal/terminaldisplay.hpp | 1 + .../eterm/terminal/terminalemulator.hpp | 21 +- .../include/eterm/terminal/terminaltypes.hpp | 1 + .../src/eterm/terminal/terminaldisplay.cpp | 37 +- .../src/eterm/terminal/terminalemulator.cpp | 407 +++++++++++------ .../src/eterm/terminal/terminalsession.cpp | 9 +- src/tests/unit_tests/eterm_tests.cpp | 227 +++++++++- 8 files changed, 787 insertions(+), 344 deletions(-) diff --git a/.agent/plans/eterm_kitty_keyboard_protocol_implementation_plan.md b/.agent/plans/eterm_kitty_keyboard_protocol_implementation_plan.md index 83f9b9012..e7fb2945a 100644 --- a/.agent/plans/eterm_kitty_keyboard_protocol_implementation_plan.md +++ b/.agent/plans/eterm_kitty_keyboard_protocol_implementation_plan.md @@ -7,7 +7,13 @@ - **Primary module:** `src/modules/eterm` - **Primary goal:** Implement Kitty keyboard protocol progressive enhancement in eTerm so modern terminal applications can distinguish key combinations that legacy terminal encoding collapses, especially `Ctrl+Enter`, `Shift+Enter`, modified Escape, modified Tab, modified Backspace, and modified printable keys. - **Initial motivating application:** OpenAI Codex CLI, which uses Crossterm keyboard enhancement flags. -- **Expected result for the motivating case:** When Codex enables the Kitty keyboard protocol and the user presses `Ctrl+Enter`, eTerm must send `CSI 13;5u` (`ESC [ 1 3 ; 5 u`) instead of the same carriage-return byte sent for plain Enter. +- **Expected result for the motivating case:** When an application enables + `REPORT_ALL_KEYS_AS_ESCAPE_CODES` and the user presses `Ctrl+Enter`, eTerm must send + `CSI 13;5u` (`ESC [ 1 3 ; 5 u`) instead of the same carriage-return byte sent for plain Enter. + `DISAMBIGUATE_ESCAPE_CODES` alone deliberately preserves legacy Enter, Tab, and Backspace + encodings. If Codex continues requesting only flags `7`, distinguishable `Ctrl+Enter` cannot be + promised without a documented protocol deviation or an application-side change to request bit + `8`. - **Compatibility requirement:** When no application has enabled the protocol, eTerm must preserve its existing legacy key behavior byte-for-byte. --- @@ -26,7 +32,7 @@ The proper fix is not an unconditional special case for Codex. eTerm must implem 1. Parse requests from the child application to query, push, set, or pop keyboard enhancement flags. 2. Store enhancement state per terminal screen, with stack behavior. -3. Reply to support queries. +3. Reply to current-state queries only once enhanced key encoding is operational. 4. Encode keyboard events according to the active flags. 5. Continue using existing legacy mappings when the protocol is inactive. 6. Support press, repeat, and release events when requested. @@ -38,8 +44,9 @@ The implementation should be split into four logical pieces: - **Protocol/state model** in `TerminalEmulator` - **CSI command parsing and responses** in `TerminalEmulator::csihandle()` -- **Key event encoding** in a focused encoder class/helper used by `TerminalDisplay` -- **Input event plumbing** in `UITerminal` / `TerminalDisplay`, including key-up and repeat support +- **Key event encoding** in a focused encoder class/helper invoked on the terminal worker +- **Input event plumbing** from `UITerminal` / `TerminalDisplay` into typed `TerminalSession` + commands, including key-up and repeat support Do not mix the new protocol rules into the existing large `keys[]` table. Keep the legacy table intact as a fallback and add a separate enhanced encoder that runs before it when active. @@ -178,30 +185,42 @@ Codex has used a combination including disambiguation, event types, and alternat ## 4.1 Event flow -The current flow is approximately: +eTerm now has a dedicated `TerminalSession` worker. The worker exclusively owns the emulator, +PTY, process, parser, terminal modes, history, and cursor. UI code may enqueue commands, drain +events, and retain immutable snapshots, but it must never read mutable emulator state. + +The current keyboard path still pre-encodes legacy bytes on the UI thread: ```text EEPP Window/Input backend -> UI event dispatcher - -> UITerminal::onKeyDown(KeyEvent) - -> TerminalDisplay::onKeyDown(keyCode, char, mod, scancode) - -> terminal shortcuts - -> Ctrl-letter handling - -> legacy TerminalKeyMap - -> PTY ttywrite() + -> UITerminal / TerminalDisplay + -> terminal-local shortcuts + -> Ctrl-letter and legacy TerminalKeyMap encoding + -> TerminalSession::write(encoded bytes) + -> ordered worker command + -> PTY ttywrite() ``` -Text input follows a separate path: +The Kitty implementation must change the child-input portion to: ```text -EEPP text input event - -> UITerminal::onTextInput(TextInputEvent) - -> TerminalDisplay::onTextInput(codepoint) - -> UTF-8 conversion - -> PTY ttywrite() +EEPP Window/Input backend + -> UI event dispatcher + -> UITerminal / TerminalDisplay + -> consume terminal-local shortcuts + -> enqueue semantic KeyDown / KeyUp / TextInput command + -> TerminalSession worker + -> read authoritative Kitty state and terminal modes + -> correlate key and committed-text events + -> enhanced or legacy encoding + -> PTY ttywrite() ``` -This split is important. Printable characters are normally emitted by `onTextInput()`, while special/control keys are emitted by `onKeyDown()`. Kitty’s `REPORT_ALL_KEYS_AS_ESCAPE_CODES` changes this assumption: printable keys may need to be emitted from the key-event path as CSI-u events, and the later text-input event must then be suppressed to prevent duplicate characters. +Printable characters currently arrive through a separate text-input event. Kitty's +`REPORT_ALL_KEYS_AS_ESCAPE_CODES` requires key/text correlation to avoid duplicate input. That +correlation belongs on the worker because the ordered command queue preserves UI event order and +the worker owns the negotiated state. ## 4.2 Existing legacy keyboard map @@ -236,16 +255,18 @@ This is why modified Enter collapses to carriage return. ## 4.3 Existing parser support placeholder -`TerminalEmulator::csihandle()` already has a branch similar to: +The CSI parser has now been characterized and updated. It stores `<`, `=`, `>`, or `?` in +`CSIEscape::priv`, preserves `;`/`:` separators in `CSIEscape::sep`, and places the final byte in +`mode[0]`. Therefore Kitty controls are represented as a prefixed final `u`, for example: -```cpp -case '=': /* Progressive enhancement sequences */ - /* Keyboard protocol ESC[=Nu */ - /* Do nothing for the moment */ - break; +```text +ESC [ > 7 u -> priv='>', arg[0]=7, mode[0]='u' +ESC [ ? u -> priv='?', arg[0]=0, mode[0]='u' ``` -This is a strong signal that progressive enhancement belongs in `TerminalEmulator`, not in the UI widget. +`TerminalEmulator::csihandle()` currently recognizes all four prefixed `u` variants and silently +ignores them. It intentionally does not answer `CSI ? u`, because a response would claim support +before enhanced input encoding exists. Bare `CSI u` continues to mean DECRC cursor restore. The CSI parser stores: @@ -260,7 +281,7 @@ struct CSIEscape { }; ``` -Before implementation, verify exactly how these sequences are represented after `csiparse()`: +The existing parser regression test covers these exact byte strings: ```text ESC [ ? u @@ -273,7 +294,10 @@ ESC [ = 7 ; 2 u ESC [ = 7 ; 3 u ``` -Do not assume that `?`, `>`, `<`, and `=` always land in the same field. The current `csihandle()` switch structure suggests some private/intermediate bytes may be placed into `mode[0]` or `priv`. Add parser tests first and adjust the handler according to observed representation. +Extend that test into state/response assertions when negotiation is implemented. One remaining +detail is that the parser currently represents an omitted numeric parameter as zero. If exact +semantics require distinguishing omitted `CSI < u` from explicit `CSI < 0 u`, add argument-presence +metadata rather than inferring presence from `narg`. ## 4.4 Screen swapping @@ -304,10 +328,10 @@ src/modules/eterm/include/eterm/terminal/kittykeyboardprotocol.hpp src/modules/eterm/src/eterm/terminal/kittykeyboardprotocol.cpp ``` -Possible test file: +Use the repository's existing unit-test file: ```text -src/modules/eterm/tests/kittykeyboardprotocol_test.cpp +src/tests/unit_tests/eterm_tests.cpp ``` Adapt to the repository’s actual test layout and build system. @@ -420,7 +444,7 @@ Names may be adjusted to project style, but maintain clear separation between: - output bytes, - duplicate-text suppression. -## 5.2 Keep protocol state owned by `TerminalEmulator` +## 5.2 Keep protocol state and encoding on the worker `TerminalEmulator` receives application output and therefore owns negotiation state. @@ -431,14 +455,7 @@ KittyKeyboardState mPrimaryKeyboardState; KittyKeyboardState mAlternateKeyboardState; ``` -Add public read-only accessors and event-state operations: - -```cpp -std::uint32_t getKeyboardEnhancementFlags() const; -bool hasKeyboardEnhancementFlag( KittyKeyboardFlag flag ) const; -``` - -Potentially add: +Potentially add private worker-only helpers: ```cpp const KittyKeyboardState& getKeyboardState() const; @@ -447,7 +464,11 @@ KittyKeyboardState& getKeyboardState(); Keep mutating access private if possible. -`TerminalDisplay` should only query active flags. It must not parse application control sequences or mutate protocol stacks directly. +Do not add a UI-facing `getKeyboardEnhancementFlags()` accessor. `TerminalDisplay` must not read +the emulator or use snapshot flags to decide how to encode input: snapshots can lag negotiation. +Instead add typed key and text commands to `TerminalSession`; their worker-side handlers query the +active state and invoke the encoder. Active flags may be copied into `TerminalSnapshot` for +diagnostics only. ## 5.3 Consider moving `sanitizeMod()` @@ -485,7 +506,10 @@ DisambiguateEscapeCodes Add `ReportAlternateKeys` only if EEPP can reliably supply shifted/base-layout key values. Add `ReportAssociatedText` only when the implementation can correlate committed text with the originating key event without duplication or loss. -However, for Codex compatibility, determine whether Crossterm requires the terminal to echo all requested bits or merely accepts the subset returned by the query. The preferred behavior is standards-compliant subset support, not falsely claiming unsupported features. +When an application sets or pushes flags, mask the request to features eTerm can honor. A later +current-state query reports the effective active subset, allowing applications to observe which +requested bits took effect. Do not treat the initial query as capability advertisement and do not +retain unsupported bits as active. A safe first complete target is to implement all five flags to the degree required by the spec, with documented backend limitations. @@ -497,7 +521,8 @@ When the child sends: CSI ? u ``` -eTerm must respond through the PTY input channel with: +eTerm must respond through the PTY input channel with the current effective flags for the active +screen: ```text CSI ? u @@ -509,24 +534,29 @@ For example: ESC [ ? 31 u ``` -if all five flags are supported. +if all five flags are currently active. A newly initialized terminal responds with `CSI ? 0 u` +even if it implements every enhancement. This is a state query, not a supported-capability mask. Use `ttywrite()` or the appropriate low-level response method with echo disabled. Follow existing device-status response patterns. -Suggested helper: +Suggested worker-only helper: ```cpp -void TerminalEmulator::replyKeyboardEnhancementFlags() { +void TerminalEmulator::replyKeyboardEnhancementState() { char buf[32]; const int len = std::snprintf( buf, sizeof( buf ), "\033[?%uu", - KITTY_KEYBOARD_SUPPORTED_FLAGS + activeKeyboardState().flags ); ttywrite( buf, static_cast( len ), 0 ); } ``` -Important: verify from the official spec whether the query response reports **supported flags** or **currently active flags**. Implement exactly the current spec. Do not infer from naming. Add a test matching Kitty and Crossterm behavior. +Applications detect protocol support by sending the state query followed by primary device +attributes. No Kitty response before DA means unsupported. To discover whether requested bits took +effect, an application sets/pushes them and then queries the resulting current state. Keep a +supported mask internally for safe flag application, but never return that mask in place of the +active state. ## 6.3 Push semantics @@ -602,15 +632,11 @@ The protocol includes: CSI = flags ; mode u ``` -The `mode` controls how `flags` affects current state. Implement all standardized modes exactly. +The optional `mode` defaults to `1` and has these standardized meanings: -Expected conceptual operations are generally: - -- replace/set absolute flags, -- set/add bits, -- clear/remove bits. - -Do not hardcode these assumptions without checking the current official table. +- `1`: replace the current state; set specified bits and reset all unspecified bits, +- `2`: set the specified bits and leave unspecified bits unchanged, +- `3`: reset the specified bits and leave unspecified bits unchanged. Implement through one function: @@ -623,13 +649,13 @@ void KittyKeyboardState::set( requestedFlags & KITTY_KEYBOARD_SUPPORTED_FLAGS; switch ( mode ) { - case /* set */: + case 1: flags = supported; break; - case /* OR */: + case 2: flags |= supported; break; - case /* clear */: + case 3: flags &= ~supported; break; default: @@ -685,9 +711,12 @@ Do not leave enhancement flags active when a new shell is spawned in the same te # 7. CSI parser implementation -## 7.1 Add parser characterization tests first +## 7.1 Extend the existing parser characterization test -Before modifying `csihandle()`, create tests that feed these exact byte strings into `TerminalEmulator`: +The existing `modern_csi_prefixes_do_not_claim_unsupported_keyboard_protocol` test already feeds +the representative query/push/pop/set strings into `TerminalEmulator` and verifies that the +unsupported implementation stays silent. Preserve it until negotiation and encoding land +together, then extend it to assert state changes and responses: ```cpp "\033[?u" @@ -701,7 +730,7 @@ Before modifying `csihandle()`, create tests that feed these exact byte strings "\033[=1;3u" ``` -Expose or instrument parsed `CSIEscape` only in tests if needed. Capture: +The established parser representation is: - `priv` - `arg[]` @@ -710,7 +739,7 @@ Expose or instrument parsed `CSIEscape` only in tests if needed. Capture: - `mode[1]` - raw buffer -This prevents implementing against an incorrect mental model of the inherited `st` parser. +Do not add test-only parser exposure unless a future parser change requires it. ## 7.2 Add a dedicated handler @@ -798,7 +827,8 @@ constexpr unsigned int MAX_POP_COUNT = 64; The control sequence is application output; the response must be terminal input. -Use the same response path as device status reports. Ensure: +Handle and answer the query on the `TerminalSession` worker, using the same response path as device +status reports. Ensure: - no local echo, - no screen rendering, @@ -807,7 +837,9 @@ Use the same response path as device status reports. Ensure: ## 7.6 Debug tracing -Add optional protocol tracing controlled by a compile-time flag or existing logger level: +Use the existing `ETERM_LOG_ERRORS` diagnostics switch for malformed protocol diagnostics. If +verbose per-key tracing is later needed, give it a distinct trace-level switch so +`ETERM_LOG_ERRORS=1` does not log every key: ```text eterm kitty-kbd: query -> response flags=31 @@ -822,6 +854,12 @@ Do not log every key at normal log levels. # 8. Key event model and EEPP plumbing +The UI thread may identify terminal-local shortcuts and normalize EEPP event data, but it must +enqueue semantic events rather than encoded bytes. Add `KeyDownCommand`, `KeyUpCommand`, and +`TextInputCommand` to `TerminalSession`. The worker performs event correlation, reads the active +screen's Kitty state and terminal modes, chooses enhanced versus legacy encoding, and writes to +the PTY. + ## 8.1 Add key-up forwarding Current `UITerminal::onKeyUp()` returns `1` without forwarding: @@ -832,7 +870,7 @@ Uint32 UITerminal::onKeyUp( const KeyEvent& ) { } ``` -To implement release reporting: +To implement release reporting, forward a semantic release event into `TerminalSession`: ```cpp Uint32 UITerminal::onKeyUp( const KeyEvent& event ) { @@ -850,7 +888,7 @@ Uint32 UITerminal::onKeyUp( const KeyEvent& event ) { Add `TerminalDisplay::onKeyUp()`. -Only emit a release sequence when: +The worker only emits a release sequence when: ```cpp activeFlags & REPORT_EVENT_TYPES @@ -1078,11 +1116,11 @@ Do not invent code values. Copy them from the official key-code table and add a ## 10.3 Enter special case -With `DISAMBIGUATE_ESCAPE_CODES` enabled: +With only `DISAMBIGUATE_ESCAPE_CODES` enabled, Enter remains a legacy exception regardless of +modifiers. The same exception applies to Tab and Backspace, allowing a user to type `reset` if an +application crashes without restoring modes. -- Plain Enter may retain legacy `\r` unless another active flag requires CSI-u. -- Modified Enter must be encoded as CSI-u. -- Ctrl+Enter must become: +With `REPORT_ALL_KEYS_AS_ESCAPE_CODES` enabled, Enter is encoded as CSI-u. Ctrl+Enter becomes: ```text CSI 13 ; 5 u @@ -1108,7 +1146,9 @@ Ctrl+Shift+Enter: CSI 13 ; 6 u ``` -When `REPORT_ALL_KEYS_AS_ESCAPE_CODES` is active, plain Enter should also use the protocol form required by the spec. +Plain Enter also uses the protocol form when report-all is active. Event-type fields are included +only when requested. Tests and acceptance criteria must not expect `CSI 13;5u` from disambiguation +alone. ## 10.4 Legacy cursor/function-key forms under enhancement @@ -1205,7 +1245,8 @@ This feature should be implemented only after input-event correlation is sound. # 12. Progressive enhancement decision algorithm -Implement one deterministic function that decides enhanced versus legacy output. +Implement one deterministic function that decides enhanced versus legacy output. It is invoked by +the `TerminalSession` worker, never by rendering/UI code. Pseudo-code: @@ -1254,7 +1295,9 @@ KittyEncodedKey KittyKeyboardEncoder::encode( } ``` -The exact `mustEncode` rules must match the spec. The above is architecture, not a substitute for the official rules. +The exact `mustEncode` rules must match the spec. In particular, Enter, Tab, and Backspace remain +legacy under disambiguation alone. The above is architecture, not a substitute for the official +rules. --- @@ -1273,9 +1316,10 @@ TextInput("a") If Kitty `REPORT_ALL_KEYS_AS_ESCAPE_CODES` causes `KeyDown(A)` to emit a CSI-u sequence and `TextInput("a")` still emits `a`, the child receives the key twice. -## 13.2 Preferred solution: correlated suppression queue +## 13.2 Preferred solution: worker-side correlated suppression queue -Add a small queue of expected text-input codepoints after an enhanced keydown consumed the event: +Add a small queue of expected text-input codepoints after an enhanced keydown consumed the event. +Keep this queue beside the worker-side encoder, not in `TerminalDisplay`: ```cpp struct SuppressedTextInput { @@ -1308,7 +1352,7 @@ For printable keys under `REPORT_ALL_KEYS_AS_ESCAPE_CODES`: This is more correct for keyboard layouts and IME but requires careful event ordering and latency handling. -Recommended architecture: +Recommended worker-side architecture: ```cpp std::optional mPendingTextKey; @@ -1357,16 +1401,11 @@ Do not ship an implementation that duplicates printable characters. # 14. Changes to `TerminalDisplay` -## 14.1 Add active flag accessor +## 14.1 Do not expose active flags to the display -Use: - -```cpp -const auto flags = - mTerminal->getKeyboardEnhancementFlags(); -``` - -Do not cache flags in `TerminalDisplay`; application output may change them at any moment. +`TerminalDisplay` must not query `TerminalEmulator`, and a flag value copied through +`TerminalSnapshot` is not authoritative enough for input encoding. The display handles local UI +shortcuts and forwards the remaining semantic event to `TerminalSession`. ## 14.2 Refactor `onKeyDown()` @@ -1388,7 +1427,7 @@ void TerminalDisplay::onKeyDown( if ( handleTerminalShortcut( keyCode, smod ) ) return; - const KittyKeyEvent event{ + const TerminalKeyEvent event{ keyCode, scancode, chr, @@ -1398,33 +1437,17 @@ void TerminalDisplay::onKeyDown( : KittyKeyEventType::Press }; - const auto enhanced = KittyKeyboardEncoder::encode( - event, - mTerminal->getKeyboardEnhancementFlags() - ); - - if ( enhanced.handled ) { - mTerminal->ttywrite( - enhanced.bytes.data(), - enhanced.bytes.size(), - 1 - ); - - if ( enhanced.suppressTextInput ) - registerTextInputSuppression( event ); - - return; - } - - handleLegacyKeyDown( keyCode, chr, mod, scancode ); + mSession->keyDown( std::move( event ) ); } ``` -Extract current shortcut and legacy map logic into helpers to make tests possible: +Extract current shortcut logic into a helper. Move the child-facing Ctrl conversion and legacy +key map into a worker-side encoder/fallback so both legacy and enhanced output use authoritative +terminal modes: ```cpp bool handleTerminalShortcut(...); -void handleLegacyKeyDown(...); +KittyEncodedKey encodeChildKeyEvent(...); // worker only ``` ## 14.3 Add `onKeyUp()` @@ -1436,33 +1459,20 @@ void TerminalDisplay::onKeyUp( const Uint32& mod, const Scancode& scancode ) { - const auto flags = - mTerminal->getKeyboardEnhancementFlags(); - - if ( !( flags & REPORT_EVENT_TYPES ) ) - return; - - KittyKeyEvent event{ + TerminalKeyEvent event{ keyCode, scancode, chr, mod, KittyKeyEventType::Release }; - - const auto encoded = - KittyKeyboardEncoder::encode( event, flags ); - - if ( encoded.handled ) - mTerminal->ttywrite( - encoded.bytes.data(), - encoded.bytes.size(), - 1 - ); + mSession->keyUp( std::move( event ) ); } ``` -Terminal shortcuts consumed on keydown should generally not emit release events to the child. Track consumed physical keys until key-up if necessary: +The worker emits a release only if the negotiated flags require it. Terminal shortcuts consumed +on keydown should generally not emit release events to the child. Track consumed physical keys on +the UI side until key-up if necessary: ```cpp std::unordered_set mLocallyConsumedKeys; @@ -1474,17 +1484,15 @@ Refactor: ```cpp void TerminalDisplay::onTextInput( const Uint32& chr ) { - if ( !mTerminal ) + if ( !mSession ) return; - - if ( handlePendingKittyTextInput( chr ) ) - return; - - // Existing UTF-8 legacy behavior. + mSession->textInput( chr ); } ``` -Ensure a multi-codepoint text-input event is handled correctly. Current API appears to pass one `Uint32`; inspect whether EEPP emits one event per codepoint. +The worker correlates this command with pending key events and chooses enhanced versus legacy +output. Ensure a multi-codepoint text-input event is handled correctly. Current API appears to +pass one `Uint32`; inspect whether EEPP emits one event per codepoint. --- @@ -1494,6 +1502,7 @@ Required changes: - Forward key-up. - Forward repeat status if `KeyEvent` exposes it. +- Forward semantic key/text data through `TerminalSession`; never read emulator state. - Preserve focus checks. - Preserve custom keybinding command interception. - Clear pending Kitty input state on focus loss if there is an existing focus callback. @@ -1525,8 +1534,8 @@ Update interfaces and all callers. When active flags are zero: -- `TerminalDisplay::onKeyDown()` must execute the old code path. -- `onTextInput()` must behave exactly as before. +- semantic key/text commands must produce the same bytes as the old `TerminalDisplay` path, +- worker-side legacy text input must behave exactly as before, - `onKeyUp()` must emit nothing. - Existing `Ctrl+A` through control-code conversion must remain unchanged. - Alt+Enter must retain `ESC CR`. @@ -1540,7 +1549,9 @@ Add regression tests comparing old expected output bytes. # 17. Handling Ctrl-letter logic -Current code manually maps Ctrl plus certain scancodes to bytes `0x01` etc. This block must occur **after** enhanced protocol encoding. +Current code manually maps Ctrl plus certain scancodes to bytes `0x01` etc. Move this child-facing +logic from `TerminalDisplay` into the worker-side encoder/fallback, after enhanced protocol +encoding. Reason: @@ -1564,10 +1575,12 @@ Do not leave the Ctrl block before the enhanced encoder. Distinguish: - Child output is parsed by `TerminalEmulator`. -- Terminal responses and user input are written to the PTY. +- Terminal responses and semantic input commands are encoded on the worker and written to the PTY. - Renderer output must never contain protocol responses. - `ttywrite(..., may_echo=0)` is likely correct for terminal-generated responses. - User key input should maintain current `may_echo` semantics, likely `1`. +- UI code must never call `TerminalEmulator::ttywrite()` directly or base encoding on a potentially + stale snapshot. Audit current uses: @@ -1703,20 +1716,23 @@ Enter -> 0d Ctrl+Enter -> 0d // current legacy behavior retained ``` -With disambiguation: +With disambiguation but without report-all: ```text -Enter -> legacy CR, unless spec requires encoded form -Ctrl+Enter -> 1b 5b 31 33 3b 35 75 -Shift+Enter -> 1b 5b 31 33 3b 32 75 -Alt+Enter -> protocol form required by spec -Ctrl+Shift+Enter -> protocol form with modifier 6 +Enter -> legacy CR +Ctrl+Enter -> legacy CR +Shift+Enter -> legacy CR +Alt+Enter -> legacy ESC CR +Ctrl+Shift+Enter -> legacy CR ``` With report-all: ```text -Enter -> enhanced form +Enter -> enhanced form +Ctrl+Enter -> 1b 5b 31 33 3b 35 75 +Shift+Enter -> 1b 5b 31 33 3b 32 75 +Ctrl+Shift+Enter -> protocol form with modifier 6 ``` With event types: @@ -1817,10 +1833,10 @@ Enter -> 0d Ctrl+Enter -> 0d ``` -Use a helper to activate flags: +Use a helper to activate report-all (which implies disambiguation): ```bash -printf '\033[>1u' +printf '\033[>8u' ``` Then press Ctrl+Enter and verify: @@ -1850,23 +1866,26 @@ ESC [ ? u and read: ```text -ESC [ ? u +ESC [ ? u ``` ## 21.3 Codex CLI 1. Build and run eTerm from the modified `develop` branch. -2. Start current Codex CLI. -3. Configure Codex: +2. Start current Codex CLI and capture its push/set sequence. +3. Verify Codex requests `REPORT_ALL_KEYS_AS_ESCAPE_CODES` (bit `8`). If it requests only flags + `7`, record that standards-compliant `Ctrl+Enter` disambiguation is unavailable and do not make + the terminal silently deviate from Kitty. +4. Configure Codex: - Enter inserts newline. - Ctrl+Enter submits. -4. Type a multiline prompt. -5. Press Enter: newline appears. -6. Press Ctrl+Enter: message submits. -7. Hold/repeat keys to ensure no duplicate or stuck events. -8. Exit Codex normally. -9. Verify shell input returns to legacy behavior. -10. Kill Codex abruptly and verify a restarted shell/process does not inherit stale flags. +5. Type a multiline prompt. +6. Press Enter: newline appears. +7. Press Ctrl+Enter: message submits when report-all is active. +8. Hold/repeat keys to ensure no duplicate or stuck events. +9. Exit Codex normally. +10. Verify shell input returns to legacy behavior. +11. Kill Codex abruptly and verify a restarted shell/process does not inherit stale flags. ## 21.4 Other TUIs @@ -1921,25 +1940,23 @@ Use constants and comments. # 23. Threading and lifetime -Determine which thread: +The thread model is established: -- parses PTY output, -- handles UI input, -- reads active flags. +- the `TerminalSession` worker exclusively owns `TerminalEmulator`, PTY reads/writes, parser state, + terminal modes, Kitty flags/stacks, pending key/text correlation, and child-input encoding; +- the UI thread handles terminal-local shortcuts and enqueues semantic commands; +- immutable snapshots are presentation-only and must not drive input encoding; +- no emulator accessor, atomic flag mirror, or mutex-protected stack is needed. -If parser updates and UI reads can occur on different threads, `flags` access is a data race. +Typed key and text commands participate in the existing ordered command queue. Add deterministic +tests for negotiation and key commands arriving close together. The worker must give already +available PTY negotiation a fair bounded parsing opportunity without allowing heavy output to +starve input commands. Query/response capability detection is also an ordering barrier: an +application cannot observe the Kitty query response before the worker has applied the state that +produced it. -Options: - -1. Confirm all terminal update/input operations run on the main thread. -2. If not, use: - - atomic active flags, - - mutex around stack mutation, - - or message passing. - -Stacks are modified rarely; a mutex is acceptable. Do not add atomics without protecting the vectors. - -Document the threading guarantee in code. +Reset both screen states in full reset and explicitly in `RestartCommand` so a replacement process +cannot inherit flags or pending input correlation from the old child. --- @@ -2019,12 +2036,13 @@ allowing modern TUIs to distinguish modified Enter and other keys. Deliverables: -- Parser tests for private CSI prefixes +- Parser tests for private CSI prefixes (**completed**; prefixed Kitty sequences are currently + recognized and deliberately ignored without a response) - Legacy byte snapshots - Confirm keydown/textinput ordering - Confirm repeat exposure - Confirm modifier constants -- Confirm thread model +- Confirm thread model (**completed**; emulator and protocol state are worker-owned) No functional change. @@ -2038,6 +2056,7 @@ Deliverables: - Push/pop/set - Reset behavior - Unit tests +- Typed semantic key/text `TerminalSession` commands, initially using legacy worker-side encoding Still no enhanced key emission. @@ -2048,7 +2067,8 @@ Deliverables: - Encoder framework - Enter, Tab, Escape, Backspace - Modified key CSI-u emission -- Ctrl+Enter works in Crossterm/Codex +- Modified ASCII/Escape disambiguation works in Crossterm +- Ctrl+Enter remains legacy unless report-all is active - Legacy fallback intact This phase can be merged independently if desired. @@ -2121,9 +2141,10 @@ The implementation is complete only when all of the following are true. ## Encoding - [ ] Plain legacy behavior is unchanged with flags zero. -- [ ] Ctrl+Enter is `CSI 13;5u` when disambiguation is active. -- [ ] Shift+Enter is distinguishable. -- [ ] Modified Escape, Tab, and Backspace are distinguishable. +- [ ] Ctrl+Enter is `CSI 13;5u` when report-all is active. +- [ ] Shift+Enter is distinguishable when report-all is active. +- [ ] Modified Escape is distinguishable under disambiguation; modified Tab and Backspace are + distinguishable when report-all is active. - [ ] Special/function keys follow official Kitty encoding. - [ ] Printable Unicode keys work in report-all mode. - [ ] No duplicate text-input events occur. @@ -2133,7 +2154,8 @@ The implementation is complete only when all of the following are true. ## Integration - [ ] Crossterm decodes the events with correct code/modifier/kind. -- [ ] Codex can bind Ctrl+Enter separately from Enter. +- [ ] Codex can bind Ctrl+Enter separately from Enter when it requests report-all, or any deliberate + compatibility deviation is separately designed and documented. - [ ] Shell, Vim/Neovim, and existing terminal use remain unaffected when protocol is inactive. - [ ] Abrupt child termination does not leave stale protocol state for a replacement process. @@ -2191,16 +2213,33 @@ KittyKeyboardState& activeKeyboardState(); const KittyKeyboardState& activeKeyboardState() const; bool handleKittyKeyboardProtocol(); -void replyKittyKeyboardProtocolSupport(); +void replyKittyKeyboardProtocolState(); void resetKittyKeyboardProtocol(); ``` -Public: +No public emulator accessor is added. State and stack access remain worker-only. + +## `src/modules/eterm/include/eterm/terminal/terminalsession.hpp` + +Add semantic event data and typed commands: ```cpp -std::uint32_t getKeyboardEnhancementFlags() const; +void keyDown( TerminalKeyEvent event ); +void keyUp( TerminalKeyEvent event ); +void textInput( TerminalTextInput event ); + +struct KeyDownCommand { TerminalKeyEvent event; }; +struct KeyUpCommand { TerminalKeyEvent event; }; +struct TextInputCommand { TerminalTextInput event; }; ``` +Add them to the ordered command variant. + +## `src/modules/eterm/src/eterm/terminal/terminalsession.cpp` + +Handle semantic input on the worker. Read the active Kitty state and current emulator modes there, +perform key/text correlation, encode either Kitty or legacy bytes, and call `ttywrite()`. + ## `src/modules/eterm/src/eterm/terminal/terminalemulator.cpp` Modify: @@ -2213,7 +2252,8 @@ Modify: - terminal-generated query response - optional debug tracing -Remove the current no-op placeholder only after replacing it with real handling. +Replace the current prefixed-`u` no-op only after negotiation state and enhanced input encoding are +implemented together. ## `src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp` @@ -2221,20 +2261,18 @@ Add: - `onKeyUp` - repeat parameter or event kind -- pending/suppressed text state - locally consumed key tracking -- helper declarations for enhanced and legacy paths +- helper declarations for terminal-local shortcuts ## `src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp` Modify: -- place enhanced encoding before Ctrl-letter legacy handling - retain terminal shortcut priority - add key-up behavior - add repeat behavior -- add text-input correlation -- preserve legacy map unchanged as fallback +- enqueue semantic key/text commands instead of encoded bytes +- move child-facing Ctrl conversion and the legacy map into the worker-side fallback ## `src/modules/eterm/src/eterm/ui/uiterminal.cpp` @@ -2264,12 +2302,12 @@ Before submitting: 8. Verify release events for locally consumed shortcuts are suppressed. 9. Verify stack depth and pop count are bounded. 10. Verify unsupported bits are masked. -11. Verify query response mask matches actual implementation. +11. Verify the query returns current effective flags, not the supported mask. 12. Verify Unicode scalar validation. 13. Verify AltGr/IME behavior. 14. Verify legacy snapshots with flags zero. 15. Verify Crossterm key dump. -16. Verify Codex Ctrl+Enter. +16. Verify Codex's requested flags before expecting Ctrl+Enter to be distinct. 17. Verify process restart clears stale state. 18. Run formatter and full test suite. @@ -2280,7 +2318,7 @@ Before submitting: A very small patch can solve only the immediate issue: ```cpp -if ( flags & DISAMBIGUATE_ESCAPE_CODES && +if ( flags & REPORT_ALL_KEYS_AS_ESCAPE_CODES && keyCode == KEY_RETURN && mod & KEYMOD_CTRL ) { ttywrite( "\033[13;5u", 7, 1 ); @@ -2288,7 +2326,9 @@ if ( flags & DISAMBIGUATE_ESCAPE_CODES && } ``` -This is **not** the requested final implementation. It may be useful as a temporary proof of concept, but it is incomplete because it omits: +This is **not** the requested final implementation. It is shown only as an illustration of the +report-all requirement and must not be added on the UI thread. A production implementation also +cannot omit: - capability query, - push/pop stack, @@ -2316,13 +2356,13 @@ ESC [ ? u eTerm replies: ```text -ESC [ ? 31 u +ESC [ ? 0 u ``` Application sends: ```text -ESC [ > 7 u +ESC [ > 15 u ``` eTerm stores prior flags on the current screen’s stack and activates: @@ -2331,6 +2371,7 @@ eTerm stores prior flags on the current screen’s stack and activates: DISAMBIGUATE_ESCAPE_CODES REPORT_EVENT_TYPES REPORT_ALTERNATE_KEYS +REPORT_ALL_KEYS_AS_ESCAPE_CODES ``` User presses Ctrl+Enter. @@ -2364,7 +2405,8 @@ KeyModifiers::CONTROL KeyEventKind::Press ``` -Codex matches its Ctrl+Enter submit binding. +An application such as Codex can match its Ctrl+Enter submit binding when it requests report-all. +If it pushes only flags `7`, Enter remains in its legacy form by design. Application exits cleanly and sends: diff --git a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp index 76904182d..bf8505f11 100644 --- a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp @@ -356,6 +356,7 @@ class TerminalDisplay { VertexBufferUniquePtr mVBForeground; std::vector mVBStyles; TerminalColorScheme mColorScheme; + TerminalColorScheme mInitialColorScheme; Uint32 mQuadVertex{ 6 }; Primitives mPrimitives; Vector2u mCurGridPos; diff --git a/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp b/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp index e99d4197e..1ac34934d 100644 --- a/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp @@ -55,7 +55,7 @@ using namespace eterm::System; namespace eterm { namespace Terminal { constexpr int ESC_BUF_SIZ = 512; -constexpr int ESC_ARG_SIZ = 16; +constexpr int ESC_ARG_SIZ = 32; constexpr int STR_BUF_SIZ = ESC_BUF_SIZ; constexpr int STR_ARG_SIZ = ESC_ARG_SIZ; @@ -104,6 +104,9 @@ struct CSIEscape { size_t len; /* raw string length */ char priv; int arg[ESC_ARG_SIZ]; + /* Separator following each argument. ECMA-48 uses ';' between parameters and ':' + * between subparameters, so preserving it is required for modern SGR colors. */ + char sep[ESC_ARG_SIZ]; int narg; /* nb of args */ char mode[2]; }; @@ -251,6 +254,12 @@ class TerminalEmulator final { /** Worker-only maximum interval between snapshots while PTY reads remain saturated. */ void setPresentationInterval( Time interval ); + /** Set the user-configured cursor style restored by DECSCUSR parameter 7. */ + void setDefaultCursorMode( TerminalCursorMode mode ); + + /** Worker-only notification that the display palette changed. */ + void notifyColorSchemeChanged(); + Vector2i getSize() const; System::IProcess* getProcess() const; @@ -285,6 +294,7 @@ class TerminalEmulator final { bool mAllDirty{ true }; Clock mPresentationClock; Time mPresentationInterval{ Microseconds( 1000000.0 / 60.0 ) }; + Clock mSynchronizedUpdateClock; bool mAllowMemoryTrimnming{ false }; int mExitCode; @@ -305,6 +315,9 @@ class TerminalEmulator final { int mAllowAltScreen; int mAllowWindowOps; + TerminalCursorMode mDefaultCursorMode{ SteadyUnderline }; + bool mColorSchemeNotifications{ false }; + int mColorScheme{ 0 }; std::string mCurrentWorkingDirectory; Vector2i mLastMousePosition{ -1, -1 }; @@ -362,7 +375,7 @@ class TerminalEmulator final { void historyStealPush( Line* lineSlot, int col ); void historyReflow( int old_col, int new_col ); void historyPopToScreen( int loaded, int col ); - void tsetattr( int*, int ); + void tsetattr( int*, int, const char* ); void tsetchar( Rune, TerminalGlyph*, int, int ); void tsetdirt( int, int ); void tsetscroll( int, int ); @@ -373,7 +386,7 @@ class TerminalEmulator final { void tcontrolcode( uchar ); void tdectest( char ); void tdefutf8( char ); - int32_t tdefcolor( int*, int*, int ); + int32_t tdefcolor( int*, const char*, int*, int ); void tdeftran( char ); void tstrsequence( uchar ); @@ -413,6 +426,8 @@ class TerminalEmulator final { int xgetcolor( int x, unsigned char* r, unsigned char* g, unsigned char* b ); void osc_color_response( int num, int index, int is_osc4 ); void handleDeviceAttributes(); + int colorScheme(); + void reportColorScheme(); void trimMemory(); diff --git a/src/modules/eterm/include/eterm/terminal/terminaltypes.hpp b/src/modules/eterm/include/eterm/terminal/terminaltypes.hpp index d9fcaae6d..ed8df0c90 100644 --- a/src/modules/eterm/include/eterm/terminal/terminaltypes.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminaltypes.hpp @@ -115,6 +115,7 @@ enum TerminalWinMode { MODE_MOUSEMANY = 1 << 15, MODE_BRCKTPASTE = 1 << 16, MODE_NUMLOCK = 1 << 17, + MODE_ALTSCRROLL = 1 << 18, MODE_MOUSE = MODE_MOUSEBTN | MODE_MOUSEMOTION | MODE_MOUSEX10 | MODE_MOUSEMANY, }; diff --git a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp index ac50d7439..1256fcf1b 100644 --- a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp @@ -450,9 +450,8 @@ std::shared_ptr TerminalDisplay::create( std::shared_ptr terminal = std::shared_ptr( new TerminalDisplay( window, font, fontSize, pixelsSize, useFrameBuffer ) ); - terminal->mSession = - TerminalSession::create( std::move( pseudoTerminal ), std::move( process ), historySize, - terminal->makeColorPalette() ); + terminal->mSession = TerminalSession::create( std::move( pseudoTerminal ), std::move( process ), + historySize, terminal->makeColorPalette() ); if ( !terminal->mSession ) { if ( freeProcessFactory ) eeSAFE_DELETE( processFactory ); @@ -484,7 +483,8 @@ TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const mFontSize( fontSize ), mSize( pixelsSize ), mUseFrameBuffer( useFrameBuffer ), - mColorScheme( TerminalColorScheme::getDefault() ) { + mColorScheme( TerminalColorScheme::getDefault() ), + mInitialColorScheme( mColorScheme ) { TerminalGlyph defaultGlyph; defaultGlyph.mode = ATTR_INVISIBLE; mCursorGlyph = defaultGlyph; @@ -504,26 +504,33 @@ TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const } void TerminalDisplay::resetColors() { + mColorScheme = mInitialColorScheme; for ( Uint32 i = 0; i < eeARRAY_SIZE( colormapped ); i++ ) - resetColor( i, i < mColorScheme.getPaletteSize() - ? mColorScheme.getPaletteIndex( i ).toHexString().c_str() + resetColor( i, i < mInitialColorScheme.getPaletteSize() + ? mInitialColorScheme.getPaletteIndex( i ).toHexString().c_str() : nullptr ); } int TerminalDisplay::resetColor( const Uint32& index, const char* name ) { if ( !name ) { if ( index < mColors.size() ) { - Color col = 0x000000FF; - - if ( index < 256 ) - col = colormapped[index]; + const Color col = index < mInitialColorScheme.getPaletteSize() + ? mInitialColorScheme.getPaletteIndex( index ) + : colormapped[index]; mColors[index] = col; mColorScheme.setPaletteIndex( index, col ); return 0; + } else if ( index == 256 || index == 257 ) { + mColorScheme.setCursor( mInitialColorScheme.getCursor() ); + return 0; + } else if ( index == 258 ) { + mColorScheme.setForeground( mInitialColorScheme.getForeground() ); + return 0; + } else if ( index == 259 ) { + mColorScheme.setBackground( mInitialColorScheme.getBackground() ); + return 0; } - // Reset to default for 256, 257, 258, 259 is not well defined here without original - // defaults return 1; } @@ -549,10 +556,7 @@ int TerminalDisplay::resetColor( const Uint32& index, const char* name ) { } } } else if ( String::iequals( "default", name ) ) { - unsigned char r, g, b; - getColor( index, &r, &g, &b ); - col = Color( r, g, b, 255 ); - colorParsed = true; + return resetColor( index, nullptr ); } else if ( Color::isColorString( std::string_view{ name }, true ) ) { col = Color::fromString( name ); colorParsed = true; @@ -677,6 +681,7 @@ const TerminalColorScheme& TerminalDisplay::getColorScheme() const { } void TerminalDisplay::setColorScheme( const TerminalColorScheme& colorScheme ) { + mInitialColorScheme = colorScheme; mColorScheme = colorScheme; resetColors(); if ( mSession ) diff --git a/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp b/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp index f2c41b860..aaed40aa1 100644 --- a/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp @@ -41,6 +41,7 @@ #include #include #include +#include using namespace EE::Network; using namespace EE::System; @@ -76,6 +77,52 @@ extern "C" { namespace eterm { namespace Terminal { +namespace { + +bool terminalDiagnosticsEnabled() { +#ifdef EE_DEBUG + return true; +#else + // Release builds stay quiet unless diagnostics are explicitly requested. This is cached because + // malformed or unsupported sequences can otherwise hit this path for every rendered cell. + static const char* setting = getenv( "ETERM_LOG_ERRORS" ); + static const bool enabled = + setting != nullptr && setting[0] != '\0' && strcmp( setting, "0" ) != 0; + return enabled; +#endif +} + +void terminalDiagnostic( const char* format, ... ) { + if ( !terminalDiagnosticsEnabled() ) + return; + va_list args; + va_start( args, format ); + vfprintf( stderr, format, args ); + va_end( args ); +} + +TerminalCursorMode blinkingCursorVariant( TerminalCursorMode mode ) { + switch ( mode ) { + case BlinkUnderline: + case SteadyUnderline: + return BlinkUnderline; + case BlinkBar: + case SteadyBar: + return BlinkBar; + case BlinkingBlock: + case BlinkingBlockDefault: + case SteadyBlock: + return BlinkingBlock; + case StExtension: + return StExtension; + case TerminalCursorMode::MAX_CURSOR: + return BlinkingBlock; + } + return BlinkingBlock; +} + +} // namespace + #if defined( EE_ARCH_X86_64 ) #if defined( __GNUC__ ) || defined( __clang__ ) __attribute__( ( target( "avx2" ) ) ) @@ -848,6 +895,17 @@ void TerminalEmulator::setPresentationInterval( Time interval ) { mPresentationInterval = interval > Time::Zero ? interval : Microseconds( 1000000.0 / 60.0 ); } +void TerminalEmulator::setDefaultCursorMode( TerminalCursorMode mode ) { + mDefaultCursorMode = mode; +} + +void TerminalEmulator::notifyColorSchemeChanged() { + const int scheme = colorScheme(); + if ( mColorSchemeNotifications && mColorScheme != 0 && scheme != mColorScheme ) + reportColorScheme(); + mColorScheme = scheme; +} + Vector2i TerminalEmulator::getSize() const { return { mTerm.col, mTerm.row }; } @@ -962,6 +1020,8 @@ void TerminalEmulator::tcursor( int mode ) { void TerminalEmulator::treset( void ) { uint i; + mColorSchemeNotifications = false; + mTerm.is_syncing = false; mTerm.c = TerminalCursor{}; mTerm.c.attr = TerminalGlyph{}; mTerm.c.attr.u = ' '; @@ -990,9 +1050,13 @@ void TerminalEmulator::treset( void ) { xsetmode( 0, MODE_MOUSE | MODE_MOUSESGR | MODE_APPKEYPAD | MODE_APPCURSOR | MODE_FOCUS | MODE_BRCKTPASTE | MODE_MOUSEX10 | MODE_MOUSEMANY ); + // Preserve eterm's established behavior and xterm's default alternateScroll resource. + xsetmode( 1, MODE_ALTSCRROLL ); auto dpy = mDpy.lock(); - if ( dpy ) + if ( dpy ) { dpy->setMode( MODE_VISIBLE, 1 ); + dpy->setCursorMode( mDefaultCursorMode ); + } } void TerminalEmulator::tnew( int col, int row, size_t historySize ) { @@ -1419,27 +1483,25 @@ void TerminalEmulator::tnewline( int first_col ) { void TerminalEmulator::csiparse( void ) { char *p = mCsiescseq.buf, *np; long int v; - int sep = ';'; /* colon or semi-colon, but not both */ mCsiescseq.narg = 0; - if ( *p == '?' ) { - mCsiescseq.priv = 1; + if ( *p == '<' || *p == '=' || *p == '>' || *p == '?' ) { + mCsiescseq.priv = *p; p++; } mCsiescseq.buf[mCsiescseq.len] = '\0'; - while ( p < mCsiescseq.buf + mCsiescseq.len ) { + while ( p < mCsiescseq.buf + mCsiescseq.len && mCsiescseq.narg < ESC_ARG_SIZ ) { np = NULL; v = strtol( p, &np, 10 ); if ( np == p ) v = 0; if ( v == LONG_MAX || v == LONG_MIN ) v = -1; - mCsiescseq.arg[mCsiescseq.narg++] = v; + mCsiescseq.arg[mCsiescseq.narg] = v; p = np; - if ( sep == ';' && *p == ':' ) - sep = ':'; /* allow override to colon once */ - if ( *p != sep || mCsiescseq.narg == ESC_ARG_SIZ ) + mCsiescseq.sep[mCsiescseq.narg++] = *p == ';' || *p == ':' ? *p : '\0'; + if ( *p != ';' && *p != ':' ) break; p++; } @@ -1596,33 +1658,63 @@ void TerminalEmulator::tdeleteline( int n ) { tscrollup( mTerm.c.y, n, 0 ); } -int32_t TerminalEmulator::tdefcolor( int* attr, int* npar, int l ) { +int32_t TerminalEmulator::tdefcolor( int* attr, const char* separators, int* npar, int l ) { int32_t idx = -1; uint r, g, b; + if ( !attr || !npar || l < 0 || l > ESC_ARG_SIZ || *npar < 0 || *npar >= l ) { + terminalDiagnostic( "erresc(color): invalid parameter index\n" ); + return idx; + } + const bool subparameters = separators && separators[*npar] == ':'; + const int selector = *npar + 1; - switch ( attr[*npar + 1] ) { + if ( selector >= l ) { + terminalDiagnostic( "erresc(color): missing color type\n" ); + return idx; + } + + switch ( attr[selector] ) { case 2: /* direct color in RGB space */ - if ( *npar + 4 >= l ) { - fprintf( stderr, "erresc(38): Incorrect number of parameters (%d)\n", *npar ); + if ( subparameters ) { + int end = selector; + while ( end < l - 1 && separators[end] == ':' ) + ++end; + const int componentCount = end - selector; + if ( componentCount != 3 && componentCount != 4 ) { + terminalDiagnostic( "erresc(color): invalid RGB subparameter count %d\n", + componentCount ); + *npar = end; + break; + } + const int rgb = selector + ( componentCount == 4 ? 2 : 1 ); + r = attr[rgb]; + g = attr[rgb + 1]; + b = attr[rgb + 2]; + *npar = end; + } else if ( *npar + 4 < l ) { + r = attr[*npar + 2]; + g = attr[*npar + 3]; + b = attr[*npar + 4]; + *npar += 4; + } else { + terminalDiagnostic( "erresc(color): incorrect number of RGB parameters (%d)\n", + *npar ); break; } - r = attr[*npar + 2]; - g = attr[*npar + 3]; - b = attr[*npar + 4]; - *npar += 4; if ( !BETWEEN( r, 0, 255 ) || !BETWEEN( g, 0, 255 ) || !BETWEEN( b, 0, 255 ) ) - fprintf( stderr, "erresc: bad rgb color (%u,%u,%u)\n", r, g, b ); + terminalDiagnostic( "erresc: bad rgb color (%u,%u,%u)\n", r, g, b ); else idx = TRUECOLOR( r, g, b ); break; case 5: /* indexed color */ if ( *npar + 2 >= l ) { - fprintf( stderr, "erresc(38): Incorrect number of parameters (%d)\n", *npar ); + terminalDiagnostic( "erresc(color): incorrect number of indexed parameters (%d)\n", + *npar ); break; } *npar += 2; if ( !BETWEEN( attr[*npar], 0, 255 ) ) - fprintf( stderr, "erresc: bad fgcolor %d\n", attr[*npar] ); + terminalDiagnostic( "erresc: bad indexed color %d\n", attr[*npar] ); else idx = attr[*npar]; break; @@ -1631,14 +1723,18 @@ int32_t TerminalEmulator::tdefcolor( int* attr, int* npar, int l ) { case 3: /* direct color in CMY space */ case 4: /* direct color in CMYK space */ default: - fprintf( stderr, "erresc(38): gfx attr %d unknown\n", attr[*npar] ); + terminalDiagnostic( "erresc(color): color type %d unknown\n", attr[selector] ); + if ( subparameters ) { + while ( *npar < l - 1 && separators[*npar] == ':' ) + ++*npar; + } break; } return idx; } -void TerminalEmulator::tsetattr( int* attr, int l ) { +void TerminalEmulator::tsetattr( int* attr, int l, const char* separators ) { // Check if this is a private sequence (should be ignored for SGR) // Private sequences start with '?' and should not affect text attributes if ( mCsiescseq.priv ) { @@ -1706,14 +1802,14 @@ void TerminalEmulator::tsetattr( int* attr, int l ) { mTerm.c.attr.mode &= ~ATTR_STRUCK; break; case 38: - if ( ( idx = tdefcolor( attr, &i, l ) ) >= 0 ) + if ( ( idx = tdefcolor( attr, separators, &i, l ) ) >= 0 ) mTerm.c.attr.fg = idx; break; case 39: /* set foreground color to default */ mTerm.c.attr.fg = mDefaultFg; break; case 48: - if ( ( idx = tdefcolor( attr, &i, l ) ) >= 0 ) + if ( ( idx = tdefcolor( attr, separators, &i, l ) ) >= 0 ) mTerm.c.attr.bg = idx; break; case 49: /* set background color to default */ @@ -1723,7 +1819,9 @@ void TerminalEmulator::tsetattr( int* attr, int l ) { /* This starts a sequence to change the color of * "underline" pixels. We don't support that and * instead eat up a following "5;n" or "2;r;g;b". */ - tdefcolor( attr, &i, l ); + tdefcolor( attr, separators, &i, l ); + break; + case 59: /* reset underline color (unsupported, therefore a no-op) */ break; default: if ( BETWEEN( attr[i], 30, 37 ) ) { @@ -1735,8 +1833,9 @@ void TerminalEmulator::tsetattr( int* attr, int l ) { } else if ( BETWEEN( attr[i], 100, 107 ) ) { mTerm.c.attr.bg = attr[i] - 100 + 8; } else { - fprintf( stderr, "erresc(default): gfx attr %d unknown\n", attr[i] ); - csidump(); + terminalDiagnostic( "erresc(default): gfx attr %d unknown\n", attr[i] ); + if ( terminalDiagnosticsEnabled() ) + csidump(); } break; } @@ -1817,6 +1916,9 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) { case 1006: /* 1006: extended reporting mode */ xsetmode( set, MODE_MOUSESGR ); break; + case 1007: /* wheel sends cursor keys on the alternate screen */ + xsetmode( set, MODE_ALTSCRROLL ); + break; case 1034: xsetmode( set, MODE_8BIT ); break; @@ -1856,19 +1958,23 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) { case 1039: /* ESC to Meta (not implemented) */ break; case 2026: { - // IGNORE DECSET/DECRST 2026 for sync updates? - // (https://codeberg.org/dnkl/foot/pulls/461/files) - // mTerm.is_syncing = ( set == 1 ); - /* if ( !mTerm.is_syncing ) { - // When syncing ends, we must perform the deferred draw + if ( set ) { + mTerm.is_syncing = true; + mSynchronizedUpdateClock.restart(); + } else if ( mTerm.is_syncing ) { + mTerm.is_syncing = false; + // Publish the complete frame immediately instead of waiting for the next + // presentation deadline. draw(); - } */ + } break; } + case 2031: /* Light/dark color-scheme change notifications. */ + mColorSchemeNotifications = set; + mColorScheme = colorScheme(); + break; default: -#ifdef EE_DEBUG - fprintf( stderr, "erresc: unknown private set/reset mode %d\n", *args ); -#endif + terminalDiagnostic( "erresc: unknown private set/reset mode %d\n", *args ); break; } } else { @@ -1888,9 +1994,7 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) { MODBIT( mTerm.mode, set, MODE_CRLF ); break; default: -#ifdef EE_DEBUG - fprintf( stderr, "erresc: unknown set/reset mode %d\n", *args ); -#endif + terminalDiagnostic( "erresc: unknown set/reset mode %d\n", *args ); break; } } @@ -1898,7 +2002,13 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) { } void TerminalEmulator::handleDeviceAttributes() { - if ( mCsiescseq.priv ) { + if ( mCsiescseq.priv == '>' ) { + char buf[64]; + const auto version = EE::Version::getVersion(); + const int revision = version.major * 10000 + version.minor * 100 + version.patch; + const int len = snprintf( buf, sizeof( buf ), "\033[>0;%d;0c", revision ); + ttywrite( buf, len, 0 ); + } else if ( mCsiescseq.priv == '?' ) { char buf[64]; int len; // Private Device Attributes - respond with terminal capabilities @@ -1921,7 +2031,7 @@ void TerminalEmulator::handleDeviceAttributes() { // Unknown private DA query break; } - } else { + } else if ( !mCsiescseq.priv ) { // Standard DA - respond with VT100 identification ttywrite( vtiden, strlen( vtiden ), 0 ); } @@ -1936,10 +2046,8 @@ void TerminalEmulator::csihandle( void ) { switch ( mCsiescseq.mode[0] ) { default: unknown: -#ifdef EE_DEBUG - fprintf( stderr, "erresc: unknown csi " ); + terminalDiagnostic( "erresc: unknown csi " ); csidump(); -#endif /* die(""); */ break; case '@': /* ICH -- Insert blank char */ @@ -2076,7 +2184,9 @@ void TerminalEmulator::csihandle( void ) { tinsertblankline( mCsiescseq.arg[0] ); break; case 'l': /* RM -- Reset Mode */ - tsetmode( mCsiescseq.priv, 0, mCsiescseq.arg, mCsiescseq.narg ); + if ( mCsiescseq.priv && mCsiescseq.priv != '?' ) + goto unknown; + tsetmode( mCsiescseq.priv == '?', 0, mCsiescseq.arg, mCsiescseq.narg ); break; case 'M': /* DL -- Delete lines */ DEFAULT( mCsiescseq.arg[0], 1 ); @@ -2099,41 +2209,19 @@ void TerminalEmulator::csihandle( void ) { tmoveato( mTerm.c.x, mCsiescseq.arg[0] - 1 ); break; case 'h': /* SM -- Set terminal mode */ - tsetmode( mCsiescseq.priv, 1, mCsiescseq.arg, mCsiescseq.narg ); + if ( mCsiescseq.priv && mCsiescseq.priv != '?' ) + goto unknown; + tsetmode( mCsiescseq.priv == '?', 1, mCsiescseq.arg, mCsiescseq.narg ); break; case 'm': /* SGR -- Terminal attribute (color) */ - tsetattr( mCsiescseq.arg, mCsiescseq.narg ); - break; - case '>': /* Private sequences */ - switch ( mCsiescseq.mode[1] ) { - case '4': /* Extended underline styles ESC[>4;Nm */ - // Extended underline styles - fallback to standard underline - /* DEFAULT( mCsiescseq.arg[0], 1 ); - switch ( mCsiescseq.arg[0] ) { - case 0: // No underline - fallback to ESC[24m - { - int fallback_args[] = { 24 }; // Reset underline - tsetattr( fallback_args, 1 ); - } break; - case 1: // Straight underline - fallback to ESC[4m - case 2: // Double underline - case 3: // Curly underline - case 4: // Dotted underline - case 5: // Dashed underline - { - int fallback_args[] = { 4 }; // Standard underline - tsetattr( fallback_args, 1 ); - } break; - default: - goto unknown; - } */ - break; - default: - goto unknown; - } + if ( mCsiescseq.priv == '>' ) + break; // Extended underline styles are not rendered yet. + tsetattr( mCsiescseq.arg, mCsiescseq.narg, mCsiescseq.sep ); break; case 'n': /* DSR – Device Status Report (cursor position) */ - if ( mCsiescseq.arg[0] == 6 ) { + if ( mCsiescseq.priv == '?' && mCsiescseq.arg[0] == 996 ) { + reportColorScheme(); + } else if ( !mCsiescseq.priv && mCsiescseq.arg[0] == 6 ) { len = snprintf( buf, sizeof( buf ), "\033[%i;%iR", mTerm.c.y + 1, mTerm.c.x + 1 ); ttywrite( buf, len, 0 ); } @@ -2152,7 +2240,12 @@ void TerminalEmulator::csihandle( void ) { tcursor( CURSOR_SAVE ); break; case 'u': /* DECRC -- Restore cursor position (ANSI.SYS) */ - if ( mCsiescseq.priv ) { + if ( mCsiescseq.priv == '?' || mCsiescseq.priv == '>' || mCsiescseq.priv == '<' || + mCsiescseq.priv == '=' ) { + // Kitty keyboard protocol. Remain in legacy mode and do not answer its query: a + // response would claim support and require encoding all subsequent key events. + break; + } else if ( mCsiescseq.priv ) { goto unknown; } else { tcursor( CURSOR_LOAD ); @@ -2161,20 +2254,27 @@ void TerminalEmulator::csihandle( void ) { case ' ': switch ( mCsiescseq.mode[1] ) { case 'q': /* DECSCUSR -- Set Cursor Style */ - if ( mCsiescseq.arg[0] < 0 || - mCsiescseq.arg[0] < TerminalCursorMode::MAX_CURSOR ) + if ( mCsiescseq.priv || mCsiescseq.arg[0] < 0 || + mCsiescseq.arg[0] >= TerminalCursorMode::StExtension ) goto unknown; dpy = mDpy.lock(); - if ( dpy ) - dpy->setCursorMode( (TerminalCursorMode)mCsiescseq.arg[0] ); + if ( dpy ) { + const auto mode = static_cast( mCsiescseq.arg[0] ); + dpy->setCursorMode( mode == BlinkingBlock + ? blinkingCursorVariant( mDefaultCursorMode ) + : mode ); + } break; default: goto unknown; } break; - case '=': /* Progressive enhancement sequences */ - /* Keyboard protocol ESC[=Nu */ - /* Do nothing for the moment */ + case 'q': /* XTVERSION -- Report terminal name and version */ + if ( mCsiescseq.priv != '>' || mCsiescseq.arg[0] != 0 ) + goto unknown; + len = snprintf( buf, sizeof( buf ), "\033P>|eterm %s\033\\", + EE::Version::getVersionName( false ).c_str() ); + ttywrite( buf, len, 0 ); break; case 't': /* Window manipulation */ switch ( mCsiescseq.arg[0] ) { @@ -2199,16 +2299,12 @@ void TerminalEmulator::csihandle( void ) { goto unknown; } break; - case '?': - /* Private mode queries - ignore or handle appropriately */ - /* For XTQMODKEYS and similar queries, we should either: - 1. Ignore completely (do nothing) - 2. Send a proper response if required */ - break; } } void TerminalEmulator::csidump( void ) { + if ( !terminalDiagnosticsEnabled() ) + return; size_t i; uint c; @@ -2283,7 +2379,7 @@ void TerminalEmulator::strhandle( void ) { setClipboard( dec ); xfree( dec ); } else { - fprintf( stderr, "erresc: invalid base64\n" ); + terminalDiagnostic( "erresc: invalid base64\n" ); } } return; @@ -2299,31 +2395,57 @@ void TerminalEmulator::strhandle( void ) { if ( !strcmp( p, "?" ) ) { osc_color_response( par, osc_table[j].idx, 0 ); } else if ( xsetcolorname( osc_table[j].idx, p ) ) { - fprintf( stderr, "erresc: invalid %s color: %s\n", osc_table[j].str, p ); + terminalDiagnostic( "erresc: invalid %s color: %s\n", osc_table[j].str, p ); } else { tfulldirt(); } return; - case 4: /* color set */ - if ( narg < 3 ) - break; - p = mStrescseq.args[2]; - /* FALLTHROUGH */ - case 104: /* color reset, here p = NULL */ - j = ( narg > 1 ) ? atoi( mStrescseq.args[1] ) : -1; - if ( resetColor( j, p ) ) { - if ( par == 104 && narg <= 1 ) - return; /* color reset without parameter */ - fprintf( stderr, "erresc: invalid color j=%d, p=%s\n", j, - p ? p : "(null)" ); - } else { - /* - * TODO if defaultbg color is changed, borders - * are dirty - */ - redraw(); + case 4: { /* set or query palette colors */ + bool changed = false; + for ( int arg = 1; arg + 1 < narg; arg += 2 ) { + p = mStrescseq.args[arg + 1]; + if ( !String::fromString( j, std::string_view{ mStrescseq.args[arg] } ) || + j < 0 ) { + terminalDiagnostic( "erresc: invalid OSC 4 color index: %s\n", + mStrescseq.args[arg] ); + continue; + } + if ( !strcmp( p, "?" ) ) { + osc_color_response( j, j, 1 ); + } else if ( resetColor( j, p ) ) { + terminalDiagnostic( "erresc: invalid color j=%d, p=%s\n", j, p ); + } else { + changed = true; + } } + if ( changed ) + redraw(); return; + } + case 104: { /* reset palette colors */ + if ( narg <= 1 ) { + loadColors(); + tfulldirt(); + return; + } + bool changed = false; + for ( int arg = 1; arg < narg; ++arg ) { + if ( !String::fromString( j, std::string_view{ mStrescseq.args[arg] } ) || + j < 0 ) { + terminalDiagnostic( "erresc: invalid OSC 104 color index: %s\n", + mStrescseq.args[arg] ); + continue; + } + if ( resetColor( j, nullptr ) ) { + terminalDiagnostic( "erresc: palette color %d not found\n", j ); + } else { + changed = true; + } + } + if ( changed ) + tfulldirt(); + return; + } case 110: /* reset dynamic VT100 text foreground color */ case 111: /* reset dynamic VT100 text background color */ case 112: /* reset dynamic text cursor color */ @@ -2332,7 +2454,7 @@ void TerminalEmulator::strhandle( void ) { if ( ( j = par - 110 ) < 0 || j >= (int)LEN( osc_table ) ) break; /* shouldn't be possible */ if ( resetColor( osc_table[j].idx, NULL ) ) { - fprintf( stderr, "erresc: %s color not found\n", osc_table[j].str ); + terminalDiagnostic( "erresc: %s color not found\n", osc_table[j].str ); } else { tfulldirt(); } @@ -2342,6 +2464,13 @@ void TerminalEmulator::strhandle( void ) { mCurrentWorkingDirectory = URI( mStrescseq.args[1] ).getPath(); return; } + case 8: /* Hyperlink: OSC 8 ; params ; URI ST */ + // Hyperlink metadata is not stored in terminal cells yet. Recognize valid open + // and close markers so applications can emit OSC 8 without producing + // diagnostics. + if ( narg >= 3 ) + return; + break; case 133: { if ( narg > 1 ) { j = ( narg > 1 ) ? mStrescseq.args[1][0] : -1; @@ -2388,10 +2517,8 @@ void TerminalEmulator::strhandle( void ) { return; } -#ifdef EE_DEBUG - logError( "erresc: unknown str " ); + terminalDiagnostic( "erresc: unknown str " ); strdump(); -#endif } void TerminalEmulator::strparse( void ) { @@ -2415,6 +2542,8 @@ void TerminalEmulator::strparse( void ) { } void TerminalEmulator::strdump( void ) { + if ( !terminalDiagnosticsEnabled() ) + return; size_t i; uint c; @@ -2531,7 +2660,7 @@ void TerminalEmulator::tdeftran( char ascii ) { char* p; if ( ( p = strchr( cs, ascii ) ) == NULL ) { - fprintf( stderr, "esc unhandled charset: ESC ( %c\n", ascii ); + terminalDiagnostic( "esc unhandled charset: ESC ( %c\n", ascii ); } else { mTerm.trantbl[mTerm.icharset] = vcs[p - cs]; } @@ -2743,8 +2872,8 @@ int TerminalEmulator::eschandle( uchar ascii ) { strhandle(); break; default: - fprintf( stderr, "erresc: unknown sequence ESC 0x%02X '%c'\n", (uchar)ascii, - isprint( ascii ) ? ascii : '.' ); + terminalDiagnostic( "erresc: unknown sequence ESC 0x%02X '%c'\n", (uchar)ascii, + isprint( ascii ) ? ascii : '.' ); break; } return 1; @@ -2996,7 +3125,7 @@ void TerminalEmulator::tresize( int col, int row ) { bool is_alt = IS_SET( MODE_ALTSCREEN ); if ( col < 1 || row < 1 ) { - fprintf( stderr, "tresize: error resizing to %dx%d\n", col, row ); + terminalDiagnostic( "tresize: error resizing to %dx%d\n", col, row ); return; } @@ -3205,9 +3334,10 @@ void TerminalEmulator::drawregion( ITerminalDisplay& dpy, int x1, int y1, int x2 } void TerminalEmulator::draw() { - // If a synchronized update is in progress, skip the physical render - // if ( mTerm.is_syncing ) - // return; + // DEC private mode 2026 makes the bytes between DECSET and DECRST one presentation unit. + // Parsing continues normally, but no partially cleared/rebuilt frame may reach the UI. + if ( mTerm.is_syncing ) + return; int cx = mTerm.c.x /*, ocx = term.ocx, ocy = term.ocy*/; @@ -3284,28 +3414,47 @@ void TerminalEmulator::osc_color_response( int num, int index, int is_osc4 ) { unsigned char r, g, b; if ( xgetcolor( is_osc4 ? num : index, &r, &g, &b ) ) { - fprintf( stderr, "erresc: failed to fetch %s color %d\n", is_osc4 ? "osc4" : "osc", - is_osc4 ? num : index ); + terminalDiagnostic( "erresc: failed to fetch %s color %d\n", is_osc4 ? "osc4" : "osc", + is_osc4 ? num : index ); return; } n = snprintf( buf, sizeof buf, "\033]%s%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", is_osc4 ? "4;" : "", num, r, r, g, g, b, b ); if ( n < 0 || n >= (int)sizeof( buf ) ) { - fprintf( stderr, "error: %s while printing %s response\n", - n < 0 ? "snprintf failed" : "truncation occurred", is_osc4 ? "osc4" : "osc" ); + terminalDiagnostic( "error: %s while printing %s response\n", + n < 0 ? "snprintf failed" : "truncation occurred", + is_osc4 ? "osc4" : "osc" ); } else { ttywrite( buf, n, 1 ); } } +int TerminalEmulator::colorScheme() { + unsigned char red, green, blue; + if ( xgetcolor( 259, &red, &green, &blue ) ) + return 0; + // The protocol describes the OS preference, which eterm does not store separately. The active + // background is the useful equivalent for applications choosing contrasting colors. + return red * 299 + green * 587 + blue * 114 < 128000 ? 1 : 2; +} + +void TerminalEmulator::reportColorScheme() { + const int scheme = colorScheme(); + if ( !scheme ) + return; + char buf[16]; + const int len = snprintf( buf, sizeof( buf ), "\033[?997;%dn", scheme ); + ttywrite( buf, len, 0 ); +} + void TerminalEmulator::mousereport( const TerminalMouseEventType& type, const Vector2i& pos, const Uint32& flags, const Uint32& mod ) { if ( !xgetmode( (TerminalWinMode)MODE_MOUSE ) && !xgetmode( MODE_MOUSESGR ) && ( TerminalMouseEventType::MouseButtonDown == type || TerminalMouseEventType::MouseButtonRelease == type ) ) { /* If mouse mode is not enabled, we send arrow keys for scroll events */ - if ( type == TerminalMouseEventType::MouseButtonDown && + if ( type == TerminalMouseEventType::MouseButtonDown && xgetmode( MODE_ALTSCRROLL ) && ( flags & ( EE_BUTTON_WUMASK | EE_BUTTON_WDMASK ) ) && tisaltscr() ) { char buf[64]; int len = 0; @@ -3568,7 +3717,6 @@ void TerminalEmulator::resize( int columns, int rows ) { return; } - mTerm.is_syncing = true; tresize( columns, rows ); redraw(); @@ -3588,6 +3736,11 @@ bool TerminalEmulator::update() { _die( "Failed to resize pty!" ); } + redraw(); + } + + // A client that fails to close a synchronized update must not freeze presentation forever. + if ( mTerm.is_syncing && mSynchronizedUpdateClock.getElapsedTime() >= Seconds( 1 ) ) { mTerm.is_syncing = false; redraw(); } diff --git a/src/modules/eterm/src/eterm/terminal/terminalsession.cpp b/src/modules/eterm/src/eterm/terminal/terminalsession.cpp index d09c8e97c..eb270fc62 100644 --- a/src/modules/eterm/src/eterm/terminal/terminalsession.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminalsession.cpp @@ -103,6 +103,8 @@ class TerminalSession::WorkerDisplay final : public ITerminalDisplay { void resetColors() { mPalette = mInitialPalette; + if ( mEmulator ) + mEmulator->notifyColorSchemeChanged(); Event event{ EventType::Color }; event.value = -1; mSession.enqueueEvent( std::move( event ), false ); @@ -144,6 +146,8 @@ class TerminalSession::WorkerDisplay final : public ITerminalDisplay { if ( !parsed ) return 1; setPaletteColor( index, color ); + if ( mEmulator ) + mEmulator->notifyColorSchemeChanged(); Event event{ EventType::Color }; event.data = name ? name : ""; event.value = static_cast( index ); @@ -195,6 +199,8 @@ class TerminalSession::WorkerDisplay final : public ITerminalDisplay { void setPalette( TerminalColorPalette palette ) { mInitialPalette = palette; mPalette = std::move( palette ); + if ( mEmulator ) + mEmulator->notifyColorSchemeChanged(); } void setFocused( bool focused ) { @@ -503,6 +509,7 @@ void TerminalSession::processCommand( Command&& command ) { mWorkerDisplay->setFocused( value.value ); mEmulator->redraw(); } else if constexpr ( std::is_same_v ) { + mEmulator->setDefaultCursorMode( value.mode ); mWorkerDisplay->setCursorMode( value.mode ); mEmulator->redraw(); } else if constexpr ( std::is_same_v ) { @@ -554,4 +561,4 @@ void TerminalSession::processCommand( Command&& command ) { std::move( command ) ); } -}} // namespace eterm::Terminal \ No newline at end of file +}} // namespace eterm::Terminal diff --git a/src/tests/unit_tests/eterm_tests.cpp b/src/tests/unit_tests/eterm_tests.cpp index 270134ef4..0570bec89 100644 --- a/src/tests/unit_tests/eterm_tests.cpp +++ b/src/tests/unit_tests/eterm_tests.cpp @@ -339,18 +339,150 @@ UTEST( eterm_session, concurrent_shutdown_is_idempotent ) { class MockDisplay : public ITerminalDisplay { public: int mDrawLines{ 0 }; + int mDrawEnds{ 0 }; uint32_t mFirstMode{ 0 }; uint32_t mSecondMode{ 0 }; + TerminalGlyph mFirstGlyph; + TerminalGlyph mSecondGlyph; + std::vector mResetColorIndices; + int mResetColorsCount{ 0 }; + Uint32 mBackground{ 0x101010FF }; bool drawBegin( Uint32, Uint32 ) override { return true; } - void drawLine( Line line, int, int, int ) override { + void drawLine( Line line, int, int y, int ) override { ++mDrawLines; - mFirstMode = line[0].mode; - mSecondMode = line[1].mode; + if ( y == 0 ) { + mFirstMode = line[0].mode; + mSecondMode = line[1].mode; + mFirstGlyph = line[0]; + mSecondGlyph = line[1]; + } } void drawCursor( int, int, TerminalGlyph, int, int, TerminalGlyph ) override {} - void drawEnd() override {} + void drawEnd() override { ++mDrawEnds; } + void resetColors() override { ++mResetColorsCount; } + int resetColor( const Uint32& index, const char* ) override { + mResetColorIndices.emplace_back( index ); + return 0; + } + bool getColor( const Uint32& index, unsigned char* r, unsigned char* g, + unsigned char* b ) override { + if ( index == 259 ) { + *r = static_cast( mBackground >> 24 ); + *g = static_cast( mBackground >> 16 ); + *b = static_cast( mBackground >> 8 ); + return true; + } + if ( index > 1 ) + return false; + *r = static_cast( 1 + index * 3 ); + *g = static_cast( 2 + index * 3 ); + *b = static_cast( 3 + index * 3 ); + return true; + } }; +UTEST( eterm, modern_csi_prefixes_do_not_claim_unsupported_keyboard_protocol ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033[?u\033[>7u\033[<1u\033[mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + + EXPECT_TRUE( ptyPtr->mWrites.empty() ); +} + +UTEST( eterm, cursor_style_and_xterm_version_queries ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033[0 q\033[6 q\033[>0q"; + pty->mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + + EXPECT_EQ( TerminalCursorMode::SteadyBar, display->getCursorMode() ); + EXPECT_EQ( static_cast( 0 ), ptyPtr->mWrites.find( "\033P>|eterm " ) ); + ASSERT_TRUE( ptyPtr->mWrites.size() >= 2 ); + EXPECT_STDSTREQ( "\033\\", ptyPtr->mWrites.substr( ptyPtr->mWrites.size() - 2 ) ); +} + +UTEST( eterm, cursor_style_zero_uses_blinking_configured_shape ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033[2 q\033[0 q"; + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + term->setDefaultCursorMode( TerminalCursorMode::SteadyUnderline ); + + term->update(); + + EXPECT_EQ( TerminalCursorMode::BlinkUnderline, display->getCursorMode() ); +} + +UTEST( eterm, osc_hyperlink_markers_are_recognized ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033]8;id=codex;https://example.com/\033\\OK\033]8;;\033\\"; + pty->mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + + EXPECT_EQ( static_cast( 'O' ), display->mFirstGlyph.u ); + EXPECT_EQ( static_cast( 'K' ), display->mSecondGlyph.u ); + EXPECT_TRUE( ptyPtr->mWrites.empty() ); +} + +UTEST( eterm, alternate_scroll_mode_controls_wheel_key_translation ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033[?1049h\033[?1007l"; + pty->mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + term->mousereport( TerminalMouseEventType::MouseButtonDown, { 0, 0 }, EE_BUTTON_WUMASK, 0 ); + EXPECT_TRUE( ptyPtr->mWrites.empty() ); + + ptyPtr->mBuffer += "\033[?1007h"; + term->update(); + term->mousereport( TerminalMouseEventType::MouseButtonDown, { 0, 0 }, EE_BUTTON_WUMASK, 0 ); + EXPECT_STDSTREQ( "\033[A", ptyPtr->mWrites ); +} + +UTEST( eterm, color_scheme_query_and_change_notification ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033[?996n\033[?2031h"; + pty->mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + EXPECT_STDSTREQ( "\033[?997;1n", ptyPtr->mWrites ); + + display->mBackground = 0xF0F0F0FF; + term->notifyColorSchemeChanged(); + EXPECT_STDSTREQ( "\033[?997;1n\033[?997;2n", ptyPtr->mWrites ); + + ptyPtr->mBuffer += "\033[?2031l"; + term->update(); + display->mBackground = 0x101010FF; + term->notifyColorSchemeChanged(); + EXPECT_STDSTREQ( "\033[?997;1n\033[?997;2n", ptyPtr->mWrites ); +} + UTEST( eterm, basic_write ) { auto pty = std::make_unique(); auto process = std::make_unique(); @@ -366,6 +498,93 @@ UTEST( eterm, basic_write ) { EXPECT_STDSTREQ( "ABC", term->getSelection() ); } +UTEST( eterm, synchronized_updates_publish_only_complete_frames ) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + display->mDrawEnds = 0; + const char partialFrame[] = "\033[?2026h\033[2Jpartial"; + term->write( partialFrame, sizeof( partialFrame ) - 1 ); + term->update(); + + EXPECT_EQ( 0, display->mDrawEnds ); + + const char completeFrame[] = "\033[Hcomplete\033[?2026l"; + term->write( completeFrame, sizeof( completeFrame ) - 1 ); + term->update(); + + EXPECT_TRUE( display->mDrawEnds > 0 ); + term->selstart( 0, 0, 0 ); + term->selextend( 7, 0, SEL_REGULAR, false ); + EXPECT_STDSTREQ( "complete", term->getSelection() ); +} + +UTEST( eterm, sgr_colon_subparameters_preserve_groups_and_optional_color_space ) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + const char sequence[] = "\033[58:2::255:192:185;59;1;38:2::12:34:56;48:5:42mX"; + term->write( sequence, sizeof( sequence ) - 1 ); + term->update(); + + EXPECT_TRUE( display->mFirstGlyph.mode & ATTR_BOLD ); + EXPECT_EQ( 0x010C2238u, display->mFirstGlyph.fg ); + EXPECT_EQ( 42u, display->mFirstGlyph.bg ); +} + +UTEST( eterm, invalid_indexed_color_keeps_the_previous_color ) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + const char sequence[] = "\033[31mA\033[38;5;283mB"; + term->write( sequence, sizeof( sequence ) - 1 ); + term->update(); + + EXPECT_EQ( 1u, display->mFirstGlyph.fg ); + EXPECT_EQ( 1u, display->mSecondGlyph.fg ); +} + +UTEST( eterm, osc_palette_queries_return_each_requested_color ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033]4;0;?;1;?\a"; + pty->mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + + EXPECT_STDSTREQ( "\033]4;0;rgb:0101/0202/0303\a\033]4;1;rgb:0404/0505/0606\a", + ptyPtr->mWrites ); +} + +UTEST( eterm, osc_color_resets_reach_palette_and_dynamic_defaults ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033]104;1;2\a\033]110\a\033]111\a\033]112\a\033]104\a"; + pty->mLoopWrites = false; + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + + ASSERT_EQ( static_cast( 5 ), display->mResetColorIndices.size() ); + EXPECT_EQ( 1u, display->mResetColorIndices[0] ); + EXPECT_EQ( 2u, display->mResetColorIndices[1] ); + EXPECT_EQ( 258u, display->mResetColorIndices[2] ); + EXPECT_EQ( 259u, display->mResetColorIndices[3] ); + EXPECT_EQ( 256u, display->mResetColorIndices[4] ); + EXPECT_EQ( 2, display->mResetColorsCount ); +} + UTEST( eterm, selection_redraw_while_idle ) { auto pty = std::make_unique(); auto process = std::make_unique(); From 56de1e1c412230195bfb47965b2a84edc011ea5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Tue, 1 Sep 2026 13:22:53 -0300 Subject: [PATCH 5/5] Migrate eterm to eterm-tabs, no need to keep the old eterm. The new eterm have tabs and splits by default, with no performance penalty. --- premake4.lua | 14 - premake5.lua | 12 - src/tools/eterm/eterm.cpp | 872 ++++++++++++++++------------ src/tools/eterm_tabs/eterm_tabs.cpp | 516 ---------------- 4 files changed, 496 insertions(+), 918 deletions(-) delete mode 100644 src/tools/eterm_tabs/eterm_tabs.cpp diff --git a/premake4.lua b/premake4.lua index 48da164d1..05b45b1ae 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1928,20 +1928,6 @@ 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 e0fe748d2..ebca58aa2 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1958,18 +1958,6 @@ 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/tools/eterm/eterm.cpp b/src/tools/eterm/eterm.cpp index 84f25cd0b..014c4c1dc 100644 --- a/src/tools/eterm/eterm.cpp +++ b/src/tools/eterm/eterm.cpp @@ -1,162 +1,343 @@ #include +#include #include -#include +#include +#include +#include +#include +#include +#include + +#include #include +#include +#include #include -EE::Window::Window* win = NULL; -std::shared_ptr terminal = nullptr; -Clock lastRender; -Clock secondsCounter; -Time frameTime{ Time::Zero }; -bool benchmarkMode{ false }; -bool warnBeforeClose{ false }; -std::string windowStringData; +using namespace EE; +using namespace EE::Graphics; +using namespace EE::Scene; +using namespace EE::System; +using namespace EE::UI; +using namespace EE::UI::Tools; +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 }; +UITabWidgetSplitter* tabSplitter{ nullptr }; +FontTrueType* terminalFont{ nullptr }; +UIIcon* terminalIcon{ nullptr }; +UIMessageBox* closeDialog{ nullptr }; +UIWidget* closeDialogWidget{ nullptr }; +TerminalLaunchConfig terminalConfig; std::map terminalColorSchemes; -bool displayingWarnBeforeClose{ false }; -bool yesPicked{ true }; -bool needsRedraw{ false }; -Rectf yesBtn; -Rectf noBtn; +const TerminalColorScheme* selectedColorScheme{ nullptr }; +Float terminalFontSize{ 12 }; +bool warnBeforeClose{ false }; +bool closeApproved{ false }; +bool benchmarkMode{ false }; +Clock secondsCounter; +SmallVector pendingExitCloseTabs; + +void updateWindowTitle(); + +class TerminalSplitterClient : public UITabWidgetSplitter::Client { + public: + void onTabCreated( UITab* tab, UIWidget* ) override { + if ( terminalIcon ) + tab->setIcon( terminalIcon->createDrawable( PixelDensity::dpToPxI( 12 ) ) ); + } + + void onWidgetFocusChange( UIWidget* ) override { updateWindowTitle(); } +}; + +TerminalSplitterClient splitterClient; + +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 configPath = Sys::getConfigPath( "eterm" ); auto colorSchemes = TerminalColorScheme::loadFromFile( resPath + "colorschemes/terminalcolorschemes.conf" ); - auto colorSchemesPath = configPath + FileSystem::getOSSlash() + "colorschemes"; - if ( FileSystem::isDirectory( colorSchemesPath ) ) { - auto colorSchemesFiles = FileSystem::filesGetInPath( colorSchemesPath ); - for ( auto& file : colorSchemesFiles ) { - auto colorSchemesInFile = TerminalColorScheme::loadFromFile( file ); - std::copy( colorSchemesInFile.begin(), colorSchemesInFile.end(), - std::back_inserter( colorSchemes ) ); + 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 ) - terminalColorSchemes.insert( { colorScheme.getName(), colorScheme } ); + for ( auto& colorScheme : colorSchemes ) { + std::string name = colorScheme.getName(); + terminalColorSchemes.emplace( std::move( name ), std::move( colorScheme ) ); + } } -void inputCallback( InputEvent* event ) { - if ( !terminal || event->Type == InputEvent::EventsSent ) +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 ( tabSplitter ) { + if ( auto* terminal = tabSplitter->getCurWidget() && + tabSplitter->getCurWidget()->isType( UI_TYPE_TERMINAL ) + ? tabSplitter->getCurWidget()->asType() + : nullptr; + terminal && !terminal->getTitle().empty() ) { + title += " - "; + title += terminal->getTitle(); + } + } + if ( benchmarkMode ) { + title += " - "; + title += String::toString( appWindow->getFPS() ); + title += " FPS"; + } + appWindow->setTitle( title ); +} - switch ( event->Type ) { - case InputEvent::MouseMotion: { - terminal->onMouseMove( win->getInput()->getMousePos(), - win->getInput()->getPressTrigger() ); - break; - } - case InputEvent::MouseButtonDown: { - if ( displayingWarnBeforeClose ) { - if ( ( win->getInput()->getPressTrigger() & EE_BUTTON_LMASK ) ) { - if ( yesBtn.contains( win->getInput()->getMousePos().asFloat() ) ) { - win->close(); - } else if ( noBtn.contains( win->getInput()->getMousePos().asFloat() ) ) { - displayingWarnBeforeClose = false; - needsRedraw = true; - } - } - } else { - terminal->onMouseDown( win->getInput()->getMousePos(), - win->getInput()->getPressTrigger() ); -#if EE_PLATFORM == EE_PLATFORM_ANDROID - win->startTextInput(); -#endif - } - break; - } - case InputEvent::MouseButtonUp: { - terminal->onMouseUp( win->getInput()->getMousePos(), - win->getInput()->getReleaseTrigger() ); +bool hasTerminals() { + bool found = false; + if ( tabSplitter ) + tabSplitter->forEachWidgetStoppable( [&found]( UIWidget* ) { + found = true; + return true; + } ); + return found; +} - if ( win->getInput()->getDoubleClickTrigger() ) { - terminal->onMouseDoubleClick( win->getInput()->getMousePos(), - win->getInput()->getDoubleClickTrigger() ); - } +bool hasRunningChildren( UITab* tab ) { + auto* terminal = terminalFromTab( tab ); + return terminal && terminal->getTerm() && + Sys::processHasChildren( terminal->getTerm()->getProcessId() ); +} - break; - } - case InputEvent::Window: { - switch ( event->window.type ) { - case InputEvent::WindowKeyboardFocusLost: - case InputEvent::WindowKeyboardFocusGain: { - terminal->setFocus( win->hasFocus() ); - break; - } - } - break; - } - case InputEvent::KeyUp: { - break; - } - case InputEvent::KeyDown: { - if ( displayingWarnBeforeClose ) { - if ( event->key.keysym.sym == EE::Window::KEY_TAB || - event->key.keysym.sym == EE::Window::KEY_LEFT || - event->key.keysym.sym == EE::Window::KEY_RIGHT ) { - yesPicked = !yesPicked; - needsRedraw = true; - } else if ( event->key.keysym.sym == EE::Window::KEY_Y ) { - win->close(); - } else if ( event->key.keysym.sym == EE::Window::KEY_N ) { - displayingWarnBeforeClose = false; - needsRedraw = true; - } else if ( event->key.keysym.sym == EE::Window::KEY_RETURN || - event->key.keysym.sym == EE::Window::KEY_KP_ENTER ) { - if ( yesPicked ) - win->close(); - else { - displayingWarnBeforeClose = false; - needsRedraw = true; - } - } else if ( event->key.keysym.sym == EE::Window::KEY_ESCAPE ) { - displayingWarnBeforeClose = false; - needsRedraw = true; - } - } else { - terminal->onKeyDown( event->key.keysym.sym, event->key.keysym.unicode, - event->key.keysym.mod, event->key.keysym.scancode ); - } +void closeTab( UITab* tab ) { + if ( !tabSplitter || !tab || !tab->getOwnedWidget() ) + return; + tabSplitter->closeTab( tab->getOwnedWidget()->asType(), + UITabWidget::FocusTabBehavior::Default ); +} -#if EE_PLATFORM == EE_PLATFORM_ANDROID - if ( event->key.keysym.sym == KEY_RETURN || - event->key.keysym.scancode == SCANCODE_RETURN ) { - win->startTextInput(); - } -#endif - break; - } - case InputEvent::TextInput: { - terminal->onTextInput( event->text.text ); - break; - } - case InputEvent::TextEditing: { - terminal->onTextEditing( event->textediting.text, event->textediting.start, - event->textediting.length ); - - break; - } - case InputEvent::VideoExpose: - terminal->setFocus( win->hasFocus() ); - terminal->invalidate(); - break; - case InputEvent::VideoResize: { - terminal->setPosition( { 0, 0 } ); - terminal->setSize( win->getSize().asFloat() ); - break; - } +void queueExitCloseTab( UITab* tab ) { + if ( tab && std::find( pendingExitCloseTabs.begin(), pendingExitCloseTabs.end(), tab ) == + pendingExitCloseTabs.end() ) { + pendingExitCloseTabs.emplace_back( tab ); } } -bool onCloseRequestCallback( EE::Window::Window* ) { - if ( warnBeforeClose && Sys::processHasChildren( terminal->getProcessId() ) ) { - displayingWarnBeforeClose = true; - needsRedraw = true; +void queueExitedTabs() { + if ( !terminalConfig.closeOnExit ) + return; + tabSplitter->forEachTab( []( UITab* tab ) { + auto* terminal = terminalFromTab( tab ); + if ( !terminal || !terminal->getTerm() ) + return; + const auto& session = terminal->getTerm()->getSession(); + auto snapshot = session ? session->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." ); + closeDialogWidget = tab->getOwnedWidget()->asType(); + closeDialog->setTitle( "eterm" ); + closeDialog->on( Event::OnConfirm, []( const Event* ) { + if ( closeDialogWidget && tabSplitter->ownedWidgetExists( closeDialogWidget ) ) + tabSplitter->closeTab( closeDialogWidget, UITabWidget::FocusTabBehavior::Default ); + } ); + closeDialog->on( Event::OnClose, []( const Event* ) { + closeDialog = nullptr; + closeDialogWidget = nullptr; + } ); + closeDialog->center(); + closeDialog->showWhenReady(); +} + +void addTabKeyBindings( UITerminal* terminal, UITab* tab ); + +UITerminal* createTerminal( UITabWidget* target = nullptr ) { + if ( !target && tabSplitter ) { + auto* current = tabSplitter->getCurWidget(); + target = current ? tabSplitter->tabWidgetFromWidget( current ) + : tabSplitter->getFirstTabWidget(); + } + if ( !target ) + return nullptr; + Sizef initialSize{ 16, 16 }; + if ( target->getContainerNode() && + target->getContainerNode()->getPixelsSize() != Sizef::Zero ) { + initialSize = target->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 = tabSplitter->createWidgetInTabWidget( target, terminal, "Terminal" ).first; + addTabKeyBindings( terminal, tab ); + terminal->on( Event::OnTitleChange, [tab, terminal]( const Event* ) { + tab->setText( terminal->getTitle().empty() ? "Terminal" : terminal->getTitle() ); + if ( tabSplitter->getCurWidget() == terminal ) + updateWindowTitle(); + } ); + terminal->getTerm()->pushEventCallback( [tab]( const TerminalDisplay::Event& event ) { + if ( terminalConfig.closeOnExit && event.type == TerminalDisplay::EventType::PROCESS_EXIT ) + queueExitCloseTab( tab ); + } ); + tabSplitter->setCurrentWidget( terminal ); + if ( !terminalConfig.executeInShell.empty() ) + terminal->executeFile( terminalConfig.executeInShell ); + terminal->setFocus(); + updateWindowTitle(); + return terminal; +} + +UITerminal* createTerminalSplit( SplitDirection direction, UITerminal* terminal ) { + auto* source = terminal ? tabSplitter->tabWidgetFromWidget( terminal ) : nullptr; + auto* target = source ? tabSplitter->splitTabWidget( direction, source ) : nullptr; + return target ? createTerminal( target ) : nullptr; +} + +void addTabKeyBindings( UITerminal* terminal, UITab* tab ) { + terminal->setCommand( "create-new-terminal", [] { createTerminal(); } ); + terminal->setCommand( "close-tab", [tab] { requestCloseTab( tab ); } ); + terminal->setCommand( "next-tab", [terminal] { + if ( auto* tabs = tabSplitter->tabWidgetFromWidget( terminal ) ) + tabs->focusNextTab(); + } ); + terminal->setCommand( "previous-tab", [terminal] { + if ( auto* tabs = tabSplitter->tabWidgetFromWidget( terminal ) ) + tabs->focusPreviousTab(); + } ); + terminal->setCommand( "split-right", + [terminal] { createTerminalSplit( SplitDirection::Right, terminal ); } ); + terminal->setCommand( "split-bottom", + [terminal] { createTerminalSplit( SplitDirection::Bottom, terminal ); } ); + terminal->setCommand( "split-left", + [terminal] { createTerminalSplit( SplitDirection::Left, terminal ); } ); + terminal->setCommand( "split-top", + [terminal] { createTerminalSplit( SplitDirection::Top, terminal ); } ); + terminal->setCommand( "switch-to-previous-split", + [terminal] { tabSplitter->switchPreviousSplit( terminal ); } ); + terminal->setCommand( "switch-to-next-split", + [terminal] { tabSplitter->switchNextSplit( terminal ); } ); + 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" ); + terminal->addKeyBinding( { KEY_L, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, + "split-right" ); + terminal->addKeyBinding( { KEY_K, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, + "split-bottom" ); + terminal->addKeyBinding( { KEY_J, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, + "split-left" ); + terminal->addKeyBinding( { KEY_I, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, + "split-top" ); + terminal->addKeyBinding( + { KEY_J, KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() }, + "switch-to-previous-split" ); + terminal->addKeyBinding( + { KEY_L, KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() }, + "switch-to-next-split" ); +} + +bool closeWindow( EE::Window::Window* ) { + if ( closeApproved || !warnBeforeClose ) + return true; + bool running = false; + tabSplitter->forEachTab( [&running]( UITab* tab ) { running |= hasRunningChildren( tab ); } ); + if ( !running ) + return true; + if ( closeDialog ) return false; - } - return true; + 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; + closeDialogWidget = nullptr; + } ); + closeDialog->center(); + closeDialog->showWhenReady(); + return false; } +} // namespace + EE_MAIN_FUNC int main( int argc, char* argv[] ) { #ifdef EE_DEBUG Log::instance()->setLogToStdOut( true ); @@ -173,8 +354,8 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { 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 fallbackFontPathF( parser, "fallback-fontpath", - "Fallback Font path", { "fallback-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 }, @@ -196,9 +377,9 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { args::ValueFlag width( parser, "winwidth", "Window width (in dp)", { "width" }, 1280 ); args::ValueFlag height( parser, "winheight", "Window height (in dp)", { "height" }, 720 ); - args::ValueFlag pixelDensityConf( parser, "pixel-density", - "Set default application pixel density", - { 'd', "pixel-density" } ); + 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" } ); @@ -226,267 +407,206 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { 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& e ) { - std::cerr << e.what() << std::endl; + } catch ( const args::ParseError& error ) { + std::cerr << error.what() << std::endl; std::cerr << parser; return EXIT_FAILURE; - } catch ( args::ValidationError& e ) { - std::cerr << e.what() << std::endl; + } catch ( args::ValidationError& error ) { + std::cerr << error.what() << std::endl; std::cerr << parser; return EXIT_FAILURE; } - SystemFontResolver::setEnabled( true ); + 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 ); - - 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 ); - - if ( listColorSchemes.Get() || colorScheme ) - loadColorSchemes( resPath ); - - if ( listColorSchemes.Get() ) { - std::cout << "Color schemes:\n"; - for ( const auto& tcs : terminalColorSchemes ) - std::cout << "\t" << tcs.first << "\n"; - return EXIT_SUCCESS; + if ( !currentDisplay ) { + std::cerr << "Display not found, exiting" << std::endl; + return EXIT_FAILURE; } - std::unique_ptr systemFontWarmUp; - if ( SystemFontResolver::isEnabled() ) { - systemFontWarmUp = - std::make_unique( [] { SystemFontResolver::instance()->warmUp(); } ); - systemFontWarmUp->launch(); + 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(); + auto remixIconFont = FontTrueType::New( "eterm-remixicon", resourceScope ); + auto noniconsFont = FontTrueType::New( "eterm-nonicons", resourceScope ); + auto codIconFont = FontTrueType::New( "eterm-codicon", resourceScope ); + if ( remixIconFont->loadFromFile( resPath + "fonts/remixicon.ttf" ) && + noniconsFont->loadFromFile( resPath + "fonts/nonicons.ttf" ) && + codIconFont->loadFromFile( resPath + "fonts/codicon.ttf" ) ) { + scene->getUIIconThemeManager()->setCurrentTheme( IconManager::init( + "eterm", remixIconFont.get(), noniconsFont.get(), codIconFont.get() ) ); + terminalIcon = scene->findIcon( "terminal" ); } - - displayManager->enableScreenSaver(); - displayManager->enableMouseFocusClickThrough(); - displayManager->disableBypassCompositor(); - defaultResourceScope().getFontService().setHinting( fontHinting.Get() ); - defaultResourceScope().getFontService().setAntialiasing( fontAntialiasing.Get() ); - - Sizei winSize( width.Get(), height.Get() ); - auto displaySize( currentDisplay->getUsableBounds().getSize() ); - if ( displaySize.getWidth() > 0 && winSize.getWidth() >= displaySize.getWidth() ) - winSize.setWidth( static_cast( displaySize.getWidth() * 0.8 ) ); - - if ( displaySize.getHeight() > 0 && winSize.getHeight() >= displaySize.getHeight() ) - winSize.setHeight( static_cast( displaySize.getHeight() * 0.75 ) ); - - win = Engine::instance()->createWindow( - WindowSettings( winSize.getWidth(), winSize.getHeight(), "eterm", WindowStyle::Default, - WindowBackend::Default, 32, resPath + "icon/eterm.png", - pixelDensityConf ? pixelDensityConf.Get() - : currentDisplay->getPixelDensity() ), - ContextSettings( vsync.Get(), benchmarkModeFlag.Get() ? 0 : maxFPS.Get() ) ); - - if ( win->isOpen() ) { - win->setClearColor( RGB( 0, 0, 0 ) ); - - benchmarkMode = benchmarkModeFlag.Get(); - warnBeforeClose = warnBeforeCloseFlag.Get(); - - FontTrueType* fontMono = nullptr; - if ( fontPath && FileSystem::fileExists( fontPath.Get() ) ) { - FileInfo file( fontPath.Get() ); - fontMono = FontTrueType::New( "monospace" ).get(); - if ( fontMono->loadFromFile( file.getFilepath() ) ) { - FontFamily::loadFromRegular( fontMono ); - } else { - fontMono = nullptr; - } - } - if ( fontMono == nullptr ) { - fontMono = FontTrueType::New( "monospace" ).get(); - fontMono->loadFromFile( resPath + "fonts/DejaVuSansMonoNerdFontComplete.ttf" ); - FontFamily::loadFromRegular( fontMono, "DejaVuSansMono" ); - } - - if ( FileSystem::fileExists( resPath + "fonts/NotoColorEmoji.ttf" ) ) { - FontTrueType::New( "emoji-color" ) - ->loadFromFile( resPath + "fonts/NotoColorEmoji.ttf" ); - } else if ( FileSystem::fileExists( resPath + "fonts/NotoEmoji-Regular.ttf" ) ) { - FontTrueType::New( "emoji-font" ) - ->loadFromFile( resPath + "fonts/NotoEmoji-Regular.ttf" ); - } - - std::string fallbackFontPath( fallbackFontPathF - ? fallbackFontPathF.Get() - : resPath + "fonts/DroidSansFallbackFull.ttf" ); - if ( FileSystem::fileExists( fallbackFontPath ) ) { - FontTrueType* fallbackFont = FontTrueType::New( "fallback-font" ).get(); - if ( fallbackFont->loadFromFile( fallbackFontPath ) ) - defaultResourceScope().getFontService().addFallbackFont( fallbackFont ); - } - - Float realMaxFPS = maxFPS.Get() ? maxFPS.Get() : currentDisplay->getRefreshRate(); - frameTime = benchmarkMode ? Time::Zero : Milliseconds( 1000.f / realMaxFPS ); - - FileInfo file( wd ? wd.Get() : FileSystem::getCurrentWorkingDirectory() ); - terminal = TerminalDisplay::create( - win, fontMono, PixelDensity::dpToPx( fontSize.Get() ), win->getSize().asFloat(), - file.isRegularFile() && file.isExecutable() ? file.getFilepath() : shell.Get(), - shellArgs ? String::split( shellArgs.Get() ) : std::vector(), - file.getDirectoryPath(), historySize.Get(), nullptr, fb.Get(), - !( file.isRegularFile() && file.isExecutable() ) ); - - if ( terminal == nullptr ) { - win->close(); - win->showMessageBox( EE::Window::Window::MessageBoxType::Error, "eterm", - "Operating System not supported." ); - terminal.reset(); - systemFontWarmUp.reset(); - Engine::destroySingleton(); - MemoryManager::showResults(); + 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; } - terminal->setFontHinting( fontHinting.Get() ); - terminal->setFontAntialiasing( fontAntialiasing.Get() ); - - terminal->setAllowMemoryTrimming( true ); - terminal->setCursorMode( cursorStyle.Get() ); - terminal->pushEventCallback( [&closeOnExit]( const TerminalDisplay::Event& event ) { - if ( event.type == TerminalDisplay::EventType::TITLE ) { - windowStringData = event.eventData; - win->setTitle( "eterm - " + windowStringData ); - } else if ( event.type == TerminalDisplay::EventType::PROCESS_EXIT && - closeOnExit.Get() ) { - win->close(); - } - } ); - if ( shell ) - terminal->setKeepAlive( false ); - - if ( colorScheme ) { - auto selColorScheme = terminalColorSchemes.find( colorScheme.Get() ); - if ( selColorScheme != terminalColorSchemes.end() ) - terminal->setColorScheme( selColorScheme->second ); - } - - if ( !executeInShell.Get().empty() ) - terminal->executeFile( executeInShell.Get() ); - - win->startTextInput(); - - win->getInput()->pushCallback( &inputCallback ); - - win->setCloseRequestCallback( - []( EE::Window::Window* win ) -> bool { return onCloseRequestCallback( win ); } ); - - win->runMainLoop( [fontMono] { - bool termNeedsUpdate = false; - win->getInput()->update(); - auto mousePos = win->getInput()->getRelativeMousePos(); - bool mouseOutsideBounds = mousePos.y < 0 || mousePos.y > win->getSize().getHeight(); - - if ( terminal ) - termNeedsUpdate = !terminal->update( !mouseOutsideBounds ); - - if ( ( terminal && ( benchmarkMode || terminal->isDirty() ) && - ( !termNeedsUpdate || lastRender.getElapsedTime() >= frameTime ) ) || - needsRedraw ) { - lastRender.restart(); - win->clear(); - terminal->draw(); - - if ( displayingWarnBeforeClose ) { - Sizef winSize{ win->getSize().asFloat() }; - Sizef buttonSize{ PixelDensity::dpToPx( 100 ), PixelDensity::dpToPx( 32 ) }; - Primitives p; - p.setColor( Color( terminal->getColorScheme().getBackground(), 200 ) ); - p.drawRectangle( { { 0, 0 }, winSize } ); - - Text text( "Are you sure you want to close this window? It is still running a " - "process.", - fontMono ); - - text.draw( ( winSize.getWidth() - text.getLocalBounds().getWidth() ) * 0.5f, - winSize.getHeight() * 0.5f - text.getTextHeight() - - PixelDensity::dpToPx( 32 ) ); - - yesBtn = Rectf{ { ( winSize.getWidth() * 0.5f - buttonSize.getWidth() * 0.5f - - PixelDensity::dpToPx( 75 ) ), - win->getHeight() * 0.5f }, - buttonSize } - .floor(); - - p.setColor( terminal->getColorScheme().getBackground() ); - p.drawRoundedRectangle( yesBtn ); - - noBtn = { Vector2f( yesBtn.getPosition().x + yesBtn.getSize().getWidth(), - yesBtn.getPosition().y ) + - Vector2f( PixelDensity::dpToPx( 50 ), 0 ), - yesBtn.getSize() }; - - p.drawRoundedRectangle( noBtn ); - - Text yes( "Yes", fontMono ); - yes.draw( - eefloor( yesBtn.getPosition().x + - ( yesBtn.getSize().getWidth() - yes.getLocalBounds().getWidth() ) * - 0.5f ), - eefloor( yesBtn.getPosition().y + - ( yesBtn.getSize().getHeight() - yes.getTextHeight() ) * 0.5f ) ); - - Text no( "No", fontMono ); - no.draw( - eeceil( noBtn.getPosition().x + - ( noBtn.getSize().getWidth() - no.getLocalBounds().getWidth() ) * - 0.5f ), - eefloor( noBtn.getPosition().y + - ( noBtn.getSize().getHeight() - no.getTextHeight() ) * 0.5f ) ); - - p.setFillMode( PrimitiveFillMode::DRAW_LINE ); - p.setColor( terminal->getColorScheme().getForeground() ); - p.drawRoundedRectangle( yesBtn ); - p.drawRoundedRectangle( noBtn ); - p.setColor( terminal->getColorScheme().getPaletteIndex( 5 ) ); - p.drawRoundedRectangle( yesPicked ? yesBtn : noBtn ); - } - - win->display(); - - needsRedraw = false; - } else if ( !benchmarkMode && !termNeedsUpdate ) { - win->getInput()->waitEvent( Milliseconds( win->hasFocus() ? 16 : 100 ) ); - } - - if ( benchmarkMode && secondsCounter.getElapsedTime() >= Seconds( 1 ) ) { - win->setTitle( "eterm - " + windowStringData + " - " + - String::toString( win->getFPS() ) + " FPS" ); - secondsCounter.restart(); - } - } ); + FontFamily::loadFromRegular( terminalFont, "DejaVuSansMono" ); } - terminal.reset(); - systemFontWarmUp.reset(); + 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 ) ); + } - Engine::destroySingleton(); + 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() ); - MemoryManager::showResults(); + mainLayout = UILinearLayout::NewVertical(); + mainLayout->setParent( scene->getRoot() ); + mainLayout->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); + mainLayout->setPixelsSize( appWindow->getSize().asFloat() ); + tabSplitter = UITabWidgetSplitter::New( &splitterClient, scene ); + tabSplitter->setHideTabBarOnSingleTab( true ); + tabSplitter->setTabTryCloseCallback( + []( UIWidget* widget, UITabWidget::FocusTabBehavior, std::function ) { + if ( warnBeforeClose ) { + auto* tab = tabSplitter->getTabFromWidget( widget ); + if ( hasRunningChildren( tab ) ) { + requestCloseTab( tab ); + return false; + } + } + return true; + } ); + tabSplitter->setOnTabWidgetCreateCb( []( UITabWidget* tabs ) { + 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 ( closeDialogWidget == closedTab->getOwnedWidget() ) + closeDialogWidget = nullptr; + if ( !hasTerminals() ) + appWindow->close(); + else + updateWindowTitle(); + } ); + } ); + auto* tabs = tabSplitter->createTabWidget( mainLayout ); + if ( !tabs ) { + std::cerr << "Could not create terminal tab widget" << std::endl; + return EXIT_FAILURE; + } + mainLayout->updateLayout(); + + for ( size_t tab = 0; tab < eemax( static_cast( 1 ), initialTabs.Get() ); ++tab ) { + if ( !createTerminal( tabs ) ) { + 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; } diff --git a/src/tools/eterm_tabs/eterm_tabs.cpp b/src/tools/eterm_tabs/eterm_tabs.cpp deleted file mode 100644 index a30aeb9fb..000000000 --- a/src/tools/eterm_tabs/eterm_tabs.cpp +++ /dev/null @@ -1,516 +0,0 @@ -#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& session = terminal->getTerm()->getSession(); - auto snapshot = session ? session->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; -}