From fe91253276cf1327d47fa5bd7f9438bd537a90ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Wed, 9 Sep 2026 14:32:43 -0300 Subject: [PATCH] eterm: - persist keybindings in keybindings.cfg with configurable primary and secondary modifiers - hot-reload bindings and merge newly introduced defaults - open keybindings.cfg in a terminal-font-sized code editor tab - indicate unsaved keybinding changes using the modified-tab marker - share tab and split commands between terminals and the keybindings editor - add bindings for terminal actions, tab navigation, numbered tabs, splits, renaming, and tab movement - expose keybindings through terminal and tab context menus - add tab split and move-left/right context menu actions --- premake4.lua | 4 +- premake5.lua | 4 +- src/tools/eterm/eterm.cpp | 108 +++++++++---------- src/tools/eterm/eterm.hpp | 49 ++++++++- src/tools/eterm/keybindings.cpp | 183 ++++++++++++++++++++++++++++++++ 5 files changed, 287 insertions(+), 61 deletions(-) create mode 100644 src/tools/eterm/keybindings.cpp diff --git a/premake4.lua b/premake4.lua index 0be013e3b..f39d00800 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1938,8 +1938,8 @@ solution "eepp" set_kind() language "C++" files { "src/tools/eterm/**.cpp" } - links { "eterm-static" } - includedirs { "src/modules/eterm/include/", "src/thirdparty" } + links { "efsw-static", "eterm-static" } + includedirs { "src/thirdparty/efsw/include", "src/modules/eterm/include/", "src/thirdparty" } if os.is_real("linux") then links { "util" } end diff --git a/premake5.lua b/premake5.lua index 86a7534b3..3038f15b2 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1958,9 +1958,9 @@ workspace "eepp" project "eterm" set_kind() language "C++" - incdirs { "src/modules/eterm/include/", "src/thirdparty" } + incdirs { "src/thirdparty/efsw/include", "src/modules/eterm/include/", "src/thirdparty" } files { "src/tools/eterm/**.cpp" } - links { "eterm-static" } + links { "efsw-static", "eterm-static" } build_link_configuration( "eterm", false ) filter { "system:windows", "action:vs*" } files { "bin/assets/icon/eterm.rc", "bin/assets/icon/eterm.ico" } diff --git a/src/tools/eterm/eterm.cpp b/src/tools/eterm/eterm.cpp index 96a10d580..066a9ba97 100644 --- a/src/tools/eterm/eterm.cpp +++ b/src/tools/eterm/eterm.cpp @@ -5,8 +5,8 @@ namespace eterm { -void App::TerminalSplitterClient::onTabCreated( UITab* tab, UIWidget* ) { - if ( mApp.terminalIcon ) +void App::TerminalSplitterClient::onTabCreated( UITab* tab, UIWidget* widget ) { + if ( mApp.terminalIcon && widget && widget->isType( UI_TYPE_TERMINAL ) ) tab->setIcon( mApp.terminalIcon->createDrawable( PixelDensity::dpToPxI( 12 ) ) ); mApp.configureTab( tab ); } @@ -393,6 +393,12 @@ void App::configureTab( UITab* tab ) { addItem( i18n( "close_tabs_to_the_right", "Close Tabs To The Right" ), "", "close-tabs-to-the-right" ); + menu->addSeparator(); + addItem( i18n( "split_left", "Split Left" ), "split-horizontal", "split-left" ); + addItem( i18n( "split_right", "Split Right" ), "split-horizontal", "split-right" ); + addItem( i18n( "split_top", "Split Top" ), "split-vertical", "split-top" ); + addItem( i18n( "split_bottom", "Split Bottom" ), "split-vertical", "split-bottom" ); + menu->addSeparator(); auto* exclusiveMode = menu->addCheckBox( i18n( "enable_exclusive_mode", "Enable Exclusive Mode" ), terminal->getExclusiveMode(), @@ -404,6 +410,11 @@ void App::configureTab( UITab* tab ) { menu->addSeparator(); const bool canMove = clickedTab->getTabWidget()->getTabCount() > 1; + const Uint32 tabIndex = clickedTab->getTabWidget()->getTabIndex( clickedTab ); + addItem( i18n( "move_tab_left", "Move Tab Left" ), "", "move-tab-left" ) + ->setEnabled( canMove && tabIndex > 0 ); + addItem( i18n( "move_tab_right", "Move Tab Right" ), "", "move-tab-right" ) + ->setEnabled( canMove && tabIndex + 1 < clickedTab->getTabWidget()->getTabCount() ); 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" ) @@ -511,6 +522,7 @@ UITerminal* App::createTerminal( UITabWidget* target ) { addItem( i18n( "new_terminal", "New Terminal" ), "terminal", "create-new-terminal" ); menu->addSeparator(); addItem( i18n( "settings", "Settings" ), "settings", "open-settings" ); + addItem( i18n( "key_bindings", "Keybindings" ), "keybindings", "open-keybindings" ); menu->on( Event::OnItemClicked, [this, terminal]( const Event* itemEvent ) { if ( !itemEvent->getNode()->isType( UI_TYPE_MENUITEM ) ) return; @@ -530,68 +542,33 @@ UITerminal* App::createTerminal( UITabWidget* target ) { return terminal; } -UITerminal* App::createTerminalSplit( SplitDirection direction, UITerminal* terminal ) { - auto* source = terminal ? tabSplitter->tabWidgetFromWidget( terminal ) : nullptr; +UITerminal* App::createTerminalSplit( SplitDirection direction, UIWidget* widget ) { + auto* source = widget ? tabSplitter->tabWidgetFromWidget( widget ) : nullptr; auto* target = source ? tabSplitter->splitTabWidget( direction, source ) : nullptr; return target ? createTerminal( target ) : nullptr; } +void App::moveTab( UIWidget* widget, int offset ) { + if ( !widget || offset == 0 ) + return; + auto* tabs = tabSplitter->tabWidgetFromWidget( widget ); + auto* tab = tabs ? tabs->getTabFromOwnedWidget( widget ) : nullptr; + if ( !tab ) + return; + const Uint32 index = tabs->getTabIndex( tab ); + if ( offset < 0 && index > 0 ) + tabs->moveTab( tab, index - 1 ); + else if ( offset > 0 && index + 1 < tabs->getTabCount() ) + tabs->moveTab( tab, index + 1 ); +} + 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 ); } ); + registerTabCommands( *terminal, terminal ); 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" ); + applyKeybindings( terminal ); } bool App::closeWindow( EE::Window::Window* ) { @@ -809,6 +786,11 @@ int App::run( int argc, char* argv[] ) { scene = app.getUI(); if ( !appWindow || !appWindow->isOpen() || !scene ) return EXIT_FAILURE; + keybindingsPath = config->getConfigPath() + "keybindings.cfg"; + loadKeybindings(); + fileWatcher = std::make_unique(); + fileWatcher->addWatch( config->getConfigPath(), this ); + fileWatcher->watch(); scene->setColorSchemePreference( config->theme.uiColorScheme ); scene->getUIThemeManager()->setDefaultFontSize( config->font.uiSize ); FileSystem::changeWorkingDirectory( initialWorkingDirectory ); @@ -829,6 +811,19 @@ int App::run( int argc, char* argv[] ) { tint: var(--primary); cursor: hand; } + Tab.tab_modified > Tab::close { + foreground-image: url("data:image/svg,"); + foreground-tint: var(--primary); + foreground-size: 6dp 6dp; + foreground-position: center; + opacity: 1; + } + Tab.tab_modified > Tab::close:hover { + foreground-image: url("data:image/svg,"); + foreground-tint: var(--tab-close-hover); + foreground-size: 10dp 10dp; + foreground-position: center; + } )css" ); auto& resourceScope = *scene->getResourceScope(); @@ -976,6 +971,8 @@ int App::run( int argc, char* argv[] ) { appWindow->runMainLoop( [this] { appWindow->getInput()->update(); SceneManager::instance()->update(); + if ( keybindingsChanged.exchange( false, std::memory_order_acq_rel ) ) + reloadKeybindings(); 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. @@ -998,6 +995,7 @@ int App::run( int argc, char* argv[] ) { secondsCounter.restart(); } } ); + fileWatcher.reset(); return EXIT_SUCCESS; } diff --git a/src/tools/eterm/eterm.hpp b/src/tools/eterm/eterm.hpp index 1eb2af4db..156491438 100644 --- a/src/tools/eterm/eterm.hpp +++ b/src/tools/eterm/eterm.hpp @@ -2,6 +2,7 @@ #include "appconfig.hpp" #include +#include #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #include using namespace EE; @@ -41,7 +43,7 @@ struct TerminalLaunchConfig { bool closeOnExit{ false }; }; -class App { +class App : private efsw::FileWatchListener { public: int run( int argc, char* argv[] ); @@ -91,10 +93,48 @@ class App { UITerminal* createTerminal( UITabWidget* target = nullptr ); - UITerminal* createTerminalSplit( SplitDirection direction, UITerminal* terminal ); + UITerminal* createTerminalSplit( SplitDirection direction, UIWidget* widget ); + + void moveTab( UIWidget* widget, int offset ); + + template void registerTabCommands( T& commandTarget, UIWidget* widget ) { + tabSplitter->registerSplitterCommands( commandTarget ); + commandTarget.setCommand( "create-new-terminal", [this] { createNewTerminal(); } ); + commandTarget.setCommand( "open-settings", [this] { showSettings(); } ); + commandTarget.setCommand( "open-keybindings", [this] { openKeybindings(); } ); + commandTarget.setCommand( "debug-widget-tree-view", + [this] { UIWidgetInspector::create( scene ); } ); + commandTarget.setCommand( "move-tab-left", [this, widget] { moveTab( widget, -1 ); } ); + commandTarget.setCommand( "move-tab-right", [this, widget] { moveTab( widget, 1 ); } ); + commandTarget.setCommand( "split-right", [this, widget] { + createTerminalSplit( SplitDirection::Right, widget ); + } ); + commandTarget.setCommand( "split-bottom", [this, widget] { + createTerminalSplit( SplitDirection::Bottom, widget ); + } ); + commandTarget.setCommand( + "split-left", [this, widget] { createTerminalSplit( SplitDirection::Left, widget ); } ); + commandTarget.setCommand( + "split-top", [this, widget] { createTerminalSplit( SplitDirection::Top, widget ); } ); + } void addTabKeyBindings( UITerminal* terminal ); + KeyBindings::ShortcutMap getDefaultKeybindings() const; + + void loadKeybindings(); + + void reloadKeybindings(); + + void applyKeybindings( UITerminal* terminal ); + + void applyKeybindings( UICodeEditor* editor ); + + void openKeybindings(); + + void handleFileAction( efsw::WatchID, const std::string& dir, const std::string& filename, + efsw::Action action, const std::string& oldFilename ) override; + void showSettings(); void openFontPicker( bool uiFont, bool fallbackFont = false ); @@ -117,6 +157,7 @@ class App { UIIcon* terminalIcon{ nullptr }; UIMessageBox* closeDialog{ nullptr }; UIWindow* settingsWindow{ nullptr }; + UICodeEditor* keybindingsEditor{ nullptr }; UIWidget* closeDialogWidget{ nullptr }; UIWindow* maximizedTabWidgetWindow{ nullptr }; UITabWidget* maximizedTabWidget{ nullptr }; @@ -124,6 +165,10 @@ class App { TerminalLaunchConfig terminalConfig; std::unique_ptr config; std::map terminalColorSchemes; + std::unordered_map keybindings; + std::string keybindingsPath; + std::unique_ptr fileWatcher; + std::atomic keybindingsChanged{ false }; const TerminalColorScheme* selectedColorScheme{ nullptr }; Float terminalFontSize{ 12 }; bool warnBeforeClose{ false }; diff --git a/src/tools/eterm/keybindings.cpp b/src/tools/eterm/keybindings.cpp new file mode 100644 index 000000000..4cee8313c --- /dev/null +++ b/src/tools/eterm/keybindings.cpp @@ -0,0 +1,183 @@ +#include "eterm.hpp" + +#include +#include +#include + +namespace eterm { + +KeyBindings::ShortcutMap App::getDefaultKeybindings() const { + return { + { { KEY_C, KEYMOD_CTRL | KEYMOD_SHIFT }, "terminal-copy" }, + { { KEY_V, KEYMOD_CTRL | KEYMOD_SHIFT }, "terminal-paste" }, + { { KEY_T, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "create-new-terminal" }, + { { KEY_E, + KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, + UITerminal::getExclusiveModeToggleCommandName() }, + { { KEY_S, KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() }, + "terminal-rename" }, + { { KEY_COMMA, KeyMod::getDefaultModifier() }, "open-settings" }, + { { KEY_W, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "close-tab" }, + { { KEY_F11, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "debug-widget-tree-view" }, + { { KEY_PAGEDOWN, KEYMOD_CTRL }, "next-tab" }, + { { KEY_PAGEUP, KEYMOD_CTRL }, "previous-tab" }, + { { KEY_TAB, KEYMOD_CTRL }, "next-tab" }, + { { KEY_TAB, KEYMOD_CTRL | KEYMOD_SHIFT }, "previous-tab" }, + { { KEY_1, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "switch-to-tab-1" }, + { { KEY_2, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "switch-to-tab-2" }, + { { KEY_3, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "switch-to-tab-3" }, + { { KEY_4, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "switch-to-tab-4" }, + { { KEY_5, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "switch-to-tab-5" }, + { { KEY_6, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "switch-to-tab-6" }, + { { KEY_7, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "switch-to-tab-7" }, + { { KEY_8, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "switch-to-tab-8" }, + { { KEY_9, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "switch-to-tab-9" }, + { { KEY_0, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "switch-to-last-tab" }, + { { KEY_LEFT, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "move-tab-left" }, + { { KEY_RIGHT, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "move-tab-right" }, + { { KEY_L, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "split-right" }, + { { KEY_K, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "split-bottom" }, + { { KEY_J, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "split-left" }, + { { KEY_I, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "split-top" }, + { { KEY_J, KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() }, + "switch-to-previous-split" }, + { { KEY_L, KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() }, + "switch-to-next-split" }, + }; +} + +void App::loadKeybindings() { + IniFile ini( keybindingsPath ); + bool changed = false; + std::string defaultModifier = ini.getValue( "modifier", "mod" ); + if ( defaultModifier.empty() ) { + defaultModifier = KeyMod::getDefaultModifierString(); + ini.setValue( "modifier", "mod", defaultModifier ); + changed = true; + } + if ( const Uint32 modifier = KeyMod::getKeyMod( defaultModifier ); modifier != KEYMOD_NONE ) + KeyMod::setDefaultModifier( modifier ); + + std::string defaultSecondaryModifier = ini.getValue( "modifier", "mod2" ); + if ( defaultSecondaryModifier.empty() ) { + defaultSecondaryModifier = KeyMod::getDefaultSecondaryModifierString(); + ini.setValue( "modifier", "mod2", defaultSecondaryModifier ); + changed = true; + } + if ( const Uint32 modifier = KeyMod::getKeyMod( defaultSecondaryModifier ); + modifier != KEYMOD_NONE && !( modifier & KeyMod::getDefaultModifier() ) ) { + KeyMod::setDefaultSecondaryModifier( modifier ); + } else { + defaultSecondaryModifier = KeyMod::getDefaultSecondaryModifierString(); + ini.setValue( "modifier", "mod2", defaultSecondaryModifier ); + changed = true; + } + + KeyBindings formatter( appWindow->getInput() ); + auto defaults = getDefaultKeybindings(); + const bool hasKeybindings = ini.findKey( "eterm" ) != IniFile::noID; + if ( hasKeybindings ) { + keybindings = ini.getKeyUnorderedMap( "eterm" ); + } else { + keybindings.clear(); + for ( const auto& shortcut : KeyBindings::getOrderedShortcuts( defaults ) ) { + const auto& command = defaults.find( shortcut )->second; + const std::string shortcutString = formatter.getShortcutString( shortcut ); + keybindings[shortcutString] = command; + ini.setValue( "eterm", shortcutString, command ); + } + changed = true; + } + + if ( hasKeybindings ) { + std::unordered_map commands; + for ( const auto& [shortcut, command] : keybindings ) + commands[command] = shortcut; + for ( const auto& shortcut : KeyBindings::getOrderedShortcuts( defaults ) ) { + const auto& command = defaults.find( shortcut )->second; + const std::string shortcutString = formatter.getShortcutString( shortcut ); + if ( commands.find( command ) == commands.end() && + keybindings.find( shortcutString ) == keybindings.end() ) { + keybindings[shortcutString] = command; + commands[command] = shortcutString; + ini.setValue( "eterm", shortcutString, command ); + changed = true; + } + } + } + if ( changed ) + ini.writeFile(); +} + +void App::applyKeybindings( UITerminal* terminal ) { + terminal->getKeyBindings().reset(); + terminal->getKeyBindings().addKeybindsStringUnordered( keybindings ); +} + +void App::applyKeybindings( UICodeEditor* editor ) { + editor->getKeyBindings().setKeybinds( *UICodeEditor::getDefaultKeybindings() ); + editor->addKeyBinding( { KEY_S, KeyMod::getDefaultModifier() }, "save-doc", true ); + editor->getKeyBindings().addKeybindsStringUnordered( keybindings ); +} + +void App::reloadKeybindings() { + if ( !FileSystem::fileExists( keybindingsPath ) ) + return; + keybindings.clear(); + loadKeybindings(); + forEachTerminal( [this]( UITerminal* terminal ) { applyKeybindings( terminal ); } ); + if ( keybindingsEditor && tabSplitter->ownedWidgetExists( keybindingsEditor ) ) + applyKeybindings( keybindingsEditor ); +} + +void App::openKeybindings() { + if ( keybindingsEditor && tabSplitter->ownedWidgetExists( keybindingsEditor ) ) { + if ( auto* tabs = tabSplitter->tabWidgetFromWidget( keybindingsEditor ) ) + tabs->setTabSelected( tabs->getTabFromOwnedWidget( keybindingsEditor ) ); + keybindingsEditor->setFocus(); + return; + } + + auto* editor = UICodeEditor::New(); + if ( editor->loadFromFile( keybindingsPath ) == TextDocument::LoadStatus::Failed ) { + editor->close(); + return; + } + editor->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); + editor->setFontSize( terminalFontSize ); + editor->setSyntaxDefinition( editor->getDocument().guessSyntax() ); + editor->getDocument().setCommand( "save-doc", [editor] { editor->getDocument().save(); } ); + registerTabCommands( editor->getDocument(), editor ); + applyKeybindings( editor ); + editor->on( Event::OnClose, [this, editor]( const Event* ) { + if ( keybindingsEditor == editor ) + keybindingsEditor = nullptr; + } ); + + auto* current = tabSplitter->getCurWidget(); + auto* target = + current ? tabSplitter->tabWidgetFromWidget( current ) : tabSplitter->getFirstTabWidget(); + keybindingsEditor = editor; + auto* tab = tabSplitter->createWidgetInTabWidget( target, editor, "keybindings.cfg" ).first; + const auto updateTabState = [editor, tab]( const Event* ) { + const bool dirty = editor->getDocument().isDirty(); + tab->removeClass( dirty ? "tab_clear" : "tab_modified" ); + tab->addClass( dirty ? "tab_modified" : "tab_clear" ); + }; + editor->on( Event::OnTextChanged, updateTabState ); + editor->on( Event::OnDocumentUndoRedo, updateTabState ); + editor->on( Event::OnDocumentSave, updateTabState ); + updateTabState( nullptr ); + editor->setFocus(); +} + +void App::handleFileAction( efsw::WatchID, const std::string&, const std::string& filename, + efsw::Action action, const std::string& oldFilename ) { + if ( ( filename == "keybindings.cfg" || oldFilename == "keybindings.cfg" ) && + ( action == efsw::Actions::Add || action == efsw::Actions::Modified || + action == efsw::Actions::Moved ) ) { + keybindingsChanged.store( true, std::memory_order_release ); + } +} + +} // namespace eterm