#include "eterm.hpp" #include "settingspanel.hpp" #include namespace eterm { void App::TerminalSplitterClient::onTabCreated( UITab* tab, UIWidget* ) { if ( mApp.terminalIcon ) tab->setIcon( mApp.terminalIcon->createDrawable( PixelDensity::dpToPxI( 12 ) ) ); mApp.configureTab( tab ); } void App::TerminalSplitterClient::onWidgetFocusChange( UIWidget* ) { mApp.updateWindowTitle(); } std::string App::getResourcePath() const { 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; } String App::i18n( const std::string& key, const String& defaultValue ) const { return scene ? scene->i18n( key, defaultValue ) : defaultValue; } void App::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* App::terminalFromTab( UITab* tab ) { return tab && tab->getOwnedWidget() && tab->getOwnedWidget()->isType( UI_TYPE_TERMINAL ) ? tab->getOwnedWidget()->asType() : nullptr; } void App::savePreferences() { if ( config && !config->savePreferences() ) Log::error( "Could not save eterm configuration to %s", config->getConfigPath() ); } void App::forEachTerminal( const std::function& fn ) { if ( !tabSplitter ) return; tabSplitter->forEachWidgetType( UI_TYPE_TERMINAL, [&fn]( UIWidget* widget ) { fn( widget->asType() ); } ); } void App::createNewTerminal() { auto* current = tabSplitter ? tabSplitter->getCurWidget() : nullptr; auto* terminal = current && current->isType( UI_TYPE_TERMINAL ) ? current->asType() : nullptr; switch ( config->terminal.newTerminalBehavior ) { case NewTerminalBehavior::VerticalSplit: createTerminalSplit( SplitDirection::Right, terminal ); break; case NewTerminalBehavior::HorizontalSplit: createTerminalSplit( SplitDirection::Bottom, terminal ); break; default: createTerminal(); break; } } void App::openFontPicker( bool uiFont, bool fallbackFont ) { const Uint32 flags = UIFontPickerDialog::ShowStyle | ( fallbackFont ? 0 : UIFontPickerDialog::ShowSize ) | ( !uiFont && !fallbackFont ? UIFontPickerDialog::MonospaceOnly : 0 ); auto* dialog = UIFontPickerDialog::New( flags ); dialog->setTitle( i18n( "select_font", "Select Font" ) ); dialog->setCloseShortcut( KEY_ESCAPE ); std::string currentPath = uiFont ? config->font.uiPath : fallbackFont ? config->font.fallbackPath : config->font.path; if ( !currentPath.empty() ) dialog->setSelectedFont( currentPath ); if ( !fallbackFont ) { auto selection = dialog->getSelection(); selection.size = static_cast( uiFont ? config->font.uiSize : config->font.size ); dialog->setSelection( selection ); } dialog->setOnFontPicked( [this, uiFont, fallbackFont]( const UIFontSelection& selection ) { if ( selection.font.path.empty() ) return; auto& resourceScope = *scene->getResourceScope(); if ( uiFont ) { auto font = FontTrueType::New( "eterm-ui-font", resourceScope ); if ( font->loadFromFile( selection.font.path ) ) { config->font.uiPath = selection.font.path; config->font.uiSize = selection.size; scene->getUIThemeManager()->setDefaultFont( font.get() ); scene->getUIThemeManager()->setDefaultFontSize( config->font.uiSize ); scene->getRoot()->reloadStyle( true, true, true, true, true ); } } else if ( fallbackFont ) { auto font = FontTrueType::New( "eterm-fallback-font", resourceScope ); if ( font->loadFromFile( selection.font.path ) ) { config->font.fallbackPath = selection.font.path; resourceScope.getFontService().addFallbackFont( std::move( font ) ); } } else { auto font = FontTrueType::New( "eterm-monospace", resourceScope ); if ( font->loadFromFile( selection.font.path ) ) { config->font.path = selection.font.path; config->font.size = selection.size; terminalFont = font.get(); terminalFontSize = PixelDensity::dpToPx( config->font.size ); FontFamily::loadFromRegular( terminalFont ); forEachTerminal( [this]( UITerminal* terminal ) { terminal->setFont( terminalFont ); terminal->setFontSize( terminalFontSize ); } ); } } savePreferences(); } ); dialog->show(); } void App::showSettings() { if ( settingsWindow ) { settingsWindow->show(); settingsWindow->toFront(); return; } settingsWindow = eterm::SettingsPanel::create( *this ); } void App::updateWindowTitle() { if ( !appWindow ) return; 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 += " " + i18n( "fps", "FPS" ); } appWindow->setTitle( title ); } bool App::hasTerminals() const { bool found = false; if ( tabSplitter ) tabSplitter->forEachWidgetStoppable( [&found]( UIWidget* ) { found = true; return true; } ); return found; } bool App::hasRunningChildren( UITab* tab ) { auto* terminal = terminalFromTab( tab ); return terminal && terminal->getTerm() && Sys::processHasChildren( terminal->getTerm()->getProcessId() ); } void App::closeTab( UITab* tab ) { if ( !tabSplitter || !tab || !tab->getOwnedWidget() ) return; tabSplitter->closeTab( tab->getOwnedWidget()->asType(), UITabWidget::FocusTabBehavior::Default ); } void App::queueExitCloseTab( UITab* tab ) { if ( tab && std::find( pendingExitCloseTabs.begin(), pendingExitCloseTabs.end(), tab ) == pendingExitCloseTabs.end() ) { pendingExitCloseTabs.emplace_back( tab ); } } void App::queueExitedTabs() { if ( !terminalConfig.closeOnExit ) return; tabSplitter->forEachTab( [this]( 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 App::requestCloseTab( UITab* tab ) { if ( !warnBeforeClose || !hasRunningChildren( tab ) ) { closeTab( tab ); return; } if ( closeDialog ) return; closeDialog = UIMessageBox::New( UIMessageBox::OK_CANCEL, i18n( "close_terminal_running_process_confirm", "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, [this]( const Event* ) { if ( closeDialogWidget && tabSplitter->ownedWidgetExists( closeDialogWidget ) ) tabSplitter->closeTab( closeDialogWidget, UITabWidget::FocusTabBehavior::Default ); } ); closeDialog->on( Event::OnClose, [this]( const Event* ) { closeDialog = nullptr; closeDialogWidget = nullptr; } ); closeDialog->center(); closeDialog->showWhenReady(); } void App::renameSession( UITerminal* terminal ) { if ( !terminal ) return; auto* msgBox = UIMessageBox::New( UIMessageBox::INPUT, i18n( "new_terminal_name", "New terminal name:" ) ); msgBox->setTitle( "eterm" ); msgBox->getTextInput()->setHint( i18n( "any_name_ellipsis", "Any name..." ) ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->on( Event::OnConfirm, [msgBox, terminal]( const Event* ) { terminal->setTitle( msgBox->getTextInput()->getText().toUtf8() ); msgBox->close(); terminal->setFocus(); } ); msgBox->showWhenReady(); } void App::maximizeTabWidget( UITabWidget* tabWidget ) { if ( !tabWidget || scene->getRoot()->hasChild( "detached_tab_widget_win" ) ) return; UIWindow::StyleConfig winCfg; winCfg.WinFlags = UI_WIN_SHADOW | UI_WIN_MODAL | UI_WIN_EPHEMERAL | UI_WIN_NO_DECORATION; auto* win = UIWindow::NewOpt( UIWindow::SIMPLE_LAYOUT, winCfg ); maximizedTabWidgetWindow = win; maximizedTabWidget = tabWidget; win->setPixelsSize( scene->getPixelsSize() - PixelDensity::dpToPx( 64 ) ); win->setId( "detached_tab_widget_win" ); win->getModalWidget()->onClick( [this]( auto ) { restoreMaximizedTabWidget(); } ); win->setAnchors( UI_ANCHOR_TOP | UI_ANCHOR_LEFT | UI_ANCHOR_BOTTOM | UI_ANCHOR_RIGHT ); win->toFront(); win->center(); win->setKeyBindingCommand( "close-maximized-tab-widget", [this] { restoreMaximizedTabWidget(); } ); win->getKeyBindings().addKeybind( { KEY_ESCAPE }, "close-maximized-tab-widget" ); win->setCheckEphemeralCloseFn( []( Node* focusNode ) { if ( focusNode->isType( UI_TYPE_POPUPMENU ) ) return false; if ( focusNode->getSceneNode()->isUISceneNode() ) { auto* sceneNode = static_cast( focusNode->getSceneNode() ); auto* widgetTreeView = sceneNode->getRoot()->hasChild( "widget-tree-view" ); if ( widgetTreeView && ( focusNode == widgetTreeView || widgetTreeView->inParentTreeOf( focusNode ) ) ) return false; } return true; } ); auto* tabWidgetParent = tabWidget->getParent(); const bool wasFirstSplit = tabWidgetParent->isType( UI_TYPE_SPLITTER ) && tabWidgetParent->asType()->getFirstWidget() == tabWidget; auto* nodeLink = UINodeLink::NewLink( tabWidget ); maximizedTabWidgetLink = nodeLink; if ( wasFirstSplit ) nodeLink->setClass( "was_first_split" ); tabWidget->setParent( win ); tabWidget->setId( "detached_tab_widget" ); tabWidget->setPixelsPosition( Vector2f::Zero ); tabWidget->setPixelsSize( win->getContainer()->getPixelsSize() ); tabWidget->setAnchors( UI_ANCHOR_TOP | UI_ANCHOR_LEFT | UI_ANCHOR_BOTTOM | UI_ANCHOR_RIGHT ); if ( !tabWidget->inParentTreeOf( scene->getEventDispatcher()->getFocusNode() ) ) tabWidget->getTabSelected()->getOwnedWidget()->setFocus(); win->on( Event::OnWindowClose, [this]( auto ) { restoreMaximizedTabWidget(); } ); nodeLink->setParent( tabWidgetParent ); nodeLink->setId( "nodelink_tab_widget" ); if ( wasFirstSplit && tabWidgetParent->isType( UI_TYPE_SPLITTER ) ) tabWidgetParent->asType()->swap(); auto* fullscreenImg = UIImage::New(); fullscreenImg->unsetFlags( UI_AUTO_SIZE ); fullscreenImg->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); fullscreenImg->setParent( win ); fullscreenImg->setSize( { 24, tabWidget->getTabBar()->getSize().getHeight() } ); fullscreenImg->setPosition( { tabWidget->getSize().getWidth() - 24, 0 } ); fullscreenImg->setAnchors( UI_ANCHOR_TOP | UI_ANCHOR_RIGHT ); if ( auto* icon = scene->findIcon( "fullscreen" ) ) fullscreenImg->setDrawable( icon->createDrawable( PixelDensity::dpToPxI( 16 ) ) ); fullscreenImg->setVerticalAlign( UI_VALIGN_CENTER ); fullscreenImg->setHorizontalAlign( UI_HALIGN_CENTER ); fullscreenImg->addClass( "pseudo_anchor" ); fullscreenImg->toFront(); fullscreenImg->onClick( [this]( const Event* ) { restoreMaximizedTabWidget(); appWindow->getCursorManager()->set( Cursor::SysType::SysArrow ); } ); } void App::restoreMaximizedTabWidget() { if ( !scene || !SceneManager::isActive() ) return; if ( !maximizedTabWidgetLink || !maximizedTabWidget ) return; auto* nodeLink = maximizedTabWidgetLink; auto* curTabWidget = maximizedTabWidget; auto* win = maximizedTabWidgetWindow; auto* splitterParent = nodeLink->getParent(); nodeLink->setParent( scene ); curTabWidget->setParent( splitterParent ); curTabWidget->setAnchors( 0 ); curTabWidget->setId( "" ); if ( nodeLink->hasClass( "was_first_split" ) && splitterParent->isType( UI_TYPE_SPLITTER ) ) { splitterParent->asType()->swap(); } nodeLink->close(); maximizedTabWidgetLink = nullptr; maximizedTabWidget = nullptr; maximizedTabWidgetWindow = nullptr; if ( win && !win->isClosing() ) win->close(); } void App::configureTab( UITab* tab ) { tab->on( Event::OnCreateContextMenu, [this]( const Event* event ) { const auto* menuEvent = static_cast( event ); auto* menu = menuEvent->getMenu(); auto* clickedTab = event->getNode()->asType(); auto* terminal = terminalFromTab( clickedTab ); if ( !menu || !terminal || !clickedTab->getTabWidget() ) return; const auto addItem = [this, menu]( const String& text, const std::string& icon, const std::string& command ) { DrawablePtr drawable; if ( auto* menuIcon = scene->findIcon( icon ) ) drawable = menuIcon->createDrawable( PixelDensity::dpToPxI( 12 ) ); auto* item = menu->add( text, std::move( drawable ) ); item->setId( command ); return item; }; addItem( i18n( "close_tab", "Close Tab" ), "document-close", "close-tab" ); addItem( i18n( "close_other_tabs", "Close Other Tabs" ), "", "close-other-tabs" ); addItem( i18n( "close_clean_tabs", "Close Clean Tabs" ), "", "close-clean-tabs" ); addItem( i18n( "close_all_tabs", "Close All Tabs" ), "", "close-all-tabs" ); addItem( i18n( "close_tabs_to_the_left", "Close Tabs To The Left" ), "", "close-tabs-to-the-left" ); addItem( i18n( "close_tabs_to_the_right", "Close Tabs To The Right" ), "", "close-tabs-to-the-right" ); menu->addSeparator(); auto* exclusiveMode = menu->addCheckBox( i18n( "enable_exclusive_mode", "Enable Exclusive Mode" ), terminal->getExclusiveMode() ); exclusiveMode->setId( UITerminal::getExclusiveModeToggleCommandName() ); addItem( i18n( "rename_session", "Rename Session" ), "", "terminal-rename" ); addItem( i18n( "settings", "Settings" ), "settings", "open-settings" ); menu->addSeparator(); const bool canMove = clickedTab->getTabWidget()->getTabCount() > 1; addItem( i18n( "move_tab_to_start", "Move Tab To Start" ), "window", "move-tab-to-start" ) ->setEnabled( canMove ); addItem( i18n( "move_tab_to_end", "Move Tab To End" ), "window", "move-tab-to-end" ) ->setEnabled( canMove ); menu->addSeparator(); addItem( scene->getRoot()->hasChild( "detached_tab_widget_win" ) ? i18n( "restore_maximized_tab_widget", "Restore Maximized Tab Widget" ) : i18n( "maximize_tab_widget", "Maximize Tab Widget" ), "fullscreen", scene->getRoot()->hasChild( "detached_tab_widget_win" ) ? "restore-maximized-tab-widget" : "maximize-tab-widget" ); menu->on( Event::OnItemClicked, [this, clickedTab, terminal]( const Event* itemEvent ) { if ( !itemEvent->getNode()->isType( UI_TYPE_MENUITEM ) ) return; const auto& command = itemEvent->getNode()->getId(); auto* previous = tabSplitter->getCurWidget(); tabSplitter->setCurrentWidget( terminal ); if ( command == "close-clean-tabs" ) { tabSplitter->tryCloseAllTabs( terminal, UITabWidget::FocusTabBehavior::Default ); } else if ( command == "move-tab-to-start" ) { clickedTab->getTabWidget()->moveTab( clickedTab, 0 ); } else if ( command == "move-tab-to-end" ) { clickedTab->getTabWidget()->moveTab( clickedTab, clickedTab->getTabWidget()->getTabCount() ); } else if ( command == "maximize-tab-widget" ) { maximizeTabWidget( clickedTab->getTabWidget() ); } else if ( command == "restore-maximized-tab-widget" ) { restoreMaximizedTabWidget(); } else if ( command == "open-settings" ) { showSettings(); } else { terminal->execute( command ); } if ( previous && tabSplitter->ownedWidgetExists( previous ) ) tabSplitter->setCurrentWidget( previous ); } ); } ); } UITerminal* App::createTerminal( UITabWidget* target ) { 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 ); terminal->setExclusiveMode( config->terminal.exclusiveMode ); terminal->setScrollViewType( config->terminal.scrollBarType ); terminal->setVerticalScrollMode( config->terminal.scrollBarMode ); if ( selectedColorScheme ) terminal->setColorScheme( *selectedColorScheme ); auto* tab = tabSplitter->createWidgetInTabWidget( target, terminal, i18n( "terminal", "Terminal" ) ) .first; addTabKeyBindings( terminal ); terminal->on( Event::OnTitleChange, [this, tab, terminal]( const Event* ) { tab->setText( terminal->getTitle().empty() ? i18n( "terminal", "Terminal" ) : String( terminal->getTitle() ) ); if ( tabSplitter->getCurWidget() == terminal ) updateWindowTitle(); } ); terminal->getTerm()->pushEventCallback( [this, tab]( const TerminalDisplay::Event& event ) { if ( terminalConfig.closeOnExit && event.type == TerminalDisplay::EventType::PROCESS_EXIT ) queueExitCloseTab( tab ); } ); terminal->on( Event::OnCreateContextMenu, [this, terminal]( const Event* event ) { auto menu = static_cast( event )->getMenu(); const auto addItem = [this, menu]( const String& text, const std::string& icon, const std::string& command ) { DrawablePtr drawable; if ( auto* menuIcon = scene->findIcon( icon ) ) drawable = menuIcon->createDrawable( PixelDensity::dpToPxI( 12 ) ); auto* item = menu->add( text, std::move( drawable ) ); item->setId( command ); return item; }; menu->addSeparator(); addItem( i18n( "settings", "Settings" ), "settings", "open-settings" ); menu->on( Event::OnItemClicked, [this, terminal]( const Event* itemEvent ) { if ( !itemEvent->getNode()->isType( UI_TYPE_MENUITEM ) ) return; const auto& command = itemEvent->getNode()->getId(); auto* previous = tabSplitter->getCurWidget(); tabSplitter->setCurrentWidget( terminal ); terminal->execute( command ); if ( previous && tabSplitter->ownedWidgetExists( previous ) ) tabSplitter->setCurrentWidget( previous ); } ); } ); tabSplitter->setCurrentWidget( terminal ); if ( !terminalConfig.executeInShell.empty() ) terminal->executeFile( terminalConfig.executeInShell ); terminal->setFocus(); updateWindowTitle(); return terminal; } UITerminal* App::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 App::addTabKeyBindings( UITerminal* terminal ) { tabSplitter->registerSplitterCommands( *terminal ); terminal->setCommand( "create-new-terminal", [this] { createNewTerminal(); } ); terminal->setCommand( "open-settings", [this] { showSettings(); } ); terminal->setCommand( "debug-widget-tree-view", [this] { UIWidgetInspector::create( scene ); } ); terminal->setCommand( "terminal-rename", [this, terminal] { renameSession( terminal ); } ); terminal->setCommand( UITerminal::getExclusiveModeToggleCommandName(), [terminal] { terminal->setExclusiveMode( !terminal->getExclusiveMode() ); } ); terminal->setCommand( "next-tab", [this, terminal] { if ( auto* tabs = tabSplitter->tabWidgetFromWidget( terminal ) ) tabs->focusNextTab(); } ); terminal->setCommand( "previous-tab", [this, terminal] { if ( auto* tabs = tabSplitter->tabWidgetFromWidget( terminal ) ) tabs->focusPreviousTab(); } ); terminal->setCommand( "split-right", [this, terminal] { createTerminalSplit( SplitDirection::Right, terminal ); } ); terminal->setCommand( "split-bottom", [this, terminal] { createTerminalSplit( SplitDirection::Bottom, terminal ); } ); terminal->setCommand( "split-left", [this, terminal] { createTerminalSplit( SplitDirection::Left, terminal ); } ); terminal->setCommand( "split-top", [this, terminal] { createTerminalSplit( SplitDirection::Top, terminal ); } ); terminal->setCommand( "switch-to-previous-split", [this, terminal] { tabSplitter->switchPreviousSplit( terminal ); } ); terminal->setCommand( "switch-to-next-split", [this, terminal] { tabSplitter->switchNextSplit( terminal ); } ); terminal->addKeyBinding( { KEY_T, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "create-new-terminal" ); terminal->addKeyBinding( { KEY_COMMA, KeyMod::getDefaultModifier() }, "open-settings" ); terminal->addKeyBinding( { KEY_W, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "close-tab" ); terminal->addKeyBinding( { KEY_F11, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "debug-widget-tree-view" ); 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 App::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; closeDialog = UIMessageBox::New( UIMessageBox::OK_CANCEL, i18n( "close_window_running_process_confirm", "Are you sure you want to close this window? It is still running a process." ) ); closeDialog->setTitle( "eterm" ); closeDialog->on( Event::OnConfirm, [this]( const Event* ) { closeApproved = true; appWindow->close(); } ); closeDialog->on( Event::OnClose, [this]( const Event* ) { closeDialog = nullptr; closeDialogWidget = nullptr; } ); closeDialog->center(); closeDialog->showWhenReady(); return false; } int App::run( int argc, char* argv[] ) { #ifdef EE_DEBUG Log::instance()->setLogToStdOut( !Runtime::isOffscreen() ); Log::instance()->setLiveWrite( true ); #endif config = std::make_unique( Sys::getConfigPath( "eterm" ) ); config->load(); 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" }, config->terminal.shell ); args::ValueFlag shellArgs( parser, "shell-args", "Shell command line arguments", { "shell-args" }, config->terminal.shellArguments ); args::ValueFlag historySize( parser, "scrollback", "Maximum history size (lines)", { 'l', "scrollback" }, config->terminal.historySize ); 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" }, config->font.path ); args::ValueFlag fallbackFontPath( parser, "fallback-fontpath", "Fallback Font path", { "fallback-font" }, config->font.fallbackPath ); args::ValueFlag fontSize( parser, "fontsize", "Font size (in dp)", { "fontsize" }, config->font.size ); 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, config->font.hinting ); 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, config->font.antialiasing ); args::ValueFlag width( parser, "winwidth", "Window width (in dp)", { "width" }, config->windowState.size.getWidth() ); args::ValueFlag height( parser, "winheight", "Window height (in dp)", { "height" }, config->windowState.size.getHeight() ); 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" }, config->terminal.executeInShell ); args::Flag vsync( parser, "vsync", "Enable vsync", { "vsync" } ); args::ValueFlag colorScheme( parser, "color-scheme", "Load color scheme", { "color-scheme" }, config->theme.colorScheme ); 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" }, config->window.maxFPS ); 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(), config->terminal.cursorStyle ); 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::Flag alwaysShowTabBar( parser, "always-show-tab-bar", "Always show the tab bar, even with a single tab.", { "always-show-tab-bar" } ); args::ValueFlag initialTabs( parser, "tabs", "Number of initial terminal tabs", { "tabs" }, config->terminal.initialTabs ); 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; } if ( !config->savePreferences() ) Log::error( "Could not save eterm configuration to %s", config->getConfigPath() ); config->terminal.shell = shell.Get(); config->terminal.shellArguments = shellArgs.Get(); config->terminal.historySize = historySize.Get(); config->font.path = fontPath.Get(); config->font.fallbackPath = fallbackFontPath.Get(); config->font.size = fontSize.Get(); config->font.hinting = fontHinting.Get(); config->font.antialiasing = fontAntialiasing.Get(); config->windowState.size = { static_cast( width.Get() ), static_cast( height.Get() ) }; if ( pixelDensity ) config->window.pixelDensity = pixelDensity.Get(); if ( wd ) config->terminal.workingDirectory = wd.Get(); config->terminal.executeInShell = executeInShell.Get(); config->theme.colorScheme = colorScheme.Get(); config->window.maxFPS = maxFPS.Get(); config->terminal.cursorStyle = cursorStyle.Get(); config->terminal.initialTabs = initialTabs.Get(); config->terminal.useFrameBuffer |= fb.Get(); config->terminal.closeOnExit |= closeOnExit.Get(); config->window.vsync |= vsync.Get(); config->window.benchmarkMode |= benchmarkModeFlag.Get(); config->window.warnBeforeClose |= warnBeforeCloseFlag.Get(); config->window.alwaysShowTabBar |= alwaysShowTabBar.Get(); const std::string initialWorkingDirectory = FileSystem::getCurrentWorkingDirectory(); const std::string resPath = getResourcePath(); 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 ( !config->theme.colorScheme.empty() ) { auto colorSchemeIt = terminalColorSchemes.find( config->theme.colorScheme ); if ( colorSchemeIt != terminalColorSchemes.end() ) selectedColorScheme = &colorSchemeIt->second; } DisplayManager* displayManager = Engine::instance()->getDisplayManager(); Display* currentDisplay = displayManager->getDisplayIndex( config->windowState.displayIndex >= 0 && config->windowState.displayIndex < displayManager->getDisplayCount() ? config->windowState.displayIndex : 0 ); if ( !currentDisplay ) { std::cerr << "Display not found, exiting" << std::endl; return EXIT_FAILURE; } Sizei windowSize( config->windowState.size ); 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 ) ); FontTrueTypePtr uiFont; if ( !config->font.uiPath.empty() && FileSystem::fileExists( config->font.uiPath ) ) { uiFont = FontTrueType::New( "eterm-ui-font" ); if ( !uiFont->loadFromFile( config->font.uiPath ) ) uiFont.reset(); } UIApplication::Settings appSettings; appSettings.basePath = FileSystem::removeLastFolderFromPath( resPath ); appSettings.pixelDensity = config->window.pixelDensity > 0 ? config->window.pixelDensity : currentDisplay->getPixelDensity(); appSettings.fontHinting = config->font.hinting; appSettings.fontAntialiasing = config->font.antialiasing; appSettings.baseFont = uiFont.get(); const Int32 frameRateLimit = config->window.benchmarkMode ? 0 : static_cast( config->window.maxFPS ); UIApplication app( WindowSettings( windowSize.getWidth(), windowSize.getHeight(), "eterm", WindowStyle::Default, WindowBackend::Default, 32, resPath + "icon/eterm.png", appSettings.pixelDensity.value() ), appSettings, ContextSettings( config->window.vsync, frameRateLimit, config->window.multisamples, config->window.rendererVersion ) ); appWindow = app.getWindow(); scene = app.getUI(); if ( !appWindow || !appWindow->isOpen() || !scene ) return EXIT_FAILURE; scene->setColorSchemePreference( config->theme.uiColorScheme ); scene->getUIThemeManager()->setDefaultFontSize( config->font.uiSize ); if ( config->windowState.position != Vector2i( -1, -1 ) ) { appWindow->setPosition( config->windowState.position.x + ( config->windowState.maximized ? -1 : 0 ), config->windowState.position.y ); } #if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN && EE_PLATFORM != EE_PLATFORM_MACOS if ( config->windowState.maximized ) appWindow->maximize(); #endif FileSystem::changeWorkingDirectory( initialWorkingDirectory ); appWindow->setClearColor( RGB( 0, 0, 0 ) ); scene->getUIThemeManager()->setDefaultEffectsEnabled( false ); scene->combineStyleSheet( R"css( TabWidget { max-tab-width: 200dp; } Tab > Tab::Text { text-overflow: ellipsis; } .pseudo_anchor { tint: var(--floating-icon); cursor: arrow; } .pseudo_anchor:hover { tint: var(--primary); cursor: hand; } )css" ); 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" ); } if ( !config->font.path.empty() && FileSystem::fileExists( config->font.path ) ) { terminalFont = FontTrueType::New( "eterm-monospace", resourceScope ).get(); if ( terminalFont->loadFromFile( config->font.path ) ) 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 ( !config->font.fallbackPath.empty() ) { if ( FileSystem::fileExists( config->font.fallbackPath ) ) { auto fallback = FontTrueType::New( "eterm-fallback-font", resourceScope ); if ( fallback->loadFromFile( config->font.fallbackPath ) ) resourceScope.getFontService().addFallbackFont( std::move( fallback ) ); } } else if ( auto fallback = resourceScope.findFont( "DroidSansFallbackFull" ) ) { resourceScope.getFontService().addFallbackFont( std::move( fallback ) ); } const std::string launchPath = config->terminal.workingDirectory.empty() ? initialWorkingDirectory : config->terminal.workingDirectory; FileInfo launchFile( launchPath ); const bool launchExecutable = launchFile.isRegularFile() && launchFile.isExecutable(); terminalConfig.program = launchExecutable ? launchFile.getFilepath() : config->terminal.shell; terminalConfig.arguments = config->terminal.shellArguments.empty() ? std::vector{} : String::split( config->terminal.shellArguments ); terminalConfig.workingDirectory = launchFile.getDirectoryPath(); terminalConfig.executeInShell = config->terminal.executeInShell; terminalConfig.historySize = config->terminal.historySize; terminalConfig.cursorStyle = config->terminal.cursorStyle; terminalConfig.fontHinting = config->font.hinting; terminalConfig.fontAntialiasing = config->font.antialiasing; terminalConfig.useFrameBuffer = config->terminal.useFrameBuffer; terminalConfig.keepAlive = !launchExecutable && config->terminal.shell.empty(); terminalConfig.closeOnExit = config->terminal.closeOnExit; warnBeforeClose = config->window.warnBeforeClose; benchmarkMode = config->window.benchmarkMode; terminalFontSize = PixelDensity::dpToPx( config->font.size ); 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( !config->window.alwaysShowTabBar ); tabSplitter->setCanCreateSplitFn( [this]( SplitDirection, UIWidget* ) { restoreMaximizedTabWidget(); return true; } ); tabSplitter->setTabTryCloseCallback( [this]( UIWidget* widget, UITabWidget::FocusTabBehavior, std::function ) { if ( auto* detachedTabWidget = scene->getRoot()->find( "detached_tab_widget" ); widget && detachedTabWidget && detachedTabWidget->inParentTreeOf( widget ) ) { restoreMaximizedTabWidget(); } if ( warnBeforeClose ) { auto* tab = tabSplitter->getTabFromWidget( widget ); if ( hasRunningChildren( tab ) ) { requestCloseTab( tab ); return false; } } return true; } ); tabSplitter->setOnTabWidgetCreateCb( [this]( UITabWidget* tabs ) { tabs->on( Event::OnTabSelected, [this]( const Event* ) { updateWindowTitle(); } ); tabs->on( Event::OnTabClosed, [this]( 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 ), config->terminal.initialTabs ); ++tab ) { if ( !createTerminal( tabs ) ) { appWindow->showMessageBox( EE::Window::Window::MessageBoxType::Error, "eterm", i18n( "operating_system_not_supported", "Operating System not supported." ) ); return EXIT_FAILURE; } } appWindow->setCloseRequestCallback( [this]( EE::Window::Window* window ) { return closeWindow( window ); } ); app.setShowMemoryManagerResult( true ); appWindow->runMainLoop( [this] { 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(); } } ); config->captureWindowState( appWindow ); if ( !config->saveWindowState() ) Log::error( "Could not save eterm window state to %s", config->getConfigPath() ); return EXIT_SUCCESS; } } // namespace eterm EE_MAIN_FUNC int main( int argc, char* argv[] ) { App app; return app.run( argc, argv ); }