From 0e97acff84ca6c859b48b22a6d42c4680ef1c432 Mon Sep 17 00:00:00 2001 From: Hugo Locurcio Date: Fri, 16 Jun 2023 09:24:32 +0200 Subject: [PATCH] Add a editor FileSystem dock action to open a terminal in selected folder This is useful to enter some commands without having to open a separate terminal and `cd` to the project folder. --- doc/classes/EditorSettings.xml | 15 ++++ editor/editor_settings.cpp | 2 + editor/filesystem_dock.cpp | 143 ++++++++++++++++++++++++++++++++- editor/filesystem_dock.h | 1 + editor/icons/Terminal.svg | 1 + 5 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 editor/icons/Terminal.svg diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index ec051c05452..3bfa24537f5 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -463,6 +463,21 @@ The program that opens raster image files when clicking "Open in External Program" option in Filesystem Dock. If not specified, the file will be opened in the system's default program. + + The terminal emulator program to use when using [b]Open in Terminal[/b] context menu action in the FileSystem dock. You can enter an absolute path to a program binary, or a path to a program that is present in the [code]PATH[/code] environment variable. + If left empty, Godot will use the default terminal emulator for the system: + - [b]Windows:[/b] PowerShell + - [b]macOS:[/b] Terminal.app + - [b]Linux:[/b] The first terminal found on the system in this order: gnome-terminal, konsole, xfce4-terminal, lxterminal, kitty, alacritty, urxvt, xterm. + To use Command Prompt (cmd) instead of PowerShell on Windows, enter [code]cmd[/code] in this field and the correct flags will automatically be used. + On macOS, make sure to point to the actual program binary located within the [code]Programs/MacOS[/code] folder of the .app bundle, rather than the .app bundle directory. + If specifying a custom terminal emulator, you may need to override [member filesystem/external_programs/terminal_emulator_flags] so it opens in the correct folder. + + + The command-line arguments to pass to the terminal emulator that is run when using [b]Open in Terminal[/b] context menu action in the FileSystem dock. See also [member filesystem/external_programs/terminal_emulator]. + If left empty, the default flags are [code]{directory}[/code], which is replaced by the absolute path to the directory that is being opened in the terminal. + [b]Note:[/b] If the terminal emulator is set to PowerShell, cmd, or Konsole, Godot will automatically prepend arguments to this list, as these terminals require nonstandard arguments to open in the correct folder. + The program that opens vector image files when clicking "Open in External Program" option in Filesystem Dock. If not specified, the file will be opened in the system's default program. diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index 1eaeee97a57..c855e435e2a 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -498,6 +498,8 @@ void EditorSettings::_load_defaults(Ref p_extra_config) { EDITOR_SETTING(Variant::STRING, PROPERTY_HINT_GLOBAL_FILE, "filesystem/external_programs/vector_image_editor", "", "") EDITOR_SETTING(Variant::STRING, PROPERTY_HINT_GLOBAL_FILE, "filesystem/external_programs/audio_editor", "", "") EDITOR_SETTING(Variant::STRING, PROPERTY_HINT_GLOBAL_FILE, "filesystem/external_programs/3d_model_editor", "", "") + EDITOR_SETTING(Variant::STRING, PROPERTY_HINT_GLOBAL_FILE, "filesystem/external_programs/terminal_emulator", "", "") + EDITOR_SETTING(Variant::STRING, PROPERTY_HINT_PLACEHOLDER_TEXT, "filesystem/external_programs/terminal_emulator_flags", "", "Call flags with placeholder: {directory}."); // Directories EDITOR_SETTING(Variant::STRING, PROPERTY_HINT_GLOBAL_DIR, "filesystem/directories/autoscan_project_path", "", "") diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp index a47bbd321d9..daa95c9c80e 100644 --- a/editor/filesystem_dock.cpp +++ b/editor/filesystem_dock.cpp @@ -2141,6 +2141,135 @@ void FileSystemDock::_file_option(int p_option, const Vector &p_selected } } break; + case FILE_OPEN_IN_TERMINAL: { + String fpath = current_path; + if (current_path == "Favorites") { + fpath = p_selected[0]; + } + + Vector terminal_emulators; + const String terminal_emulator_setting = EDITOR_GET("filesystem/external_programs/terminal_emulator"); + if (terminal_emulator_setting.is_empty()) { + // Figure out a default terminal emulator to use. +#if defined(WINDOWS_ENABLED) + // Default to PowerShell as done by Windows 10 and later. + terminal_emulators.push_back("powershell"); +#elif defined(MACOS_ENABLED) + terminal_emulators.push_back("/System/Applications/Utilities/Terminal.app"); +#elif defined(LINUXBSD_ENABLED) + // Try terminal emulators that ship with common Linux distributions first. + terminal_emulators.push_back("gnome-terminal"); + terminal_emulators.push_back("konsole"); + terminal_emulators.push_back("xfce4-terminal"); + terminal_emulators.push_back("lxterminal"); + terminal_emulators.push_back("kitty"); + terminal_emulators.push_back("alacritty"); + terminal_emulators.push_back("urxvt"); + terminal_emulators.push_back("xterm"); +#endif + } else { + // Use the user-specified terminal. + terminal_emulators.push_back(terminal_emulator_setting); + } + + String arguments = EDITOR_GET("filesystem/external_programs/terminal_emulator_flags"); + if (arguments.is_empty()) { + // NOTE: This default value is ignored further below if the terminal executable is `powershell` or `cmd`, + // due to these terminals requiring nonstandard syntax to start in a specified folder. + arguments = "{directory}"; + } + +#ifdef LINUXBSD_ENABLED + String chosen_terminal_emulator; + for (const String &terminal_emulator : terminal_emulators) { + List test_args; // Required for `execute()`, as it doesn't accept `Vector`. + test_args.push_back("-v"); + test_args.push_back(terminal_emulator); + // Silence command name being printed when found. (stderr is already silenced by `OS::execute()` by default.) + // FIXME: This doesn't appear to silence stdout. + test_args.push_back(">"); + test_args.push_back("/dev/null"); + int exit_code = 0; + const Error err = OS::get_singleton()->execute("command", test_args, nullptr, &exit_code); + if (err == OK && exit_code == EXIT_SUCCESS) { + chosen_terminal_emulator = terminal_emulator; + break; + } else if (err == ERR_CANT_FORK) { + ERR_PRINT_ED(vformat(TTR("Couldn't run external program to check for terminal emulator presence: command -v %s"), terminal_emulator)); + } + } +#else + // On Windows and macOS, the first (and only) terminal emulator in the list is always available. + String chosen_terminal_emulator = terminal_emulators[0]; +#endif + + List terminal_emulator_args; // Required for `execute()`, as it doesn't accept `Vector`. +#ifdef LINUXBSD_ENABLED + // Prepend default arguments based on the terminal emulator name. + // Use `String.ends_with()` so that installations in non-default paths + // or `/usr/local/bin` are detected correctly. + if (chosen_terminal_emulator.ends_with("konsole")) { + terminal_emulator_args.push_back("--workdir"); + } +#endif + + bool append_default_args = true; + +#ifdef WINDOWS_ENABLED + // Prepend default arguments based on the terminal emulator name. + // Use `String.get_basename().to_lower()` to handle Windows' case-insensitive paths + // with optional file extensions for executables in `PATH`. + if (chosen_terminal_emulator.get_basename().to_lower() == "powershell") { + terminal_emulator_args.push_back("-noexit"); + terminal_emulator_args.push_back("-command"); + terminal_emulator_args.push_back("cd '{directory}'"); + append_default_args = false; + } else if (chosen_terminal_emulator.get_basename().to_lower() == "cmd") { + terminal_emulator_args.push_back("/K"); + terminal_emulator_args.push_back("cd /d {directory}"); + append_default_args = false; + } +#endif + + Vector arguments_array = arguments.split(" "); + for (const String &argument : arguments_array) { + if (!append_default_args && argument == "{directory}") { + // Prevent appending a `{directory}` placeholder twice when using powershell or cmd. + // This allows users to enter the path to cmd or PowerShell in the custom terminal emulator path, + // and make it work without having to enter custom arguments. + continue; + } + terminal_emulator_args.push_back(argument); + } + + const bool is_directory = fpath.ends_with("/"); + for (int i = 0; i < terminal_emulator_args.size(); i++) { + if (is_directory) { + terminal_emulator_args[i] = terminal_emulator_args[i].replace("{directory}", ProjectSettings::get_singleton()->globalize_path(fpath)); + } else { + terminal_emulator_args[i] = terminal_emulator_args[i].replace("{directory}", ProjectSettings::get_singleton()->globalize_path(fpath).get_base_dir()); + } + } + + if (OS::get_singleton()->is_stdout_verbose()) { + // Print full command line to help with troubleshooting. + String command_string = chosen_terminal_emulator; + for (const String &arg : terminal_emulator_args) { + command_string += " " + arg; + } + print_line("Opening terminal emulator:", command_string); + } + + const Error err = OS::get_singleton()->create_process(chosen_terminal_emulator, terminal_emulator_args, nullptr, true); + if (err != OK) { + String args_string; + for (int i = 0; i < terminal_emulator_args.size(); i++) { + args_string += terminal_emulator_args[i]; + } + ERR_PRINT_ED(vformat(TTR("Couldn't run external terminal program (error code %d): %s %s\nCheck `filesystem/external_programs/terminal_emulator` and `filesystem/external_programs/terminal_emulator_flags` in the Editor Settings."), err, chosen_terminal_emulator, args_string)); + } + } break; + case FILE_OPEN: { // Open folders. TreeItem *selected = tree->get_root(); @@ -3061,12 +3190,16 @@ void FileSystemDock::_file_and_folders_fill_popup(PopupMenu *p_popup, Vectoradd_icon_shortcut(get_editor_theme_icon(SNAME("Filesystem")), ED_GET_SHORTCUT("filesystem_dock/show_in_explorer"), FILE_SHOW_IN_EXPLORER); - p_popup->set_item_text(p_popup->get_item_index(FILE_SHOW_IN_EXPLORER), item_text); + p_popup->set_item_text(p_popup->get_item_index(FILE_SHOW_IN_EXPLORER), is_directory ? TTR("Open in File Manager") : TTR("Show in File Manager")); + if (!is_directory) { p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("ExternalLink")), ED_GET_SHORTCUT("filesystem_dock/open_in_external_program"), FILE_OPEN_EXTERNAL); } + + p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Terminal")), ED_GET_SHORTCUT("filesystem_dock/open_in_terminal"), FILE_OPEN_IN_TERMINAL); + p_popup->set_item_text(p_popup->get_item_index(FILE_OPEN_IN_TERMINAL), is_directory ? TTR("Open in Terminal") : TTR("Open Containing Folder in Terminal")); #endif current_path = fpath; @@ -3111,6 +3244,7 @@ void FileSystemDock::_tree_empty_click(const Vector2 &p_pos, MouseButton p_butto // Opening the system file manager is not supported on the Android and web editors. tree_popup->add_separator(); tree_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Filesystem")), ED_GET_SHORTCUT("filesystem_dock/show_in_explorer"), FILE_SHOW_IN_EXPLORER); + tree_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Terminal")), ED_GET_SHORTCUT("filesystem_dock/open_in_terminal"), FILE_OPEN_IN_TERMINAL); #endif tree_popup->set_position(tree->get_screen_position() + p_pos); @@ -3289,6 +3423,8 @@ void FileSystemDock::_tree_gui_input(Ref p_event) { _tree_rmb_option(FILE_SHOW_IN_EXPLORER); } else if (ED_IS_SHORTCUT("filesystem_dock/open_in_external_program", p_event)) { _tree_rmb_option(FILE_OPEN_EXTERNAL); + } else if (ED_IS_SHORTCUT("filesystem_dock/open_in_terminal", p_event)) { + _tree_rmb_option(FILE_OPEN_IN_TERMINAL); } else if (ED_IS_SHORTCUT("editor/open_search", p_event)) { focus_on_filter(); } else { @@ -3349,6 +3485,8 @@ void FileSystemDock::_file_list_gui_input(Ref p_event) { _file_list_rmb_option(FILE_RENAME); } else if (ED_IS_SHORTCUT("filesystem_dock/show_in_explorer", p_event)) { _file_list_rmb_option(FILE_SHOW_IN_EXPLORER); + } else if (ED_IS_SHORTCUT("filesystem_dock/open_in_terminal", p_event)) { + _file_list_rmb_option(FILE_OPEN_IN_TERMINAL); } else if (ED_IS_SHORTCUT("editor/open_search", p_event)) { focus_on_filter(); } else { @@ -3545,6 +3683,7 @@ FileSystemDock::FileSystemDock() { // Opening the system file manager or opening in an external program is not supported on the Android and web editors. ED_SHORTCUT("filesystem_dock/show_in_explorer", TTR("Open in File Manager")); ED_SHORTCUT("filesystem_dock/open_in_external_program", TTR("Open in External Program")); + ED_SHORTCUT("filesystem_dock/open_in_terminal", TTR("Open in Terminal")); #endif // Properly translating color names would require a separate HashMap, so for simplicity they are provided as comments. diff --git a/editor/filesystem_dock.h b/editor/filesystem_dock.h index 1cde735cb8b..c22d41791e0 100644 --- a/editor/filesystem_dock.h +++ b/editor/filesystem_dock.h @@ -121,6 +121,7 @@ private: FILE_NEW, FILE_SHOW_IN_EXPLORER, FILE_OPEN_EXTERNAL, + FILE_OPEN_IN_TERMINAL, FILE_COPY_PATH, FILE_COPY_UID, FOLDER_EXPAND_ALL, diff --git a/editor/icons/Terminal.svg b/editor/icons/Terminal.svg new file mode 100644 index 00000000000..5d92bdf8bea --- /dev/null +++ b/editor/icons/Terminal.svg @@ -0,0 +1 @@ +