diff --git a/include/eepp/system/sys.hpp b/include/eepp/system/sys.hpp index ef1e0f9fc..b94d24fda 100644 --- a/include/eepp/system/sys.hpp +++ b/include/eepp/system/sys.hpp @@ -124,6 +124,12 @@ class EE_API Sys { /** @return The process environment variables */ static std::unordered_map getEnvironmentVariables(); + + /** @return The process ids found with the correspoding process / binary / executable name */ + static std::vector pidof( const std::string& processName ); + + /** @returns The unix timestamp of the process creation time */ + static Uint64 getProcessCreationTime( Uint64 pid ); }; }} // namespace EE::System diff --git a/include/eepp/ui/uihelper.hpp b/include/eepp/ui/uihelper.hpp index a47602470..d231c39c3 100644 --- a/include/eepp/ui/uihelper.hpp +++ b/include/eepp/ui/uihelper.hpp @@ -105,7 +105,7 @@ enum UINodeType { UI_TYPE_STACK_LAYOUT, UI_TYPE_MODULES = 10000, UI_TYPE_TERMINAL = 10001, - UI_TYPE_USER = 100000 + UI_TYPE_USER = 200000 }; enum class ScrollBarMode : Uint32 { Auto, AlwaysOn, AlwaysOff }; diff --git a/projects/linux/ee.creator.user b/projects/linux/ee.creator.user index 0b9e6c370..afdebe4f2 100644 --- a/projects/linux/ee.creator.user +++ b/projects/linux/ee.creator.user @@ -1,6 +1,6 @@ - + EnvironmentId diff --git a/src/eepp/system/sys.cpp b/src/eepp/system/sys.cpp index 414b87ef9..cac0b54ff 100644 --- a/src/eepp/system/sys.cpp +++ b/src/eepp/system/sys.cpp @@ -1,11 +1,14 @@ #include #include #include +#include #include #include +#include #include #include #include +#include #include #include @@ -15,8 +18,10 @@ #endif #if defined( EE_PLATFORM_POSIX ) +#include #include #include +#include #endif #if EE_PLATFORM == EE_PLATFORM_MACOS @@ -31,12 +36,24 @@ #include #undef GetDiskFreeSpace #undef GetTempPath + +// clang-format off +#include +#include +// clang-format on + +// Dynamically load PSAPI functions for Windows +typedef BOOL( WINAPI* EnumProcesses_t )( DWORD*, DWORD, DWORD* ); +typedef BOOL( WINAPI* EnumProcessModules_t )( HANDLE, HMODULE*, DWORD, LPDWORD ); +typedef DWORD( WINAPI* GetModuleBaseName_t )( HANDLE, HMODULE, LPSTR, DWORD ); + #elif EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_ANDROID #include #include -#include +#include #elif EE_PLATFORM == EE_PLATFORM_HAIKU #include +#include #include #include #include @@ -47,10 +64,13 @@ #elif EE_PLATFORM == EE_PLATFORM_SOLARIS #include #elif EE_PLATFORM == EE_PLATFORM_BSD -#include +#include +#include +#include #endif #if EE_PLATFORM == EE_PLATFORM_MACOS || EE_PLATFORM == EE_PLATFORM_IOS +#include #include #include #endif @@ -1268,7 +1288,9 @@ std::string Sys::getProcessFilePath() { #if EE_PLATFORM == EE_PLATFORM_WIN std::wstring exename( _MAX_DIR, 0 ); - GetModuleFileNameW( 0, &exename[0], _MAX_PATH ); + DWORD size = GetModuleFileNameW( 0, &exename[0], _MAX_PATH ); + if ( size > 0 && size < _MAX_PATH ) + exename.resize( size ); // Resize to actual size without extra null characters return String( exename ).toUtf8(); #elif EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_ANDROID char path[] = "/proc/self/exe"; @@ -1300,4 +1322,243 @@ std::string Sys::getProcessFilePath() { #endif } +Uint64 Sys::getProcessCreationTime( Uint64 pid ) { + Uint64 creationTime = 0; + +#if EE_PLATFORM == EE_PLATFORM_WIN + int rpid = static_cast( pid ); + HANDLE hProcess = OpenProcess( PROCESS_QUERY_INFORMATION, FALSE, rpid ); + if ( hProcess == NULL ) { + return -1; + } + + FILETIME creationFileTime, exitFileTime, kernelFileTime, userFileTime; + if ( GetProcessTimes( hProcess, &creationFileTime, &exitFileTime, &kernelFileTime, + &userFileTime ) ) { + ULARGE_INTEGER ull; + ull.LowPart = creationFileTime.dwLowDateTime; + ull.HighPart = creationFileTime.dwHighDateTime; + + // Convert from Windows file time to Unix timestamp + creationTime = + static_cast( ( ull.QuadPart - 116444736000000000ULL ) / 10000000ULL ); + } else { + creationTime = -1; + } + + CloseHandle( hProcess ); + +#elif EE_PLATFORM == EE_PLATFORM_LINUX + std::ifstream statFile( "/proc/" + std::to_string( pid ) + "/stat" ); + if ( !statFile.is_open() ) { + return -1; + } + + std::string token; + long startTimeTicks = 0; + int field = 1; + while ( statFile >> token ) { + if ( field == 22 ) { // The 22nd field is the start time in clock ticks + startTimeTicks = std::stol( token ); + break; + } + field++; + } + + struct sysinfo sysInfo; + sysinfo( &sysInfo ); + long uptime = sysInfo.uptime; + + long clockTicksPerSecond = sysconf( _SC_CLK_TCK ); + creationTime = time( NULL ) - uptime + ( startTimeTicks / clockTicksPerSecond ); + + statFile.close(); + +#elif EE_PLATFORM == EE_PLATFORM_MACOS + struct proc_bsdinfo procInfo; + int rpid = static_cast( pid ); + int status = proc_pidinfo( rpid, PROC_PIDTBSDINFO, 0, &procInfo, sizeof( procInfo ) ); + if ( status <= 0 ) { + return -1; + } + + creationTime = procInfo.pbi_start_tvsec; + +#elif EE_PLATFORM == EE_PLATFORM_BSD + struct kinfo_proc proc; + int rpid = static_cast( pid ); + size_t procLen = sizeof( proc ); + int mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, rpid }; + + if ( sysctl( mib, 4, &proc, &procLen, NULL, 0 ) < 0 ) { + return -1; + } + + creationTime = proc.ki_start.tv_sec; + +#elif EE_PLATFORM == EE_PLATFORM_HAIKU + thread_info threadInfo; + int rpid = static_cast( pid ); + status_t result = get_thread_info( rpid, &threadInfo ); // Get thread info for the PID passed + if ( result == B_OK ) { + // Approximate creation time by subtracting CPU time (user_time + kernel_time) from current + // time + creationTime = time( NULL ) - ( threadInfo.user_time + threadInfo.kernel_time ) / + 1000000; // Convert microseconds to seconds + } else { + return -1; + } + +#endif + + return creationTime; +} + +std::vector Sys::pidof( const std::string& processName ) { +#if EE_PLATFORM == EE_PLATFORM_WIN + std::vector pids; + std::vector extensions = getEnvSplitted( "PATHEXT" ); + + HMODULE hPsapi = LoadLibrary( TEXT( "psapi.dll" ) ); + if ( !hPsapi ) + return pids; + + EnumProcesses_t EnumProcesses = (EnumProcesses_t)GetProcAddress( hPsapi, "EnumProcesses" ); + EnumProcessModules_t EnumProcessModules = + (EnumProcessModules_t)GetProcAddress( hPsapi, "EnumProcessModules" ); + GetModuleBaseName_t GetModuleBaseName = + (GetModuleBaseName_t)GetProcAddress( hPsapi, "GetModuleBaseNameA" ); + + if ( !EnumProcesses || !EnumProcessModules || !GetModuleBaseName ) { + FreeLibrary( hPsapi ); + eePRINTL( "EnumProcesses or EnumProcessModules or GetModuleBaseName failed" ); + return pids; + } + + DWORD processIds[1024], cbNeeded; + if ( !EnumProcesses( processIds, sizeof( processIds ), &cbNeeded ) ) { + FreeLibrary( hPsapi ); + eePRINTL( "EnumProcesses failed" ); + return pids; + } + + DWORD numProcesses = cbNeeded / sizeof( DWORD ); + + for ( DWORD i = 0; i < numProcesses; ++i ) { + if ( processIds[i] == 0 ) + continue; + + HANDLE hProcess = + OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processIds[i] ); + if ( hProcess ) { + HMODULE hMod; + DWORD cbNeededMod; + if ( EnumProcessModules( hProcess, &hMod, sizeof( hMod ), &cbNeededMod ) ) { + char szProcessName[MAX_PATH]; + if ( GetModuleBaseName( hProcess, hMod, szProcessName, + sizeof( szProcessName ) / sizeof( char ) ) ) { + std::string actualName( szProcessName, std::strlen( szProcessName ) ); + + // Check if the process name matches the input name with or without extensions + if ( actualName == processName ) { + pids.push_back( processIds[i] ); + } else { + for ( const auto& ext : extensions ) { + std::string extName = processName + ext; + if ( actualName == extName ) { + pids.push_back( processIds[i] ); + break; + } + } + } + } + } + CloseHandle( hProcess ); + } + } + + for ( auto pid : pids ) + eePRINTL( "Found pid %d", pid ); + + FreeLibrary( hPsapi ); + return pids; +#elif EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_ANDROID || \ + EE_PLATFORM == EE_PLATFORM_MACOS + std::vector pids; + DIR* dir = opendir( "/proc" ); + if ( !dir ) { + return pids; + } + + struct dirent* entry; + while ( ( entry = readdir( dir ) ) != NULL ) { + if ( entry->d_type == DT_DIR && isdigit( entry->d_name[0] ) ) { + std::string pidDir = "/proc/" + std::string( entry->d_name ); + std::string cmdPath = pidDir + "/comm"; + FILE* cmdFile = fopen( cmdPath.c_str(), "r" ); + if ( cmdFile ) { + char cmdline[256]; + if ( fgets( cmdline, sizeof( cmdline ), cmdFile ) != NULL ) { + cmdline[strcspn( cmdline, "\n" )] = 0; // Remove newline + if ( processName == cmdline ) { + pids.push_back( atoi( entry->d_name ) ); + } + } + fclose( cmdFile ); + } + } + } + + closedir( dir ); + return pids; +#elif EE_PLATFORM == EE_PLATFORM_BSD + std::vector pids; + + int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0 }; + size_t len; + + if ( sysctl( mib, 4, NULL, &len, NULL, 0 ) == -1 ) { + return pids; + } + + struct kinfo_proc* procs = (struct kinfo_proc*)malloc( len ); + if ( !procs ) { + return pids; + } + + if ( sysctl( mib, 4, procs, &len, NULL, 0 ) == -1 ) { + free( procs ); + return pids; + } + + int proc_count = len / sizeof( struct kinfo_proc ); + + for ( int i = 0; i < proc_count; i++ ) { + std::string name( procs[i].ki_comm ); + if ( name == processName ) { + pids.push_back( procs[i].ki_pid ); + } + } + + free( procs ); + return pids; +#elif EE_PLATFORM == EE_PLATFORM_HAIKU + std::vector pids; + team_info teamInfo; + int32 cookie = 0; + std::string lProcessName = String::toLower( processName ); + + while ( get_next_team_info( &cookie, &teamInfo ) == B_OK ) { + if ( lProcessName == String::toLower( FileSystem::fileNameFromPath( teamInfo.args ) ) ) { + pids.push_back( teamInfo.team ); + } + } + + return pids; +#else +#warning Platform not supported + return {}; +#endif +} + }} // namespace EE::System diff --git a/src/tools/ecode/appconfig.cpp b/src/tools/ecode/appconfig.cpp index 139f35648..ddd291047 100644 --- a/src/tools/ecode/appconfig.cpp +++ b/src/tools/ecode/appconfig.cpp @@ -117,6 +117,7 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath, ui.showStatusBar = ini.getValueB( "ui", "show_status_bar", true ); ui.showMenuBar = ini.getValueB( "ui", "show_menu_bar", false ); ui.welcomeScreen = ini.getValueB( "ui", "welcome_screen", true ); + ui.singleInstance = ini.getValueB( "ui", "single_instance", true ); ui.panelPosition = panelPositionFromString( ini.getValue( "ui", "panel_position", "left" ) ); ui.serifFont = ini.getValue( "ui", "serif_font", "fonts/NotoSans-Regular.ttf" ); ui.monospaceFont = ini.getValue( "ui", "monospace_font", "fonts/DejaVuSansMono.ttf" ); @@ -274,6 +275,7 @@ void AppConfig::save( const std::vector& recentFiles, ini.setValueB( "ui", "show_status_bar", ui.showStatusBar ); ini.setValueB( "ui", "show_menu_bar", ui.showMenuBar ); ini.setValueB( "ui", "welcome_screen", ui.welcomeScreen ); + ini.setValueB( "ui", "single_instance", ui.singleInstance ); ini.setValue( "ui", "panel_position", panelPositionToString( ui.panelPosition ) ); ini.setValue( "ui", "serif_font", ui.serifFont ); ini.setValue( "ui", "monospace_font", ui.monospaceFont ); diff --git a/src/tools/ecode/appconfig.hpp b/src/tools/ecode/appconfig.hpp index ee3f5f52d..5774ca87f 100644 --- a/src/tools/ecode/appconfig.hpp +++ b/src/tools/ecode/appconfig.hpp @@ -33,6 +33,7 @@ struct UIConfig { bool showStatusBar{ true }; bool showMenuBar{ false }; bool welcomeScreen{ true }; + bool singleInstance{ true }; PanelPosition panelPosition{ PanelPosition::Left }; std::string serifFont; std::string monospaceFont; diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index fd4192ccb..bfe03c873 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -529,6 +529,7 @@ bool App::loadConfig( const LogLevel& logLevel, const Sizeu& displaySize, bool s mThemesPath = mConfigPath + "themes"; mScriptsPath = mConfigPath + "scripts"; mPlaygroundPath = mConfigPath + "playground"; + mIpcPath = mConfigPath + "ipc"; mColorSchemesPath = mConfigPath + "editor" + FileSystem::getOSSlash() + "colorschemes" + FileSystem::getOSSlash(); mTerminalManager = std::make_unique( this ); @@ -557,6 +558,16 @@ bool App::loadConfig( const LogLevel& logLevel, const Sizeu& displaySize, bool s FileSystem::makeDir( mPlaygroundPath ); FileSystem::dirAddSlashAtEnd( mPlaygroundPath ); + if ( !FileSystem::fileExists( mIpcPath ) ) + FileSystem::makeDir( mIpcPath ); + FileSystem::dirAddSlashAtEnd( mIpcPath ); + + Uint64 pid = Sys::getProcessID(); + mPidPath = mIpcPath + String::toString( pid ); + FileSystem::dirAddSlashAtEnd( mPidPath ); + if ( !FileSystem::fileExists( mPidPath ) ) + FileSystem::makeDir( mPidPath ); + mLogsPath = mConfigPath + "ecode.log"; #ifndef EE_DEBUG @@ -754,6 +765,14 @@ App::App( const size_t& jobs, const std::vector& args ) : mSettingsActions( std::make_unique( this ) ) { } +static void fsRemoveAll( const std::string& fpath ) { +#if EE_PLATFORM == EE_PLATFORM_WIN + fs::remove_all( std::filesystem::path( String( fpath ).toWideString() ) ); +#else + fs::remove_all( fpath ); +#endif +} + App::~App() { if ( mProjectBuildManager ) mProjectBuildManager.reset(); @@ -772,10 +791,26 @@ App::~App() { eeSAFE_DELETE( mSplitter ); if ( mFileSystemListener ) { + if ( mIpcListenerId ) + mFileSystemListener->removeListener( mIpcListenerId ); delete mFileSystemListener; mFileSystemListener = nullptr; } mDirTree.reset(); + + fsRemoveAll( mPidPath ); +} + +void App::updateRecentButtons() { + updateOpenRecentFolderBtn(); + + if ( mSplitter ) { + mSplitter->forEachWidgetType( + static_cast( CustomWidgets::UI_TYPE_WELCOME_TAB ), []( UIWidget* widget ) { + UIWelcomeScreen* welcomeTab = static_cast( widget ); + welcomeTab->refresh(); + } ); + } } void App::updateRecentFiles() { @@ -807,6 +842,7 @@ void App::updateRecentFiles() { } else if ( id == "clear-menu" ) { mRecentFiles.clear(); updateRecentFiles(); + updateRecentButtons(); } else { const String& txt = event->getNode()->asType()->getText(); std::string path( txt.toUtf8() ); @@ -847,6 +883,7 @@ void App::updateRecentFolders() { if ( id == "clear-menu" ) { mRecentFolders.clear(); updateRecentFolders(); + updateRecentButtons(); } else if ( id == "restore-last-session-at-startup" ) { mConfig.workspace.restoreLastSession = event->getNode()->asType()->isActive(); @@ -1575,51 +1612,50 @@ std::map App::getDefaultKeybindings() { std::map App::getLocalKeybindings() { return { { { KEY_RETURN, KEYMOD_LALT | KEYMOD_LCTRL }, "fullscreen-toggle" }, - { { KEY_F3, KEYMOD_NONE }, "repeat-find" }, { { KEY_F3, KEYMOD_SHIFT }, "find-prev" }, - { { KEY_F12, KEYMOD_NONE }, "console-toggle" }, - { { KEY_F, KeyMod::getDefaultModifier() }, "find-replace" }, - { { KEY_Q, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "close-app" }, - { { KEY_O, KeyMod::getDefaultModifier() }, "open-file" }, - { { KEY_W, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "download-file-web" }, - { { KEY_O, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-folder" }, - { { KEY_F11, KEYMOD_NONE }, "debug-widget-tree-view" }, - { { KEY_K, KeyMod::getDefaultModifier() }, "open-locatebar" }, - { { KEY_P, KeyMod::getDefaultModifier() }, "open-command-palette" }, - { { KEY_F, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-global-search" }, - { { KEY_L, KeyMod::getDefaultModifier() }, "go-to-line" }, + { { KEY_F3, KEYMOD_NONE }, "repeat-find" }, + { { KEY_F3, KEYMOD_SHIFT }, "find-prev" }, + { { KEY_F12, KEYMOD_NONE }, "console-toggle" }, + { { KEY_F, KeyMod::getDefaultModifier() }, "find-replace" }, + { { KEY_Q, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "close-app" }, + { { KEY_O, KeyMod::getDefaultModifier() }, "open-file" }, + { { KEY_W, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "download-file-web" }, + { { KEY_O, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-folder" }, + { { KEY_F11, KEYMOD_NONE }, "debug-widget-tree-view" }, + { { KEY_K, KeyMod::getDefaultModifier() }, "open-locatebar" }, + { { KEY_P, KeyMod::getDefaultModifier() }, "open-command-palette" }, + { { KEY_F, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-global-search" }, + { { KEY_L, KeyMod::getDefaultModifier() }, "go-to-line" }, #if EE_PLATFORM == EE_PLATFORM_MACOS - { { KEY_M, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "menu-toggle" }, + { { KEY_M, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "menu-toggle" }, #else - { { KEY_M, KeyMod::getDefaultModifier() }, "menu-toggle" }, + { { KEY_M, KeyMod::getDefaultModifier() }, "menu-toggle" }, #endif - { { KEY_S, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "save-all" }, - { { KEY_F9, KEYMOD_LALT }, "switch-side-panel" }, - { { KEY_J, KeyMod::getDefaultModifier() | KEYMOD_LALT | KEYMOD_SHIFT }, - "terminal-split-left" }, - { { KEY_L, KeyMod::getDefaultModifier() | KEYMOD_LALT | KEYMOD_SHIFT }, - "terminal-split-right" }, - { { KEY_I, KeyMod::getDefaultModifier() | KEYMOD_LALT | KEYMOD_SHIFT }, - "terminal-split-top" }, - { { KEY_K, KeyMod::getDefaultModifier() | KEYMOD_LALT | KEYMOD_SHIFT }, - "terminal-split-bottom" }, - { { KEY_S, KeyMod::getDefaultModifier() | KEYMOD_LALT | KEYMOD_SHIFT }, - "terminal-split-swap" }, - { { KEY_T, KeyMod::getDefaultModifier() | KEYMOD_LALT | KEYMOD_SHIFT }, - "reopen-closed-tab" }, - { { KEY_1, KEYMOD_LALT }, "toggle-status-locate-bar" }, - { { KEY_2, KEYMOD_LALT }, "toggle-status-global-search-bar" }, - { { KEY_3, KEYMOD_LALT }, "toggle-status-terminal" }, - { { KEY_4, KEYMOD_LALT }, "toggle-status-build-output" }, - { { KEY_5, KEYMOD_LALT }, "toggle-status-app-output" }, - { { KEY_B, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "project-build-start" }, - { { KEY_C, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "project-build-cancel" }, - { { KEY_F5, KEYMOD_NONE }, "project-build-and-run" }, - { { KEY_O, KEYMOD_LALT | KEYMOD_SHIFT }, "show-open-documents" }, - { { KEY_K, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, - "open-workspace-symbol-search" }, - { { KEY_P, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, - "open-document-symbol-search" }, - { { KEY_N, KEYMOD_SHIFT | KEYMOD_LALT }, "create-new-window" }, + { { KEY_S, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "save-all" }, + { { KEY_F9, KEYMOD_LALT }, "switch-side-panel" }, + { { KEY_J, KeyMod::getDefaultModifier() | KEYMOD_LALT | KEYMOD_SHIFT }, + "terminal-split-left" }, + { { KEY_L, KeyMod::getDefaultModifier() | KEYMOD_LALT | KEYMOD_SHIFT }, + "terminal-split-right" }, + { { KEY_I, KeyMod::getDefaultModifier() | KEYMOD_LALT | KEYMOD_SHIFT }, + "terminal-split-top" }, + { { KEY_K, KeyMod::getDefaultModifier() | KEYMOD_LALT | KEYMOD_SHIFT }, + "terminal-split-bottom" }, + { { KEY_S, KeyMod::getDefaultModifier() | KEYMOD_LALT | KEYMOD_SHIFT }, + "terminal-split-swap" }, + { { KEY_T, KeyMod::getDefaultModifier() | KEYMOD_LALT | KEYMOD_SHIFT }, + "reopen-closed-tab" }, + { { KEY_1, KEYMOD_LALT }, "toggle-status-locate-bar" }, + { { KEY_2, KEYMOD_LALT }, "toggle-status-global-search-bar" }, + { { KEY_3, KEYMOD_LALT }, "toggle-status-terminal" }, + { { KEY_4, KEYMOD_LALT }, "toggle-status-build-output" }, + { { KEY_5, KEYMOD_LALT }, "toggle-status-app-output" }, + { { KEY_B, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "project-build-start" }, + { { KEY_C, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "project-build-cancel" }, + { { KEY_F5, KEYMOD_NONE }, "project-build-and-run" }, + { { KEY_O, KEYMOD_LALT | KEYMOD_SHIFT }, "show-open-documents" }, + { { KEY_K, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-workspace-symbol-search" }, + { { KEY_P, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-document-symbol-search" }, + { { KEY_N, KEYMOD_SHIFT | KEYMOD_LALT }, "create-new-window" }, }; } @@ -1628,15 +1664,15 @@ std::map App::getLocalKeybindings() { std::map App::getMigrateKeybindings() { return { { "fullscreen-toggle", "alt+return" }, { "switch-to-tab-1", "alt+1" }, - { "switch-to-tab-2", "alt+2" }, { "switch-to-tab-3", "alt+3" }, - { "switch-to-tab-4", "alt+4" }, { "switch-to-tab-5", "alt+5" }, - { "switch-to-tab-6", "alt+6" }, { "switch-to-tab-7", "alt+7" }, - { "switch-to-tab-8", "alt+8" }, { "switch-to-tab-9", "alt+9" }, - { "switch-to-last-tab", "alt+0" }, + { "switch-to-tab-2", "alt+2" }, { "switch-to-tab-3", "alt+3" }, + { "switch-to-tab-4", "alt+4" }, { "switch-to-tab-5", "alt+5" }, + { "switch-to-tab-6", "alt+6" }, { "switch-to-tab-7", "alt+7" }, + { "switch-to-tab-8", "alt+8" }, { "switch-to-tab-9", "alt+9" }, + { "switch-to-last-tab", "alt+0" }, #if EE_PLATFORM == EE_PLATFORM_MACOS - { "menu-toggle", "mod+shift+m" }, + { "menu-toggle", "mod+shift+m" }, #endif - { "lock-toggle", "mod+shift+l" }, + { "lock-toggle", "mod+shift+l" }, }; } @@ -2657,6 +2693,20 @@ void App::consoleToggle() { mSplitter->getCurWidget()->setFocus(); } +std::function +App::getForcePositionFn( TextPosition initialPosition ) { + std::function forcePosition; + if ( initialPosition.isValid() ) { + forcePosition = [this, initialPosition]( UICodeEditor* editor, const auto& ) { + editor->runOnMainThread( [this, initialPosition, editor] { + editor->goToLine( initialPosition ); + mSplitter->addEditorPositionToNavigationHistory( editor ); + } ); + }; + } + return forcePosition; +} + void App::initProjectTreeView( std::string path, bool openClean ) { mProjectViewEmptyCont = mUISceneNode->find( "project_view_empty" ); mProjectViewEmptyCont->find( "open_folder" ) @@ -2771,16 +2821,7 @@ void App::initProjectTreeView( std::string path, bool openClean ) { if ( mFileSystemListener ) mFileSystemListener->setFileSystemModel( mFileSystemModel ); - std::function - forcePosition; - if ( initialPosition.isValid() ) { - forcePosition = [this, initialPosition]( UICodeEditor* editor, const auto& ) { - editor->runOnMainThread( [this, initialPosition, editor] { - editor->goToLine( initialPosition ); - mSplitter->addEditorPositionToNavigationHistory( editor ); - } ); - }; - } + auto forcePosition = getForcePositionFn( initialPosition ); if ( FileSystem::fileExists( rpath ) ) { loadFileFromPath( rpath, false, nullptr, forcePosition ); @@ -3040,6 +3081,56 @@ FontTrueType* App::loadFont( const std::string& name, std::string fontPath, return nullptr; } +bool App::needsRedirectToRunningProcess( std::string file ) { + if ( !mConfig.ui.singleInstance || file.empty() ) + return false; + + bool hasPosition = pathHasPosition( file ); + TextPosition position; + if ( hasPosition ) { + auto pathAndPosition = getPathAndPosition( file ); + file = pathAndPosition.first; + position = pathAndPosition.second; + } + + std::string rpath( FileSystem::getRealPath( file ) ); + FileInfo finfo( rpath ); + + if ( !finfo.exists() || finfo.isDirectory() ) + return false; + + std::string processName( FileSystem::fileNameFromPath( Sys::getProcessFilePath() ) ); + auto pids = Sys::pidof( processName ); + if ( pids.size() <= 1 ) + return false; + + Uint64 processPid = Sys::getProcessID(); + Uint64 latestPid = processPid; + Uint64 lastCreationTime = 0; + + for ( const auto pid : pids ) { + if ( pid != Sys::getProcessID() ) { + Uint64 creationTime = Sys::getProcessCreationTime( pid ); + if ( creationTime >= lastCreationTime ) { + latestPid = pid; + lastCreationTime = creationTime; + } + } + } + + if ( latestPid == processPid ) + return false; + + std::string pidPath = mIpcPath + String::toString( latestPid ); + if ( !FileSystem::isDirectory( pidPath ) ) + return false; + FileSystem::dirAddSlashAtEnd( pidPath ); + FileSystem::fileWrite( pidPath + MD5::fromString( finfo.getFilepath() ).toHexString(), + finfo.getFilepath() + + ( position.isValid() ? position.toPositionString() : "" ) ); + return true; +} + void App::init( const LogLevel& logLevel, std::string file, const Float& pidelDensity, const std::string& colorScheme, bool terminal, bool frameBuffer, bool benchmarkMode, const std::string& css, bool health, const std::string& healthLang, @@ -3073,6 +3164,9 @@ void App::init( const LogLevel& logLevel, std::string file, const Float& pidelDe return; } + if ( needsRedirectToRunningProcess( file ) ) + return; + currentDisplay = displayManager->getDisplayIndex( mConfig.windowState.displayIndex < displayManager->getDisplayCount() ? mConfig.windowState.displayIndex @@ -3437,8 +3531,37 @@ void App::init( const LogLevel& logLevel, std::string file, const Float& pidelDe mFileWatcher = new efsw::FileWatcher(); mFileSystemListener = new FileSystemListener( mSplitter, mFileSystemModel, { mLogsPath } ); mFileWatcher->addWatch( mPluginsPath, mFileSystemListener ); + mFileWatcher->addWatch( mPidPath, mFileSystemListener ); mFileWatcher->watch(); mPluginManager->setFileSystemListener( mFileSystemListener ); + mIpcListenerId = mFileSystemListener->addListener( [this]( const FileEvent& fe, + const FileInfo& fi ) { + if ( !( ( fe.type == FileSystemEventType::Add || + fe.type == FileSystemEventType::Modified ) && + fe.directory == mPidPath ) ) + return; + std::string path; + FileSystem::fileGet( fi.getFilepath(), path ); + String::trimInPlace( path, ' ' ); + String::trimInPlace( path, '\n' ); + + bool hasPosition = pathHasPosition( path ); + TextPosition initialPosition; + if ( hasPosition ) { + auto pathAndPosition = getPathAndPosition( path ); + path = pathAndPosition.first; + initialPosition = pathAndPosition.second; + } + + if ( FileSystem::fileExists( path ) ) { + mUISceneNode->runOnMainThread( [path, initialPosition, this] { + loadFileFromPath( path, true, nullptr, getForcePositionFn( initialPosition ) ); + } ); + if ( !mWindow->hasFocus() ) + mWindow->raise(); + } + FileSystem::fileRemove( fi.getFilepath() ); + } ); #endif mNotificationCenter = std::make_unique( diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index 00ff9061f..052b71f37 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -24,6 +24,10 @@ using namespace eterm::UI; +enum class CustomWidgets { + UI_TYPE_WELCOME_TAB = UI_TYPE_USER + 1, +}; + namespace ecode { class AutoCompletePlugin; @@ -332,6 +336,8 @@ class App : public UICodeEditorSplitter::Client { void updateRecentFolders(); + void updateRecentButtons(); + const CodeEditorConfig& getCodeEditorConfig() const; AppConfig& getConfig(); @@ -495,6 +501,8 @@ class App : public UICodeEditorSplitter::Client { std::string mi18nPath; std::string mScriptsPath; std::string mPlaygroundPath; + std::string mIpcPath; + std::string mPidPath; Float mDisplayDPI{ 96 }; std::shared_ptr mThreadPool; std::shared_ptr mDirTree; @@ -546,6 +554,7 @@ class App : public UICodeEditorSplitter::Client { UIMenuBar* mMenuBar{ nullptr }; std::unique_ptr mSettingsActions; std::vector mPathsToLoad; + Uint64 mIpcListenerId{ 0 }; void saveAllProcess(); @@ -647,6 +656,11 @@ class App : public UICodeEditorSplitter::Client { void insertRecentFileAndUpdateUI( const std::string& path ); void createWelcomeTab(); + + bool needsRedirectToRunningProcess( std::string file ); + + std::function + getForcePositionFn( TextPosition initialPosition ); }; } // namespace ecode diff --git a/src/tools/ecode/plugins/autocomplete/autocompleteplugin.hpp b/src/tools/ecode/plugins/autocomplete/autocompleteplugin.hpp index df37872af..c8828894c 100644 --- a/src/tools/ecode/plugins/autocomplete/autocompleteplugin.hpp +++ b/src/tools/ecode/plugins/autocomplete/autocompleteplugin.hpp @@ -154,7 +154,7 @@ class AutoCompletePlugin : public Plugin { Text mSuggestionDoc; size_t mMaxLabelCharacters{ 100 }; String::HashType mConfigHash{ 0 }; - UnorderedMap mKeyBindings; + std::unordered_map mKeyBindings; std::unordered_map mShortcuts; Float mRowHeight{ 0 }; diff --git a/src/tools/ecode/plugins/lsp/lspclientplugin.cpp b/src/tools/ecode/plugins/lsp/lspclientplugin.cpp index d9ba53c84..99027eb18 100644 --- a/src/tools/ecode/plugins/lsp/lspclientplugin.cpp +++ b/src/tools/ecode/plugins/lsp/lspclientplugin.cpp @@ -251,25 +251,27 @@ LSPClientPlugin::~LSPClientPlugin() { mShuttingDown = true; mManager->unsubscribeMessages( this ); unsubscribeFileSystemListener(); - Lock l( mDocMutex ); - for ( const auto& editor : mEditors ) { - for ( auto& kb : mKeyBindings ) { - editor.first->getKeyBindings().removeCommandKeybind( kb.first ); - if ( editor.first->hasDocument() ) - editor.first->getDocument().removeCommand( kb.first ); + { + Lock l( mDocMutex ); + for ( const auto& editor : mEditors ) { + for ( auto& kb : mKeyBindings ) { + editor.first->getKeyBindings().removeCommandKeybind( kb.first ); + if ( editor.first->hasDocument() ) + editor.first->getDocument().removeCommand( kb.first ); + } + for ( auto listener : editor.second ) + editor.first->removeEventListener( listener ); + if ( mBreadcrumb ) + editor.first->unregisterTopSpace( this ); + editor.first->unregisterPlugin( this ); } - for ( auto listener : editor.second ) - editor.first->removeEventListener( listener ); - if ( mBreadcrumb ) - editor.first->unregisterTopSpace( this ); - editor.first->unregisterPlugin( this ); - } - if ( nullptr == mManager->getSplitter() ) - return; - for ( const auto& editor : mEditorsTags ) { - if ( mManager->getSplitter()->editorExists( editor.first ) ) { - for ( const auto& tag : editor.second ) - editor.first->removeActionsByTag( tag ); + if ( nullptr == mManager->getSplitter() ) + return; + for ( const auto& editor : mEditorsTags ) { + if ( mManager->getSplitter()->editorExists( editor.first ) ) { + for ( const auto& tag : editor.second ) + editor.first->removeActionsByTag( tag ); + } } } } diff --git a/src/tools/ecode/plugins/lsp/lspclientplugin.hpp b/src/tools/ecode/plugins/lsp/lspclientplugin.hpp index 2da8d8ca7..b417b9842 100644 --- a/src/tools/ecode/plugins/lsp/lspclientplugin.hpp +++ b/src/tools/ecode/plugins/lsp/lspclientplugin.hpp @@ -126,7 +126,7 @@ class LSPClientPlugin : public Plugin { bool mBreadcrumb{ true }; bool mHoveringBreadcrumb{ false }; StyleSheetLength mBreadcrumbHeight{ "20dp" }; - UnorderedMap mKeyBindings; /* cmd, shortcut */ + std::unordered_map mKeyBindings; /* cmd, shortcut */ UnorderedMap> mDelayedDocs; Uint32 mHoverWaitCb{ 0 }; LSPHover mCurrentHover; diff --git a/src/tools/ecode/plugins/plugin.cpp b/src/tools/ecode/plugins/plugin.cpp index a9ed8510a..e07087899 100644 --- a/src/tools/ecode/plugins/plugin.cpp +++ b/src/tools/ecode/plugins/plugin.cpp @@ -71,6 +71,11 @@ void Plugin::showMessage( LSPMessageType type, const std::string& message, &msgReq ); } +Plugin::~Plugin() { + while ( mLoading ) + Sys::sleep( Milliseconds( 1 ) ); +} + void Plugin::onFileSystemEvent( const FileEvent& ev, const FileInfo& file ) { if ( ev.type != FileSystemEventType::Modified || mShuttingDown || isLoading() ) return; diff --git a/src/tools/ecode/plugins/plugin.hpp b/src/tools/ecode/plugins/plugin.hpp index b7bca9a84..7825c9ff5 100644 --- a/src/tools/ecode/plugins/plugin.hpp +++ b/src/tools/ecode/plugins/plugin.hpp @@ -17,6 +17,8 @@ class Plugin : public UICodeEditorPlugin { public: explicit Plugin( PluginManager* manager ); + virtual ~Plugin(); + void subscribeFileSystemListener(); void unsubscribeFileSystemListener(); @@ -89,23 +91,23 @@ class PluginBase : public Plugin { //! If the configuration is stored in a file, keep track of the config hash String::HashType mConfigHash{ 0 }; - virtual void onDocumentLoaded( TextDocument* ){}; + virtual void onDocumentLoaded( TextDocument* ) {}; - virtual void onDocumentClosed( TextDocument* ){}; + virtual void onDocumentClosed( TextDocument* ) {}; - virtual void onDocumentChanged( UICodeEditor*, TextDocument* /*oldDoc*/ ){}; + virtual void onDocumentChanged( UICodeEditor*, TextDocument* /*oldDoc*/ ) {}; - virtual void onRegisterListeners( UICodeEditor*, std::vector& /*listeners*/ ){}; + virtual void onRegisterListeners( UICodeEditor*, std::vector& /*listeners*/ ) {}; //! Usually used to remove keybindings in an editor - virtual void onBeforeUnregister( UICodeEditor* ){}; + virtual void onBeforeUnregister( UICodeEditor* ) {}; - virtual void onRegisterDocument( TextDocument* ){}; + virtual void onRegisterDocument( TextDocument* ) {}; - virtual void onUnregisterEditor( UICodeEditor* ){}; + virtual void onUnregisterEditor( UICodeEditor* ) {}; //! Usually used to unregister commands in a document - virtual void onUnregisterDocument( TextDocument* ){}; + virtual void onUnregisterDocument( TextDocument* ) {}; }; } // namespace ecode diff --git a/src/tools/ecode/settingsmenu.cpp b/src/tools/ecode/settingsmenu.cpp index 40e2263e4..60a66c33f 100644 --- a/src/tools/ecode/settingsmenu.cpp +++ b/src/tools/ecode/settingsmenu.cpp @@ -1169,6 +1169,14 @@ UIMenu* SettingsMenu::createWindowMenu() { ->setId( "zoom-reset" ); mWindowMenu->addSeparator(); + mWindowMenu + ->addCheckBox( i18n( "single_instance_enable", "Enable Single Instance" ), + mApp->getConfig().ui.singleInstance ) + ->setTooltipText( + i18n( "single_instance_desc", + "Newly opened files will be opened in the latest opened ecode instance." ) ) + ->setId( "single-instance-enable" ); + mWindowMenu ->addCheckBox( i18n( "welcome_screen_enable", "Enable Welcome Screen" ), mApp->getConfig().ui.welcomeScreen ) @@ -1187,6 +1195,10 @@ UIMenu* SettingsMenu::createWindowMenu() { } else if ( "welcome-screen-enable" == item->getId() ) { bool active = item->asType()->isActive(); mApp->getConfig().ui.welcomeScreen = active; + } else if ( "single-instance-enable" == item->getId() ) { + bool active = item->asType()->isActive(); + mApp->getConfig().ui.singleInstance = active; + mApp->saveConfig(); } else { String text = String( event->getNode()->asType()->getId() ).toLower(); String::replaceAll( text, " ", "-" ); diff --git a/src/tools/ecode/uiwelcomescreen.cpp b/src/tools/ecode/uiwelcomescreen.cpp index 7473c39cc..bb3d86a73 100644 --- a/src/tools/ecode/uiwelcomescreen.cpp +++ b/src/tools/ecode/uiwelcomescreen.cpp @@ -177,11 +177,20 @@ UIWelcomeScreen* UIWelcomeScreen::New( App* app ) { return eeNew( UIWelcomeScreen, ( app ) ); } +Uint32 UIWelcomeScreen::getType() const { + return static_cast( CustomWidgets::UI_TYPE_WELCOME_TAB ); +} + +bool UIWelcomeScreen::isType( const Uint32& type ) const { + return UIWelcomeScreen::getType() == type ? true : UIRelativeLayout::isType( type ); +} + UIWelcomeScreen::UIWelcomeScreen( App* app ) : UIRelativeLayout(), WidgetCommandExecuter( getUISceneNode()->getWindow()->getInput() ), mApp( app ) { setId( "welcome_ecode" ); + addClass( "welcome_tab" ); setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); app->registerUnlockedCommands( *this ); getUISceneNode()->loadLayoutFromString( LAYOUT, this, String::hash( "UIWelcomeScreen" ) ); @@ -192,8 +201,7 @@ UIWelcomeScreen::UIWelcomeScreen( App* app ) : return; node->setTooltipText( getKeyBindings().getCommandKeybindString( id ) ); node->onClick( - [this]( const MouseEvent* event ) { mApp->runCommand( event->getNode()->getId() ); }, - EE_BUTTON_LEFT ); + [this]( const MouseEvent* event ) { mApp->runCommand( event->getNode()->getId() ); } ); }; auto bindBtns = [bindBtn]( const std::initializer_list ids ) { @@ -205,26 +213,14 @@ UIWelcomeScreen::UIWelcomeScreen( App* app ) : "check-for-updates", "plugin-manager-open", "keybindings" } ); auto recentFolders = find( "recent-folders" ); - if ( !mApp->getRecentFolders().empty() ) { - recentFolders->onClick( - [this]( const MouseEvent* event ) { - mApp->createAndShowRecentFolderPopUpMenu( event->getNode() ); - }, - EE_BUTTON_LEFT ); - } else { - recentFolders->setEnabled( false ); - } + recentFolders->onClick( [this]( const MouseEvent* event ) { + mApp->createAndShowRecentFolderPopUpMenu( event->getNode() ); + } ); auto recentFiles = find( "recent-files" ); - if ( !mApp->getRecentFiles().empty() ) { - recentFiles->onClick( - [this]( const MouseEvent* event ) { - mApp->createAndShowRecentFilesPopUpMenu( event->getNode() ); - }, - EE_BUTTON_LEFT ); - } else { - recentFiles->setEnabled( false ); - } + recentFiles->onClick( [this]( const MouseEvent* event ) { + mApp->createAndShowRecentFilesPopUpMenu( event->getNode() ); + } ); find( "main_menu_shortcut" )->setText( mApp->getKeybind( "menu-toggle" ) ); @@ -244,6 +240,15 @@ UIWelcomeScreen::UIWelcomeScreen( App* app ) : welcomeDisabledChk->on( Event::OnValueChange, [welcomeDisabledChk, this]( auto ) { mApp->getConfig().ui.welcomeScreen = !welcomeDisabledChk->isChecked(); } ); + + refresh(); +} + +void UIWelcomeScreen::refresh() { + auto recentFolders = find( "recent-folders" ); + auto recentFiles = find( "recent-files" ); + recentFolders->setEnabled( !mApp->getRecentFolders().empty() ); + recentFiles->setEnabled( !mApp->getRecentFiles().empty() ); } } // namespace ecode diff --git a/src/tools/ecode/uiwelcomescreen.hpp b/src/tools/ecode/uiwelcomescreen.hpp index 616dda966..c0e0f7e0f 100644 --- a/src/tools/ecode/uiwelcomescreen.hpp +++ b/src/tools/ecode/uiwelcomescreen.hpp @@ -24,6 +24,12 @@ class UIWelcomeScreen : public UIRelativeLayout, public WidgetCommandExecuter { return WidgetCommandExecuter::onKeyDown( event ); } + void refresh(); + + Uint32 getType() const; + + bool isType( const Uint32& type ) const; + protected: App* mApp{ nullptr }; };