#include "terminalmanager.hpp" #include "ecode.hpp" #include using namespace std::literals; namespace ecode { TerminalManager::TerminalManager( App* app ) : mApp( app ) {} UITerminal* TerminalManager::createTerminalInSplitter( const std::string& workingDir, std::string program, const std::vector& args, bool fallback ) { #if EE_PLATFORM == EE_PLATFORM_WIN std::string os = Sys::getOSName( true ); if ( !LuaPattern::hasMatches( os, "Windows 1%d"sv ) && !LuaPattern::hasMatches( os, "Windows Server 201[69]"sv ) && !LuaPattern::hasMatches( os, "Windows Server 202%d"sv ) ) return nullptr; #endif UITerminal* term = nullptr; auto splitter = mApp->getSplitter(); auto& config = mApp->getConfig(); if ( splitter && splitter->hasSplit() ) { if ( splitter->getTabWidgets().size() == 2 ) { UIOrientation orientation = splitter->getMainSplitOrientation(); if ( config.term.newTerminalOrientation == NewTerminalOrientation::Vertical && orientation == UIOrientation::Horizontal ) { term = createNewTerminal( "", splitter->getTabWidgets()[1], workingDir, program, args, fallback ); } else if ( config.term.newTerminalOrientation == NewTerminalOrientation::Horizontal && orientation == UIOrientation::Vertical ) { term = createNewTerminal( "", splitter->getTabWidgets()[1], workingDir, program, args, fallback ); } else { term = createNewTerminal( "", nullptr, workingDir, program, args ); } } else { term = createNewTerminal( "", nullptr, workingDir, program, args ); } } else if ( splitter ) { switch ( config.term.newTerminalOrientation ) { case NewTerminalOrientation::Vertical: { auto cwd = workingDir.empty() ? mApp->getCurrentWorkingDir() : workingDir; splitter->split( SplitDirection::Right, splitter->getCurWidget(), false ); term = createNewTerminal( "", nullptr, cwd, program, args, fallback ); break; } case NewTerminalOrientation::Horizontal: { auto cwd = workingDir.empty() ? mApp->getCurrentWorkingDir() : workingDir; splitter->split( SplitDirection::Bottom, splitter->getCurWidget(), false ); term = createNewTerminal( "", nullptr, cwd, program, args, fallback ); break; } case NewTerminalOrientation::Same: { term = createNewTerminal( "", nullptr, "", program, args, fallback ); break; } } } return term; } void TerminalManager::applyTerminalColorScheme( const TerminalColorScheme& colorScheme ) { mApp->getSplitter()->forEachWidget( [colorScheme]( UIWidget* widget ) { if ( widget->isType( UI_TYPE_TERMINAL ) ) widget->asType()->setColorScheme( colorScheme ); } ); if ( mApp->getStatusTerminalController() && mApp->getStatusTerminalController()->getUITerminal() ) { mApp->getStatusTerminalController()->getUITerminal()->setColorScheme( colorScheme ); } } void TerminalManager::setTerminalColorScheme( const std::string& name ) { if ( name != mTerminalCurrentColorScheme ) { mTerminalCurrentColorScheme = name; mApp->termConfig().colorScheme = name; auto csIt = mTerminalColorSchemes.find( mTerminalCurrentColorScheme ); applyTerminalColorScheme( csIt != mTerminalColorSchemes.end() ? mTerminalColorSchemes.at( mTerminalCurrentColorScheme ) : TerminalColorScheme::getDefault() ); mApp->getNotificationCenter()->addNotification( String::format( mApp->i18n( "terminal_color_scheme_set", "Terminal color scheme: %s" ).toUtf8().c_str(), mTerminalCurrentColorScheme.c_str() ) ); updateColorSchemeMenu(); } } void TerminalManager::loadTerminalColorSchemes() { auto colorSchemes = TerminalColorScheme::loadFromFile( mApp->resPath() + "colorschemes/terminalcolorschemes.conf" ); if ( colorSchemes.empty() ) colorSchemes.emplace_back( TerminalColorScheme::getDefault() ); if ( FileSystem::isDirectory( mTerminalColorSchemesPath ) ) { auto colorSchemesFiles = FileSystem::filesGetInPath( mTerminalColorSchemesPath ); for ( const auto& file : colorSchemesFiles ) { auto colorSchemesInFile = TerminalColorScheme::loadFromFile( mTerminalColorSchemesPath + file ); for ( const auto& coloScheme : colorSchemesInFile ) colorSchemes.emplace_back( coloScheme ); } } else { FileSystem::makeDir( mTerminalColorSchemesPath, true ); } for ( const auto& colorScheme : colorSchemes ) mTerminalColorSchemes.insert( { colorScheme.getName(), colorScheme } ); mTerminalCurrentColorScheme = mApp->termConfig().colorScheme; if ( mTerminalColorSchemes.find( mTerminalCurrentColorScheme ) == mTerminalColorSchemes.end() ) mTerminalCurrentColorScheme = mTerminalColorSchemes.begin()->first; } std::map TerminalManager::getTerminalKeybindings() { return { { { KEY_T, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "create-new-terminal" }, { { KEY_E, KeyMod::getDefaultModifier() | KEYMOD_LALT | KEYMOD_SHIFT }, UITerminal::getExclusiveModeToggleCommandName() }, { { KEY_S, KEYMOD_LALT | KeyMod::getDefaultModifier() }, "terminal-rename" }, }; } const std::string& TerminalManager::getTerminalColorSchemesPath() const { return mTerminalColorSchemesPath; } void TerminalManager::setTerminalColorSchemesPath( const std::string& terminalColorSchemesPath ) { mTerminalColorSchemesPath = terminalColorSchemesPath; } void TerminalManager::updateColorSchemeMenu() { for ( UIPopUpMenu* menu : mColorSchemeMenues ) { for ( size_t i = 0; i < menu->getCount(); i++ ) { UIWidget* widget = menu->getItem( i ); if ( widget->isType( UI_TYPE_MENURADIOBUTTON ) ) { auto* menuItem = widget->asType(); menuItem->setActive( mTerminalCurrentColorScheme == menuItem->getText() ); } } } } bool TerminalManager::getUseFrameBuffer() const { return mUseFrameBuffer; } void TerminalManager::setUseFrameBuffer( bool useFrameBuffer ) { mUseFrameBuffer = useFrameBuffer; } void TerminalManager::configureTerminalShell() { static const auto layout( R"xml( )xml" ); UIWindow* window = mApp->getUISceneNode()->loadLayoutFromString( layout )->asType(); UIComboBox* shellCombo = window->find( "shell_combo" ); UIPushButton* ok = window->find( "ok" ); UIPushButton* cancel = window->find( "cancel" ); const std::vector list{ "bash", "sh", "zsh", "fish", "nu", "csh", "tcsh", "ksh", "dash", "cmd", "powershell", }; std::vector found; for ( const auto& i : list ) { std::string f( Sys::which( i ) ); if ( !f.empty() ) found.emplace_back( std::move( f ) ); } shellCombo->getListBox()->addListBoxItems( found ); const char* shellEnv = std::getenv( "SHELL" ); if ( !mApp->termConfig().shell.empty() ) { shellCombo->getListBox()->setSelected( mApp->termConfig().shell ); shellCombo->setText( mApp->termConfig().shell ); } else if ( shellEnv ) { std::string shellEnvStr( FileSystem::fileExists( shellEnv ) ? shellEnv : Sys::which( shellEnv ) ); if ( !shellEnvStr.empty() ) shellCombo->getListBox()->setSelected( shellEnvStr ); } else if ( Sys::getPlatform() == "Windows" ) { std::string shellEnvStr( Sys::which( "powershell" ) ); if ( !shellEnvStr.empty() ) shellCombo->getListBox()->setSelected( shellEnvStr ); } const auto setShellFn = []( App* app, UIWindow* window, UIComboBox* shellCombo ) { std::string shell( shellCombo->getText().toUtf8() ); if ( !Sys::which( shell ).empty() || FileSystem::fileExists( shell ) ) { app->getConfig().term.shell = shell; window->closeWindow(); } else { app->errorMsgBox( app->i18n( "shell_not_found", "The shell selected was not found in the file system.\nMake " "sure that the shell is visible in your PATH" ) ); } }; if ( shellCombo->getListBox()->getVerticalScrollBar() && found.size() > shellCombo->getDropDownList()->getMaxNumVisibleItems() ) shellCombo->getListBox()->getVerticalScrollBar()->setClickStep( shellCombo->getDropDownList()->getMaxNumVisibleItems() / (Float)found.size() ); ok->setFocus(); ok->onClick( [this, setShellFn, window, shellCombo]( const MouseEvent* ) { setShellFn( mApp, window, shellCombo ); }, MouseButton::EE_BUTTON_LEFT ); cancel->onClick( [window]( const MouseEvent* ) { window->closeWindow(); }, EE_BUTTON_LEFT ); window->on( Event::KeyDown, [window]( const Event* event ) { if ( event->asKeyEvent()->getKeyCode() == KEY_ESCAPE ) window->closeWindow(); } ); window->center(); window->on( Event::OnWindowReady, [ok]( const Event* ) { ok->setFocus(); } ); } void TerminalManager::configureTerminalScrollback() { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::INPUT, mApp->i18n( "configure_terminal_scrollback", "Configure terminal scrollback:" ) ); msgBox->setTitle( mApp->getWindowTitle() ); msgBox->setCloseShortcut( { KEY_ESCAPE, 0 } ); msgBox->getTextInput()->setAllowOnlyNumbers( true, false ); msgBox->getTextInput()->setText( String::toString( mApp->getConfig().term.scrollback ) ); msgBox->center(); msgBox->showWhenReady(); msgBox->on( Event::OnConfirm, [this, msgBox]( const Event* ) { int val; if ( String::fromString( val, msgBox->getTextInput()->getText() ) && val >= 0 ) { mApp->getConfig().term.scrollback = val; msgBox->closeWindow(); } } ); } UIMenu* TerminalManager::createColorSchemeMenu( bool emptyMenu ) { mColorSchemeMenuesCreatedWithHeight = emptyMenu ? 0 : mApp->uiSceneNode()->getPixelsSize().getHeight(); size_t maxItems = 19; auto cb = [this]( const Event* event ) { UIMenuItem* item = event->getNode()->asType(); const String& name = item->getText(); setTerminalColorScheme( name ); }; UIPopUpMenu* menu = UIPopUpMenu::New(); menu->addEventListener( Event::OnItemClicked, cb ); mColorSchemeMenues.push_back( menu ); size_t total = 0; const auto& colorSchemes = mTerminalColorSchemes; if ( emptyMenu ) return mColorSchemeMenues[0]; for ( auto& colorScheme : colorSchemes ) { menu->addRadioButton( colorScheme.first, mTerminalCurrentColorScheme == colorScheme.first ); if ( mColorSchemeMenues.size() == 1 && menu->getCount() == 1 ) { menu->reloadStyle( true, true ); Float height = menu->getPixelsSize().getHeight(); Float tHeight = mApp->uiSceneNode()->getPixelsSize().getHeight(); maxItems = (int)eeceil( tHeight / height ) - 2; } total++; if ( menu->getCount() == maxItems && colorSchemes.size() - total > 1 ) { UIPopUpMenu* newMenu = UIPopUpMenu::New(); menu->addSubMenu( mApp->i18n( "more_ellipsis", "More..." ), nullptr, newMenu ); newMenu->addEventListener( Event::OnItemClicked, cb ); mColorSchemeMenues.push_back( newMenu ); menu = newMenu; } } return mColorSchemeMenues[0]; } void TerminalManager::updateMenuColorScheme( UIMenuSubMenu* colorSchemeMenu ) { if ( mColorSchemeMenuesCreatedWithHeight != mApp->uiSceneNode()->getPixelsSize().getHeight() ) { for ( UIPopUpMenu* menu : mColorSchemeMenues ) menu->close(); mColorSchemeMenues.clear(); auto* newMenu = createColorSchemeMenu(); newMenu->reloadStyle( true, true ); colorSchemeMenu->setSubMenu( newMenu ); } } #if EE_PLATFORM == EE_PLATFORM_WIN std::string quoteString( std::string str ) { std::string escapedStr = ""; for ( char chr : str ) { if ( std::strchr( "()%!^\"<>&| ", chr ) ) escapedStr += '^'; escapedStr += chr; } return escapedStr; } static int openExternal( const std::string& defShell, const std::string& cmd, const std::string& scriptsPath, const std::string& workingDir ) { // This is an utility bat script based in the Geany utility script called "geany-run-helper" static const std::string RUN_HELPER = R"shellscript(REM USAGE: ecode-run-helper DIRECTORY AUTOCLOSE COMMAND... REM unnecessary, but we get the directory cd %1 shift REM save autoclose option and remove it set autoclose=%1 shift REM spawn the child REM it's tricky because shift doesn't affect %*, so hack it out REM https://en.wikibooks.org/wiki/Windows_Batch_Scripting#Command-line_arguments set SPAWN= :argloop if -%1-==-- goto argloop_end set SPAWN=%SPAWN% %1 shift goto argloop :argloop_end %SPAWN% REM show the result echo: echo: echo:------------------ echo:(program exited with code: %ERRORLEVEL%) echo: REM and if wanted, wait on the user if not %autoclose%==1 pause )shellscript"; if ( !cmd.empty() && !scriptsPath.empty() ) { std::string runHelperPath = scriptsPath + "ecode-run-helper.bat"; bool canContinue = true; if ( !FileSystem::fileExists( runHelperPath ) ) { if ( !FileSystem::fileWrite( runHelperPath, RUN_HELPER ) ) canContinue = false; } if ( canContinue ) { std::string cmdDir = String::trim( FileSystem::fileRemoveFileName( cmd ) ); if ( cmdDir.empty() ) cmdDir = workingDir; std::string cmdFile = String::trim( FileSystem::fileNameFromPath( cmd ) ); auto fcmd = "cmd.exe /q /c " + quoteString( "\"" + runHelperPath + "\" \"" + cmdDir + "\" 0 \"" + cmdFile + "\"" ); Log::info( "Running: %s", fcmd ); return Sys::execute( fcmd, workingDir ); } else { Log::info( "Couldn't write runHelperPath %s", runHelperPath ); } } std::vector options; if ( !defShell.empty() ) options.push_back( defShell ); options.push_back( "cmd" ); options.push_back( "powershell" ); for ( const auto& option : options ) { auto externalShell( Sys::which( option ) ); if ( !externalShell.empty() ) { if ( !cmd.empty() ) { auto fcmd = externalShell + " /q /c " + quoteString( "\"" + cmd + "\"" ); Log::info( "Running: %s", fcmd ); return Sys::execute( fcmd, workingDir ); } else { return Sys::execute( externalShell, workingDir ); } } } return 0; } #elif EE_PLATFORM == EE_PLATFORM_MACOS static int openExternal( const std::string&, const std::string& cmd, const std::string&, const std::string& workingDir ) { static const std::string externalShell = "open -a terminal"; if ( !cmd.empty() ) { std::string fcmd = externalShell + " \"" + cmd + "\""; Log::info( "Running: %s", fcmd ); return Sys::execute( fcmd, workingDir ); } return Sys::execute( externalShell, workingDir ); } #else static int openExternal( const std::string&, const std::string& cmd, const std::string&, const std::string& workingDir ) { std::vector options = { "gnome-terminal", "konsole", "xterm", "st" }; for ( const auto& option : options ) { auto externalShell( Sys::which( option ) ); if ( !externalShell.empty() ) { if ( !cmd.empty() ) { auto fcmd = externalShell + " -e \"" + cmd + "\""; Log::info( "Running: %s", fcmd ); return Sys::execute( fcmd, workingDir ); } else { return Sys::execute( externalShell, workingDir ); } } } return 0; } #endif int TerminalManager::openInExternalTerminal( const std::string& cmd, const std::string& workingDir ) { Log::info( "Trying to open in external terminal: %s %s", cmd, workingDir ); return openExternal( mApp->termConfig().shell, cmd, mApp->getScriptsPath(), workingDir ); } void TerminalManager::displayError( const std::string& workingDir ) { if ( mApp->getConfig().term.unsupportedOSWarnDisabled ) { openExternal( mApp->termConfig().shell, "", mApp->getScriptsPath(), workingDir ); } else { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK, mApp->i18n( "feature_not_supported_in_os", "This feature is not supported in this Operating System.\necode will try " "to open an external terminal." ) ); UICheckBox* chkDoNotWarn = UICheckBox::New(); chkDoNotWarn->setLayoutMargin( Rectf( 0, 8, 0, 0 ) ) ->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::WrapContent ) ->setLayoutGravity( UI_HALIGN_LEFT | UI_VALIGN_CENTER ) ->setClipType( ClipType::None ) ->setParent( msgBox->getLayoutCont()->getFirstChild() ) ->setId( "terminal-not-supported-chk" ); chkDoNotWarn->setText( mApp->i18n( "terminal_not_supported_do_not_warn", "Always open an external terminal (do not warn)" ) ); chkDoNotWarn->toPosition( 1 ); msgBox->on( Event::OnConfirm, [this, chkDoNotWarn, workingDir]( const Event* ) { if ( chkDoNotWarn->isChecked() ) mApp->getConfig().term.unsupportedOSWarnDisabled = true; openExternal( mApp->termConfig().shell, "", mApp->getScriptsPath(), workingDir ); } ); msgBox->showWhenReady(); } } UITerminal* TerminalManager::createNewTerminal( const std::string& title, UITabWidget* inTabWidget, const std::string& workingDir, std::string program, const std::vector& args, bool fallback ) { #if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK, mApp->i18n( "feature_not_supported_in_emscripten", "This feature is only supported in the desktop version of ecode." ) ); msgBox->showWhenReady(); return nullptr; #else UITabWidget* tabWidget = nullptr; if ( !inTabWidget ) { UIWidget* curWidget = mApp->getSplitter()->getCurWidget(); if ( !curWidget ) return nullptr; tabWidget = mApp->getSplitter()->tabWidgetFromWidget( curWidget ); } else { tabWidget = inTabWidget; } if ( !tabWidget ) { if ( !mApp->getSplitter()->getTabWidgets().empty() ) { tabWidget = mApp->getSplitter()->getTabWidgets()[0]; } else { return nullptr; } } Sizef initialSize( 16, 16 ); if ( tabWidget->getContainerNode() ) { initialSize = tabWidget->getContainerNode()->getPixelsSize(); if ( Sizef::Zero == initialSize ) { // HACK: Force the Scene Node to update the styles and layouts. tabWidget->getUISceneNode()->updateDirtyStyles(); tabWidget->getUISceneNode()->updateDirtyStyleStates(); tabWidget->getUISceneNode()->updateDirtyLayouts(); initialSize = tabWidget->getContainerNode()->getPixelsSize(); } } if ( program.empty() && !mApp->termConfig().shell.empty() ) program = mApp->termConfig().shell; UITerminal* term = UITerminal::New( mApp->getTerminalFont() ? mApp->getTerminalFont() : mApp->getFontMono(), mApp->termConfig().fontSize.asPixels( 0, Sizef(), mApp->getDisplayDPI() ), initialSize, program, args, !workingDir.empty() ? workingDir : mApp->getCurrentWorkingDir(), mApp->termConfig().scrollback, nullptr, mUseFrameBuffer ); if ( term == nullptr || term->getTerm() == nullptr ) { if ( fallback ) displayError( workingDir ); return nullptr; } auto ret = mApp->getSplitter()->createWidgetInTabWidget( tabWidget, term, title.empty() ? mApp->i18n( "shell", "Shell" ).toUtf8() : title, true ); ret.first->setIcon( mApp->findIcon( "filetype-bash" ) ); mApp->getSplitter()->removeUnusedTab( tabWidget, true, false ); term->setTitle( title ); auto csIt = mTerminalColorSchemes.find( mTerminalCurrentColorScheme ); term->getTerm()->getTerminal()->setAllowMemoryTrimnming( true ); term->setColorScheme( csIt != mTerminalColorSchemes.end() ? mTerminalColorSchemes.at( mTerminalCurrentColorScheme ) : TerminalColorScheme::getDefault() ); term->addEventListener( Event::OnTitleChange, [this]( const Event* event ) { if ( event->getNode() != mApp->getSplitter()->getCurWidget() ) return; mApp->setAppTitle( event->getNode()->asType()->getTitle() ); } ); setKeybindings( term ); term->setCommand( "terminal-rename", [this, term] { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::INPUT, mApp->i18n( "new_terminal_name", "New terminal name:" ) ); msgBox->setTitle( mApp->getWindowTitle() ); msgBox->getTextInput()->setHint( mApp->i18n( "any_name_ellipsis", "Any name..." ) ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->showWhenReady(); msgBox->addEventListener( Event::OnConfirm, [msgBox, term]( const Event* ) { std::string title( msgBox->getTextInput()->getText().toUtf8() ); term->setTitle( title ); msgBox->close(); term->setFocus(); } ); } ); term->setCommand( "switch-to-previous-colorscheme", [this] { auto it = mTerminalColorSchemes.find( mTerminalCurrentColorScheme ); auto prev = std::prev( it, 1 ); if ( prev != mTerminalColorSchemes.end() ) { setTerminalColorScheme( prev->first ); } else { setTerminalColorScheme( mTerminalColorSchemes.rbegin()->first ); } } ); term->setCommand( "switch-to-next-colorscheme", [this] { auto it = mTerminalColorSchemes.find( mTerminalCurrentColorScheme ); setTerminalColorScheme( ++it != mTerminalColorSchemes.end() ? it->first : mTerminalColorSchemes.begin()->first ); } ); term->setCommand( UITerminal::getExclusiveModeToggleCommandName(), [term, this] { term->setExclusiveMode( !term->getExclusiveMode() ); mApp->updateTerminalMenu(); } ); mApp->registerUnlockedCommands( *term ); mApp->getSplitter()->registerSplitterCommands( *term ); term->setFocus(); return term; #endif } void TerminalManager::setKeybindings( UITerminal* term ) { term->getKeyBindings().reset(); term->addKeyBinds( mApp->getRealLocalKeybindings() ); term->addKeyBinds( mApp->getRealSplitterKeybindings() ); term->addKeyBinds( mApp->getRealTerminalKeybindings() ); // Remove the keybinds that are problematic for a terminal term->getKeyBindings().removeCommandsKeybind( { "open-file", "download-file-web", "open-folder", "debug-draw-highlight-toggle", "debug-draw-boxes-toggle", "debug-draw-debug-data", "debug-widget-tree-view", "open-locatebar", "open-command-palette", "open-global-search", "menu-toggle", "console-toggle", "go-to-line", "editor-go-back", "editor-go-forward", "project-run-executable", "project-build-and-run" } ); } } // namespace ecode