From 93ddce0f44121ca24290e62dad1159c9838ca9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Gon=C3=A7alves?= Date: Sat, 17 May 2025 15:53:30 -0300 Subject: [PATCH 1/7] Add an embedded terminal --- gresources/nemo-shell-ui.xml | 1 + libnemo-private/org.nemo.gschema.xml | 47 +- meson.build | 1 + src/meson.build | 3 +- src/nemo-actions.h | 1 + src/nemo-terminal-widget.c | 2986 ++++++++++++++++++++++++++ src/nemo-terminal-widget.h | 131 ++ src/nemo-window-manage-views.c | 5 + src/nemo-window-menus.c | 24 + src/nemo-window-slot.c | 122 ++ src/nemo-window-slot.h | 12 + 11 files changed, 3331 insertions(+), 2 deletions(-) create mode 100644 src/nemo-terminal-widget.c create mode 100644 src/nemo-terminal-widget.h diff --git a/gresources/nemo-shell-ui.xml b/gresources/nemo-shell-ui.xml index 50ce6d15f..ecaa2c403 100644 --- a/gresources/nemo-shell-ui.xml +++ b/gresources/nemo-shell-ui.xml @@ -71,6 +71,7 @@ + diff --git a/libnemo-private/org.nemo.gschema.xml b/libnemo-private/org.nemo.gschema.xml index 63b8b348b..8c5b75899 100644 --- a/libnemo-private/org.nemo.gschema.xml +++ b/libnemo-private/org.nemo.gschema.xml @@ -59,7 +59,7 @@ - + @@ -67,6 +67,21 @@ + + + + + + + + + + + + + + + @@ -625,6 +640,36 @@ Whether the navigation window should be maximized. Whether the navigation window should be maximized by default. + + 'both' + Local terminal folder synchronization mode + Determines how the embedded terminal synchronizes its current directory with the file manager when navigating local folders. 'none': No synchronization. 'fm-to-term': File manager navigation changes terminal directory. 'term-to-fm': Terminal navigation changes file manager location. 'both': Synchronization works in both directions. + + + 'off' + SSH terminal auto-connection and synchronization mode preference + Determines behavior when an SFTP location is active. 'off': Do not connect automatically. 'sync-both': Connect and sync both ways. 'sync-fm-to-term': Connect and sync File Manager to Terminal. 'sync-term-to-fm': Connect and sync Terminal to File Manager. 'sync-none': Connect without folder synchronization. + + + 300 + Terminal panel height + Height of the terminal panel in pixels. + + + false + Terminal pane visibility + Whether the terminal pane should be visible. + + + 'system' + Terminal color scheme + The color scheme to use for the embedded terminal. Available values: 'system', 'dark', 'light', 'solarized-dark', 'solarized-light', 'matrix', 'one-half-dark', 'one-half-light', 'monokai', 'custom'. + + + 12 + Terminal font size + The font size to use for the embedded terminal in point units. + 170 Width of the side pane diff --git a/meson.build b/meson.build index 5eee1ed34..73fcded03 100644 --- a/meson.build +++ b/meson.build @@ -77,6 +77,7 @@ gmodule = dependency('gmodule-no-export-2.0', version: glib_version) gobject = dependency('gobject-2.0', version: '>=2.0') go_intr = dependency('gobject-introspection-1.0', version: '>=1.0') json = dependency('json-glib-1.0', version: '>=1.6') +vte = dependency('vte-2.91', version: '>=0.52.0') cinnamon= dependency('cinnamon-desktop', version: '>=4.8.0') gail = dependency('gail-3.0') diff --git a/src/meson.build b/src/meson.build index 6f6cd55d6..671811b93 100644 --- a/src/meson.build +++ b/src/meson.build @@ -60,6 +60,7 @@ nemoCommon_sources = [ 'nemo-script-config-widget.c', 'nemo-self-check-functions.c', 'nemo-statusbar.c', + 'nemo-terminal-widget.c', 'nemo-thumbnail-problem-bar.c', 'nemo-toolbar.c', 'nemo-trash-bar.c', @@ -103,7 +104,7 @@ if enableEmptyView endif nemo_deps = [ cinnamon, gail, glib, gtk, math, - egg, nemo_extension, nemo_private, xapp ] + egg, nemo_extension, nemo_private, xapp, vte ] if exempi_enabled nemo_deps += exempi diff --git a/src/nemo-actions.h b/src/nemo-actions.h index 0ec1c0722..27f4d37dd 100644 --- a/src/nemo-actions.h +++ b/src/nemo-actions.h @@ -149,6 +149,7 @@ #define NEMO_ACTION_OPEN_IN_TERMINAL "OpenInTerminal" #define NEMO_ACTION_FOLLOW_SYMLINK "FollowSymbolicLink" #define NEMO_ACTION_OPEN_CONTAINING_FOLDER "OpenContainingFolder" +#define NEMO_ACTION_SHOW_HIDE_TERMINAL "Show Hide Terminal" #define NEMO_ACTION_PLUGIN_MANAGER "NemoPluginManager" diff --git a/src/nemo-terminal-widget.c b/src/nemo-terminal-widget.c new file mode 100644 index 000000000..023c3bcd0 --- /dev/null +++ b/src/nemo-terminal-widget.c @@ -0,0 +1,2986 @@ +/* nemo-terminal-widget.c + + Copyright (C) 2025 + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public + License along with this program; if not, see . + + Author: Bruno Goncalves + */ + + +#include "nemo-terminal-widget.h" + +#include +#include +#include +#include +#include "nemo-window.h" +#include "nemo-window-slot.h" + +/* Data keys for g_object_set_data / g_object_get_data */ +static const gchar * const DATA_KEY_SSH_HOSTNAME = "ssh-hostname"; +static const gchar * const DATA_KEY_SSH_USERNAME = "ssh-username"; +static const gchar * const DATA_KEY_SSH_PORT = "ssh-port"; +static const gchar * const DATA_KEY_SSH_SYNC_MODE = "ssh-sync-mode"; +static const gchar * const DATA_KEY_SCHEME_NAME = "scheme-name"; +static const gchar * const DATA_KEY_FONT_SIZE = "font-size"; +static const gchar * const DATA_KEY_LOCAL_SYNC_MODE = "local-sync-mode"; +static const gchar * const DATA_KEY_SFTP_AUTO_CONNECT_MODE = "sftp-auto-connect-mode"; + +/* Forward declarations */ + +/* Action Callbacks */ +static void on_copy_activate (GSimpleAction *action, + GVariant *parameter, + gpointer user_data); +static void on_paste_activate (GSimpleAction *action, + GVariant *parameter, + gpointer user_data); +static void on_select_all_activate (GSimpleAction *action, + GVariant *parameter, + gpointer user_data); + +/* Terminal Event Callbacks */ +static gboolean on_terminal_button_press(GtkWidget *widget, + GdkEventButton *event, + gpointer user_data); +static gboolean on_terminal_key_press(GtkWidget *widget, + GdkEventKey *event, + gpointer user_data); +static void on_terminal_contents_changed(VteTerminal *terminal, + gpointer user_data); +static void on_terminal_directory_changed(VteTerminal *terminal, + gpointer user_data); +static void on_terminal_child_exited(VteTerminal *terminal, + gint status, + gpointer user_data); + +/* Menu Item Callbacks */ +static void on_color_scheme_changed(GtkWidget *widget, + gpointer user_data); +static void on_font_size_changed(GtkWidget *widget, + gpointer user_data); +static void on_local_sync_mode_changed(GtkWidget *widget, + gpointer user_data); +static void on_sftp_auto_connect_behavior_changed(GtkWidget *widget, + gpointer user_data); +static void on_ssh_connect_activate(GtkWidget *widget, /* GtkMenuItem */ + gpointer user_data); +static void on_ssh_exit_activate(GtkWidget *widget, /* GtkMenuItem */ + gpointer user_data); +static void _on_menu_item_activate_widget_action (GtkMenuItem *menuitem, + gpointer user_data); + +/* SSH Helper Functions */ +static void _initiate_ssh_connection(NemoTerminalWidget *self, + const gchar *hostname, + const gchar *username, + const gchar *port, + NemoTerminalSyncMode sync_mode); +static void clear_ssh_state(NemoTerminalWidget *self); +static gchar *build_ssh_command_string(const gchar *hostname, + const gchar *username, + const gchar *port); +static gboolean parse_gvfs_ssh_path(GFile *location, + gchar **hostname, + gchar **username, + gchar **port); +static gchar *get_remote_path_from_sftp_gfile(GFile *location); + +/* Directory and Command Helper Functions */ +static void change_directory_in_terminal(NemoTerminalWidget *self, GFile *location); +static void feed_cd_command(VteTerminal *terminal, const char *path); + +/* UI and State Helper Functions */ +static void setup_terminal_font(VteTerminal *terminal); +static int nemo_terminal_get_font_size(void); +static void nemo_terminal_widget_save_font_size(NemoTerminalWidget *self, int font_size); +static void reset_terminal_to_current_location(NemoTerminalWidget *self); +static gboolean focus_once_and_remove(gpointer widget_data); +static gboolean reset_toggling_flag(gpointer user_data); +static GtkWidget * create_terminal_popup_menu(NemoTerminalWidget *self); +static void on_container_size_changed(GtkPaned *paned, GParamSpec *pspec, gpointer user_data); +static void on_paned_destroy(GtkWidget *widget, gpointer user_data); +static gboolean apply_initial_size_idle(gpointer user_data); +static void _add_action_menu_item_compat(GtkMenuShell *menu_shell, + NemoTerminalWidget *self, + const gchar *label, + const gchar *detailed_action_name); +static void _add_callback_menu_item(GtkMenuShell *menu_shell, + const gchar *label, + GCallback callback, + gpointer user_data); +static GtkWidget *_add_radio_menu_item_with_data(GtkMenuShell *menu_shell, + GSList **radio_group, + const gchar *label, + const gchar *data_key, + gpointer data_value, + gboolean is_active, + GCallback activate_callback, + gpointer user_data); + +/* GObject Lifecycle */ +static void nemo_terminal_widget_finalize(GObject *object); + + +/** + * on_ssh_connect_activate: + * @widget: The GtkMenuItem that was activated. + * @user_data: The #NemoTerminalWidget instance. + * + * Callback for when an SSH connection menu item is activated. + * Retrieves SSH connection details from the menu item's data and + * initiates the SSH connection. + */ +static void +on_ssh_connect_activate(GtkWidget *widget, + gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + + // Retrieve connection parameters stored on the menu item + const gchar *hostname = g_object_get_data(G_OBJECT(widget), DATA_KEY_SSH_HOSTNAME); + const gchar *username = g_object_get_data(G_OBJECT(widget), DATA_KEY_SSH_USERNAME); + const gchar *port = g_object_get_data(G_OBJECT(widget), DATA_KEY_SSH_PORT); + NemoTerminalSyncMode sync_mode = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), DATA_KEY_SSH_SYNC_MODE)); + + if (hostname != NULL) + { + _initiate_ssh_connection(self, hostname, username, port, sync_mode); + } + else + { + g_warning("Hostname not found on SSH menu item for manual connection."); + } +} + +/** + * _initiate_ssh_connection: + * @self: The #NemoTerminalWidget instance. + * @hostname: The hostname to connect to. + * @username: (Optional) The username for the SSH connection. + * @port: (Optional) The port for the SSH connection. + * @sync_mode: The directory synchronization mode to use for this SSH session. + * + * Builds and executes the SSH command in the terminal. Sets up SSH state + * variables and prepares for directory synchronization if configured. + * The command fed to the terminal includes "; exit" to ensure the shell + * process hosting the ssh client exits, triggering on_terminal_child_exited. + */ +static void +_initiate_ssh_connection(NemoTerminalWidget *self, + const gchar *hostname, + const gchar *username, + const gchar *port, + NemoTerminalSyncMode sync_mode) +{ + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + g_return_if_fail(hostname != NULL && *hostname != '\0'); + + // Build the basic "ssh user@host -p port\n" command + gchar *ssh_command_line_nl = build_ssh_command_string(hostname, username, port); + if (!ssh_command_line_nl) + { + g_warning("Failed to build SSH command string."); + return; + } + + // Append "; exit" to the SSH command. + // This ensures that when the ssh client process finishes, the local shell + // running it also exits, which correctly triggers 'on_terminal_child_exited'. + g_autofree gchar *final_command_to_feed = NULL; + if (g_str_has_suffix(ssh_command_line_nl, "\n")) + { + // Remove trailing '\n' from ssh_command_line_nl and append "; exit\n" + GString *str_builder = g_string_new_len(ssh_command_line_nl, strlen(ssh_command_line_nl) - 1); + g_string_append(str_builder, "; exit\n"); + final_command_to_feed = g_string_free(str_builder, FALSE); + } + else + { + // Fallback: Should not happen if build_ssh_command_string is consistent + g_warning("_initiate_ssh_connection: ssh_command_line_nl did not end with newline as expected."); + final_command_to_feed = g_strconcat(ssh_command_line_nl, "; exit\n", NULL); + } + g_free(ssh_command_line_nl); // Free the original command string + + if (!final_command_to_feed) { + g_warning("Failed to build final SSH command string with ; exit."); + return; + } + + // Mark that we're in the process of connecting via SSH. + // Full setup (like cd to remote path) happens after connection is established (see on_terminal_contents_changed). + self->ssh_connecting = TRUE; + self->pending_ssh_sync_mode = sync_mode; + + // Set SSH mode and store connection details + self->in_ssh_mode = TRUE; + self->ssh_sync_mode = sync_mode; // Set the determined sync mode for this session + g_free(self->ssh_hostname); + self->ssh_hostname = g_strdup(hostname); + g_free(self->ssh_username); + self->ssh_username = g_strdup(username); + g_free(self->ssh_port); + self->ssh_port = g_strdup(port); + + // If current location is SFTP, try to get remote path for potential `cd` after connection + if (self->current_location && G_IS_FILE(self->current_location)) + { + g_free(self->ssh_remote_path); + self->ssh_remote_path = get_remote_path_from_sftp_gfile(self->current_location); + } + + // Show SSH indicator in the UI + if (self->ssh_indicator) + { + gtk_widget_show(self->ssh_indicator); + } + + // Feed the complete command (e.g., "ssh user@host; exit\n") to the terminal + vte_terminal_feed_child(self->terminal, final_command_to_feed, -1); +} + +/** + * clear_ssh_state: + * @self: The #NemoTerminalWidget instance. + * + * Clears all SSH-related state variables in the widget, + * effectively ending the SSH mode. + */ +static void +clear_ssh_state(NemoTerminalWidget *self) +{ + if (self->in_ssh_mode || self->ssh_connecting) + { + self->in_ssh_mode = FALSE; + self->ssh_sync_mode = NEMO_TERMINAL_SYNC_NONE; // Reset to default + g_clear_pointer(&self->ssh_hostname, g_free); + g_clear_pointer(&self->ssh_username, g_free); + g_clear_pointer(&self->ssh_port, g_free); + g_clear_pointer(&self->ssh_remote_path, g_free); + self->ssh_connecting = FALSE; // Ensure this is reset + self->pending_ssh_sync_mode = NEMO_TERMINAL_SYNC_NONE; + } +} + +/** + * reset_terminal_to_current_location: + * @self: The #NemoTerminalWidget instance. + * + * Resets the terminal state, typically after an SSH session ends. + * It clears SSH state, hides the SSH indicator, updates the terminal's + * current location to match the file manager's active native location, + * and spawns a new local shell. + */ +static void +reset_terminal_to_current_location(NemoTerminalWidget *self) +{ + NemoWindowSlot *slot = NULL; + NemoWindow *win = NULL; + + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + + // Ignore the next cd signal that might be emitted by the shell startup + self->ignore_next_terminal_cd_signal = TRUE; + + // Hide SSH indicator + if (self->ssh_indicator) { + gtk_widget_hide(self->ssh_indicator); + } + + // Attempt to get the active Nemo window and slot to find the current FM location + if (self->container_paned) { + GtkWidget *toplevel = gtk_widget_get_toplevel(GTK_WIDGET(self->container_paned)); + if (toplevel && NEMO_IS_WINDOW(toplevel)) { + win = NEMO_WINDOW(toplevel); + slot = nemo_window_get_active_slot(win); + } + } + + // Update current_location to the file manager's active native path + if (slot && slot->location && G_IS_FILE(slot->location) && g_file_is_native(slot->location)) { + g_set_object(&self->current_location, slot->location); + } else { + // Fallback to no specific location (will use home or default) + g_set_object(&self->current_location, NULL); + } + + clear_ssh_state(self); // Crucial: resets SSH mode flags and data + spawn_terminal_in_widget(self); // Spawn a new local shell +} + +/** + * on_ssh_exit_activate: + * @widget: The GtkMenuItem that was activated ("Disconnect from SSH"). + * @user_data: The #NemoTerminalWidget instance. + * + * Handles the user's request to disconnect from an active SSH session. + * Feeds an "exit" command to the terminal (intended for the remote shell) + * and then resets the terminal to a local state. + */ +static void +on_ssh_exit_activate(GtkWidget *widget, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + + if (!self->in_ssh_mode) return; // Not in SSH mode, nothing to exit + + self->is_exiting_ssh = TRUE; // Flag to manage state in on_terminal_child_exited + self->ignore_next_terminal_cd_signal = TRUE; // Ignore cd from shell startup + + // Send "exit\n" to the terminal. This should terminate the remote shell. + // The "; exit" part of the original ssh command will then cause the local + // child process (that ran ssh) to exit, triggering on_terminal_child_exited. + vte_terminal_feed_child(self->terminal, "exit\n", -1); + + // Proactively reset the terminal state. on_terminal_child_exited will see + // is_exiting_ssh = TRUE and will not attempt another reset. + reset_terminal_to_current_location(self); + + self->is_exiting_ssh = FALSE; // Reset the flag +} + +/* Action entries for the terminal (copy, paste, select-all) */ +static GActionEntry terminal_entries[] = { + {"copy", on_copy_activate, NULL, NULL, NULL}, + {"paste", on_paste_activate, NULL, NULL, NULL}, + {"select-all", on_select_all_activate, NULL, NULL, NULL}, +}; + +/* GObject properties */ +enum +{ + PROP_0, + PROP_CURRENT_LOCATION, + N_PROPS +}; + +static GParamSpec *properties[N_PROPS]; + +/* GObject signals */ +enum +{ + CHANGE_DIRECTORY, + TOGGLE_VISIBILITY, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE(NemoTerminalWidget, nemo_terminal_widget, GTK_TYPE_BOX) + +/*** Helper structs for menu creation ***/ +typedef struct { + const gchar *id; // Internal identifier for the scheme + const gchar *label_pot; // Translatable label (e.g., N_("System")) +} MenuSchemeEntry; + +static const MenuSchemeEntry COLOR_SCHEME_ENTRIES[] = { + {"system", N_("System")}, + {"dark", N_("Dark")}, + {"light", N_("Light")}, + {"solarized-dark", N_("Solarized Dark")}, + {"solarized-light", N_("Solarized Light")}, + {"matrix", N_("Matrix")}, + {"one-half-dark", N_("One Half Dark")}, + {"one-half-light", N_("One Half Light")}, + {"monokai", N_("Monokai")}, +}; + +typedef struct { + int size_pts; // Font size in points +} MenuFontSizeEntry; + +static const MenuFontSizeEntry FONT_SIZE_ENTRIES[] = { + {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}, + {17}, {18}, {20}, {22}, {24}, {28}, {32}, {36}, {40}, {48} +}; + +typedef struct { + NemoTerminalSyncMode mode; // Sync mode enum value + const gchar *label_pot; // Translatable label +} MenuSyncModeEntry; + +static const MenuSyncModeEntry LOCAL_SYNC_MODE_ENTRIES[] = { + {NEMO_TERMINAL_SYNC_BOTH, N_("Sync Both Ways")}, + {NEMO_TERMINAL_SYNC_FM_TO_TERM, N_("Sync File Manager → Terminal")}, + {NEMO_TERMINAL_SYNC_TERM_TO_FM, N_("Sync Terminal → File Manager")}, + {NEMO_TERMINAL_SYNC_NONE, N_("No Sync")} +}; + +typedef struct { + NemoTerminalSshAutoConnectMode mode; // Auto-connect mode enum + const gchar *label_pot; // Translatable label + NemoTerminalSyncMode sync_mode_for_connection; // Sync mode to use if auto-connecting +} MenuSshAutoConnectEntry; + +static const MenuSshAutoConnectEntry SFTP_AUTO_CONNECT_ENTRIES[] = { + {NEMO_TERMINAL_SSH_AUTOCONNECT_OFF, N_("Do not connect automatically"), NEMO_TERMINAL_SYNC_NONE}, + {NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH, N_("Automatically connect and sync both ways"), NEMO_TERMINAL_SYNC_BOTH}, + {NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM, N_("Automatically connect and sync: File Manager → Terminal"), NEMO_TERMINAL_SYNC_FM_TO_TERM}, + {NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM, N_("Automatically connect and sync: Terminal → File Manager"), NEMO_TERMINAL_SYNC_TERM_TO_FM}, + {NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_NONE, N_("Automatically connect without syncing"), NEMO_TERMINAL_SYNC_NONE} +}; + +static const MenuSyncModeEntry MANUAL_SSH_SYNC_ENTRIES[] = { + {NEMO_TERMINAL_SYNC_BOTH, N_("Sync folder both ways")}, + {NEMO_TERMINAL_SYNC_FM_TO_TERM, N_("Sync folder from File Manager → Terminal")}, + {NEMO_TERMINAL_SYNC_TERM_TO_FM, N_("Sync folder from Terminal → File Manager")}, + {NEMO_TERMINAL_SYNC_NONE, N_("No folder sync")} +}; + +/** + * _on_menu_item_activate_widget_action: + * @menuitem: The #GtkMenuItem that was activated. + * @user_data: Unused in this callback. + * + * Compatibility handler to bridge GtkMenuItem's "activate" signal to + * a GAction registered on the widget. The widget instance and action name + * are retrieved from data set on the menu item. + */ +static void +_on_menu_item_activate_widget_action (GtkMenuItem *menuitem, + gpointer user_data) +{ + // Retrieve the NemoTerminalWidget instance and the detailed action name stored on the menu item. + NemoTerminalWidget *self = (NemoTerminalWidget *)g_object_get_data(G_OBJECT(menuitem), "ntw-self"); + const gchar *detailed_action_name = (const gchar *)g_object_get_data(G_OBJECT(menuitem), "ntw-action-name"); + + if (self && detailed_action_name) { + // Parse the action name from the detailed_action_name (e.g., "terminal.copy" -> "copy"). + // The detailed_action_name includes a prefix like "widget." or "terminal.". + const gchar *dot = strchr(detailed_action_name, '.'); + if (dot && self->action_group) { + const gchar *action_name = dot + 1; // Get the action name part after the dot. + g_action_group_activate_action(G_ACTION_GROUP(self->action_group), action_name, NULL); + } else { + g_warning("Could not parse action name or action group not found for menu item: %s", detailed_action_name); + } + } +} + +/** + * _add_action_menu_item_compat: + * @menu_shell: The #GtkMenuShell to add the item to. + * @self: The #NemoTerminalWidget instance (used for context in the action). + * @label: The translatable label for the menu item. + * @detailed_action_name: The full action name (e.g., "terminal.copy") to activate. + * + * Helper to create a #GtkMenuItem that, when activated, triggers a GAction + * on the widget. This is for compatibility where direct GAction use in menus + * might be problematic or for older GTK versions/styles. + */ +static void +_add_action_menu_item_compat(GtkMenuShell *menu_shell, + NemoTerminalWidget *self, + const gchar *label, + const gchar *detailed_action_name) +{ + GtkWidget *item = gtk_menu_item_new_with_label(label); + // Store necessary context on the GtkMenuItem itself for the callback. + g_object_set_data(G_OBJECT(item), "ntw-self", self); + g_object_set_data(G_OBJECT(item), "ntw-action-name", (gpointer)detailed_action_name); // Cast is okay for const gchar* + g_signal_connect(item, "activate", G_CALLBACK(_on_menu_item_activate_widget_action), NULL); // user_data for signal is NULL + gtk_menu_shell_append(menu_shell, item); +} + +/** + * _add_callback_menu_item: + * @menu_shell: The #GtkMenuShell to add the item to. + * @label: The translatable label for the menu item. + * @callback: The GCallback function to invoke on activation. + * @user_data: User data to pass to the callback. + * + * Helper to create a #GtkMenuItem that calls a specific C callback function + * when activated. + */ +static void +_add_callback_menu_item(GtkMenuShell *menu_shell, + const gchar *label, + GCallback callback, + gpointer user_data) +{ + GtkWidget *item = gtk_menu_item_new_with_label(label); + g_signal_connect(item, "activate", callback, user_data); + gtk_menu_shell_append(menu_shell, item); +} + +/** + * _add_radio_menu_item_with_data: + * @menu_shell: The #GtkMenuShell to add the item to. + * @radio_group: Pointer to the GSList representing the radio group. + * @label: The translatable label for the menu item. + * @data_key: The key for attaching data_value to the item. + * @data_value: The data to associate with the menu item (e.g., an enum or string). + * @is_active: Whether this radio item should be initially active. + * @activate_callback: The GCallback function to invoke on activation. + * @user_data: User data to pass to the callback. + * + * Helper to create a #GtkRadioMenuItem, associate data with it, + * and connect its "activate" signal. + * + * Returns: The created #GtkWidget (the radio menu item). + */ +static GtkWidget * +_add_radio_menu_item_with_data(GtkMenuShell *menu_shell, + GSList **radio_group, + const gchar *label, + const gchar *data_key, + gpointer data_value, + gboolean is_active, + GCallback activate_callback, + gpointer user_data) +{ + GtkWidget *item = gtk_radio_menu_item_new_with_label(*radio_group, label); + if (*radio_group == NULL) { // First item in the group + *radio_group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item)); + } + + g_object_set_data(G_OBJECT(item), data_key, data_value); + + if (is_active) { + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE); + } + g_signal_connect(item, "activate", activate_callback, user_data); + gtk_menu_shell_append(menu_shell, item); + return item; +} + +/** + * create_terminal_popup_menu: + * @self: The #NemoTerminalWidget instance. + * + * Creates and populates the context menu for the terminal widget. + * Includes options for copy/paste, color schemes, font sizes, + * synchronization modes, and SSH connection management. + * + * Returns: A new #GtkWidget (the #GtkMenu). The caller does not own the GtkWidget. + * The menu will be shown via gtk_menu_popup. + */ +static GtkWidget * +create_terminal_popup_menu(NemoTerminalWidget *self) +{ + GtkWidget *menu, *menu_item, *submenu; + gboolean is_sftp_location = FALSE; + g_autofree gchar *current_uri = NULL; + + menu = gtk_menu_new(); + + /* Standard Edit Actions: Copy, Paste, Select All */ + _add_action_menu_item_compat(GTK_MENU_SHELL(menu), self, _("Copy"), "terminal.copy"); + _add_action_menu_item_compat(GTK_MENU_SHELL(menu), self, _("Paste"), "terminal.paste"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + _add_action_menu_item_compat(GTK_MENU_SHELL(menu), self, _("Select All"), "terminal.select-all"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + + /* Color Scheme Submenu */ + menu_item = gtk_menu_item_new_with_label(_("Color Scheme")); + submenu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + + GSList *color_scheme_radio_group = NULL; + const gchar *current_scheme_val = nemo_terminal_widget_get_color_scheme(self); + for (gsize i = 0; i < G_N_ELEMENTS(COLOR_SCHEME_ENTRIES); ++i) { + _add_radio_menu_item_with_data(GTK_MENU_SHELL(submenu), &color_scheme_radio_group, + _(COLOR_SCHEME_ENTRIES[i].label_pot), + DATA_KEY_SCHEME_NAME, (gpointer)COLOR_SCHEME_ENTRIES[i].id, + (g_strcmp0(current_scheme_val, COLOR_SCHEME_ENTRIES[i].id) == 0), + G_CALLBACK(on_color_scheme_changed), self); + } + + /* Font Size Submenu */ + menu_item = gtk_menu_item_new_with_label(_("Font Size")); + submenu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + + // Get current font size to pre-select the closest one in the menu + g_autoptr(PangoFontDescription) current_font_desc = pango_font_description_copy( + vte_terminal_get_font(self->terminal)); + int current_size_pts = pango_font_description_get_size(current_font_desc) / PANGO_SCALE; + + // Find the closest predefined font size to the current one + int closest_size_idx = 0; + if (G_N_ELEMENTS(FONT_SIZE_ENTRIES) > 0) { + int min_diff = abs(FONT_SIZE_ENTRIES[0].size_pts - current_size_pts); + for (gsize i = 1; i < G_N_ELEMENTS(FONT_SIZE_ENTRIES); i++) { + int diff = abs(FONT_SIZE_ENTRIES[i].size_pts - current_size_pts); + if (diff < min_diff) { + min_diff = diff; + closest_size_idx = i; + } + } + } + + GSList *font_size_radio_group = NULL; + for (gsize i = 0; i < G_N_ELEMENTS(FONT_SIZE_ENTRIES); ++i) { + g_autofree gchar *label = g_strdup_printf("%d", FONT_SIZE_ENTRIES[i].size_pts); + _add_radio_menu_item_with_data(GTK_MENU_SHELL(submenu), &font_size_radio_group, + label, DATA_KEY_FONT_SIZE, + GINT_TO_POINTER(FONT_SIZE_ENTRIES[i].size_pts), + (i == closest_size_idx), // Check if this is the closest size + G_CALLBACK(on_font_size_changed), self); + } + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + + // Determine if the current location is an SFTP path + if (self->current_location != NULL && G_IS_FILE(self->current_location)) { + current_uri = g_file_get_uri(self->current_location); + if (current_uri && g_str_has_prefix(current_uri, "sftp://")) { + is_sftp_location = TRUE; + } + } else if (self->current_location != NULL) { + // This case should ideally not happen if current_location is always GFile or NULL + g_warning("self->current_location is not a GFile in create_terminal_popup_menu"); + } + + /* Local Folder Sync Submenu (only shown if not in SSH mode) */ + if (!self->in_ssh_mode) { + menu_item = gtk_menu_item_new_with_label(_("Local Folder Sync")); + GtkWidget *local_sync_submenu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), local_sync_submenu); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + + GSList *local_sync_radio_group = NULL; + for (gsize i = 0; i < G_N_ELEMENTS(LOCAL_SYNC_MODE_ENTRIES); ++i) { + _add_radio_menu_item_with_data(GTK_MENU_SHELL(local_sync_submenu), &local_sync_radio_group, + _(LOCAL_SYNC_MODE_ENTRIES[i].label_pot), + DATA_KEY_LOCAL_SYNC_MODE, GINT_TO_POINTER(LOCAL_SYNC_MODE_ENTRIES[i].mode), + (self->local_sync_mode == LOCAL_SYNC_MODE_ENTRIES[i].mode), + G_CALLBACK(on_local_sync_mode_changed), self); + } + } + + /* SSH Auto-Connect Submenu (only shown if not in SSH mode) */ + if (!self->in_ssh_mode) { + menu_item = gtk_menu_item_new_with_label(_("SSH Auto-Connect")); + GtkWidget *sftp_auto_connect_submenu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), sftp_auto_connect_submenu); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + + GSList *sftp_auto_radio_group = NULL; + for (gsize i = 0; i < G_N_ELEMENTS(SFTP_AUTO_CONNECT_ENTRIES); ++i) { + g_autofree gchar *label = g_strdup(_(SFTP_AUTO_CONNECT_ENTRIES[i].label_pot)); + GtkWidget *auto_item_widget = _add_radio_menu_item_with_data( + GTK_MENU_SHELL(sftp_auto_connect_submenu), &sftp_auto_radio_group, + label, DATA_KEY_SFTP_AUTO_CONNECT_MODE, + GINT_TO_POINTER(SFTP_AUTO_CONNECT_ENTRIES[i].mode), + (self->ssh_auto_connect_mode == SFTP_AUTO_CONNECT_ENTRIES[i].mode), + G_CALLBACK(on_sftp_auto_connect_behavior_changed), self); + + // If current location is SFTP, attach its details to the auto-connect menu item + // This allows immediate connection if an auto-connect option is chosen. + if (is_sftp_location && self->current_location && G_IS_FILE(self->current_location) && auto_item_widget) { + gchar *h = NULL, *u = NULL, *p = NULL; + if (parse_gvfs_ssh_path(self->current_location, &h, &u, &p)) { + g_object_set_data_full(G_OBJECT(auto_item_widget), DATA_KEY_SSH_HOSTNAME, h, (GDestroyNotify)g_free); + g_object_set_data_full(G_OBJECT(auto_item_widget), DATA_KEY_SSH_USERNAME, u, (GDestroyNotify)g_free); + g_object_set_data_full(G_OBJECT(auto_item_widget), DATA_KEY_SSH_PORT, p, (GDestroyNotify)g_free); + } else { + // Free if parse_gvfs_ssh_path allocated them but returned FALSE + g_free(h); g_free(u); g_free(p); + } + } + } + } + + /* SSH Connection Management: Disconnect (if in SSH) or Manual Connect (if on SFTP path) */ + if (self->in_ssh_mode) { + // Option to disconnect from the current SSH session + _add_callback_menu_item(GTK_MENU_SHELL(menu), _("Disconnect from SSH"), + G_CALLBACK(on_ssh_exit_activate), self); + } else if (is_sftp_location && self->current_location && G_IS_FILE(self->current_location)) { + // Option to manually connect to the current SFTP location via SSH + gchar *hostname = NULL, *username = NULL, *port = NULL; + gboolean can_connect_ssh = parse_gvfs_ssh_path(self->current_location, &hostname, &username, &port); + + if (can_connect_ssh && hostname != NULL) { + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + + // Build a descriptive label for the SSH connection submenu + g_autofree GString *label_gstr = g_string_new(_("SSH Connection to ")); + if (username != NULL && *username != '\0') { + g_string_append_printf(label_gstr, "%s@", username); + } + g_string_append(label_gstr, hostname); + if (port != NULL && *port != '\0') { + g_string_append_printf(label_gstr, ":%s", port); + } + + menu_item = gtk_menu_item_new_with_label(label_gstr->str); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + + // Submenu for choosing sync mode for manual SSH connection + GtkWidget *ssh_manual_connect_menu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), ssh_manual_connect_menu); + + for (gsize i = 0; i < G_N_ELEMENTS(MANUAL_SSH_SYNC_ENTRIES); ++i) { + GtkWidget *sync_item = gtk_menu_item_new_with_label(_(MANUAL_SSH_SYNC_ENTRIES[i].label_pot)); + // Store SSH details on the menu item for the callback + g_object_set_data_full(G_OBJECT(sync_item), DATA_KEY_SSH_HOSTNAME, g_strdup(hostname), (GDestroyNotify)g_free); + g_object_set_data_full(G_OBJECT(sync_item), DATA_KEY_SSH_USERNAME, g_strdup(username), (GDestroyNotify)g_free); + g_object_set_data_full(G_OBJECT(sync_item), DATA_KEY_SSH_PORT, g_strdup(port), (GDestroyNotify)g_free); + g_object_set_data(G_OBJECT(sync_item), DATA_KEY_SSH_SYNC_MODE, GINT_TO_POINTER(MANUAL_SSH_SYNC_ENTRIES[i].mode)); + g_signal_connect(sync_item, "activate", G_CALLBACK(on_ssh_connect_activate), self); + gtk_menu_shell_append(GTK_MENU_SHELL(ssh_manual_connect_menu), sync_item); + } + } + g_free(hostname); g_free(username); g_free(port); + } + + gtk_widget_show_all(menu); + return menu; +} + + +/** + * change_directory_in_terminal: + * @self: The #NemoTerminalWidget instance. + * @location: The #GFile representing the new directory. + * + * Changes the current working directory in the VTE terminal to match the + * provided @location. This function respects the configured synchronization + * mode (local or SSH) and only performs the `cd` if synchronization from + * File Manager to Terminal is enabled. + */ +static void +change_directory_in_terminal(NemoTerminalWidget *self, GFile *location) +{ + g_autofree char *path = NULL; + gboolean should_sync = TRUE; // Assume sync unless checks determine otherwise + + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + g_return_if_fail(location != NULL && G_IS_FILE(location)); + + /* Determine if a 'cd' command should be sent based on current mode and sync settings */ + if (self->in_ssh_mode) + { + // In SSH mode, sync from FM to Terminal requires SYNC_BOTH or SYNC_FM_TO_TERM + if (self->ssh_sync_mode != NEMO_TERMINAL_SYNC_BOTH && + self->ssh_sync_mode != NEMO_TERMINAL_SYNC_FM_TO_TERM) + { + should_sync = FALSE; + } + + if (should_sync) + { + // For SSH, get the remote path from the SFTP GFile + path = get_remote_path_from_sftp_gfile(location); + if (!path) + { + g_warning("Failed to get remote path for SSH cd from GFile URI: %s", + g_file_peek_path(location) ? g_file_peek_path(location) : "(unknown)"); + } + else if (path[0] == '\0') // Empty path typically means root + { + g_free(path); + path = g_strdup("/"); + } + } + } + else // Not in SSH mode (local terminal) + { + // For local terminal, sync from FM to Terminal requires SYNC_BOTH or SYNC_FM_TO_TERM + if (self->local_sync_mode != NEMO_TERMINAL_SYNC_BOTH && + self->local_sync_mode != NEMO_TERMINAL_SYNC_FM_TO_TERM) + { + should_sync = FALSE; + } + } + + if (!should_sync) + { + return; // Sync not enabled for this direction + } + + // If not in SSH mode and sync is enabled, get the local file path + if (!self->in_ssh_mode) // Implies should_sync is TRUE here for local + { + if (g_file_query_exists(location, NULL)) + { + path = g_file_get_path(location); + } + else + { + // Target location doesn't exist, warn and abort cd + g_autofree gchar *uri_for_warning = g_file_get_uri(location); + g_warning("Target local location %s for cd no longer exists. Aborting cd.", + uri_for_warning ? uri_for_warning : "(unknown URI)"); + path = NULL; // Ensure path is NULL so no cd command is fed + } + } + + // If a valid path was determined, feed the cd command + if (path != NULL && *path != '\0') // Ensure path is not NULL or empty + { + // Tell terminal to ignore its own "directory-changed" signal for this explicit cd + self->ignore_next_terminal_cd_signal = TRUE; + feed_cd_command(VTE_TERMINAL(self->terminal), path); + } + else if (should_sync) // Path is NULL but we intended to sync + { + g_warning("Path for cd command is NULL, aborting cd. Location: %s", + g_file_peek_path(location) ? g_file_peek_path(location) : "(unknown)"); + } +} + +/** + * get_remote_path_from_sftp_gfile: + * @location: A #GFile, presumably an SFTP location. + * + * Extracts the absolute remote path from an SFTP #GFile. + * It first tries to parse the URI (e.g., "sftp://host/path"). + * As a fallback for GVFS-mounted SFTP locations that might appear as local + * file paths (e.g., "/run/user/UID/gvfs/sftp:host=.../remote/path"), + * it attempts to parse these paths. + * + * Returns: A newly allocated string containing the remote path (e.g., "/home/user/docs"), + * or "/" if the path component is empty. Returns %NULL on failure. + * The caller must free the returned string. + */ +static gchar * +get_remote_path_from_sftp_gfile(GFile *location) +{ + g_return_val_if_fail(G_IS_FILE(location), NULL); + + gchar *remote_path = NULL; + g_autofree gchar *uri = g_file_get_uri(location); + + if (uri && g_str_has_prefix(uri, "sftp://")) + { + // Unescape the URI to handle special characters in path components + g_autofree gchar *decoded_uri = g_uri_unescape_string(uri, NULL); + if (decoded_uri) { + // Find the path part: sftp://[userinfo@]host[:port]/path + // Add 7 to skip "sftp://". + const char *host_part_end = strstr(decoded_uri + 7, "/"); + if (host_part_end) { // Found a '/' after the host part + remote_path = g_strdup(host_part_end); + } else { // No path component after host, implies root directory on server + remote_path = g_strdup("/"); + } + } + } + else // Fallback: Try to parse as a GVFS local mount path for SFTP + { + g_autofree gchar *path = g_file_get_path(location); + // Example GVFS path: /run/user/1000/gvfs/sftp:host=example.com,user=myuser/actual/remote/path + // We need to extract "/actual/remote/path" + if (path && g_str_has_prefix(path, "/run/user/") && strstr(path, "/gvfs/sftp:host=")) + { + // Find the start of the GVFS SFTP details part + char *sftp_details_part = strstr(path, "/gvfs/sftp:host="); + if (sftp_details_part) + { + // The actual remote path starts after the GVFS connection string part. + // e.g. "sftp:host=...,user=..." or "sftp:host=..." + // The first '/' after this connection string segment marks the start of the remote path. + char *path_start = strchr(sftp_details_part + strlen("/gvfs/"), '/'); // Search for '/' after "/gvfs/" + if (path_start) + { + remote_path = g_strdup(path_start); + } + else // No further '/' means it's the root of the SFTP mount + { + remote_path = g_strdup("/"); + } + } + } + } + return remote_path; +} + +/** + * setup_terminal_font: + * @terminal: The #VteTerminal widget. + * + * Configures the font for the VTE terminal. It uses the system's monospace + * font setting ("org.gnome.desktop.interface monospace-font-name") and + * a font size retrieved via `nemo_terminal_get_font_size()`. + */ +static void +setup_terminal_font(VteTerminal *terminal) +{ + g_autoptr(PangoFontDescription) font_desc = NULL; + g_autoptr(GSettings) interface_settings = NULL; + g_autofree gchar *font_name = NULL; + int font_size_pts; + + // Get system monospace font name + interface_settings = g_settings_new("org.gnome.desktop.interface"); + font_name = g_settings_get_string(interface_settings, "monospace-font-name"); + + // Get saved/default font size for the terminal + font_size_pts = nemo_terminal_get_font_size(); + + if (font_name && *font_name) + { + font_desc = pango_font_description_from_string(font_name); + } + else // Fallback if system font name is not set + { + font_desc = pango_font_description_new(); + pango_font_description_set_family(font_desc, "Monospace"); // Default to generic "Monospace" + } + + pango_font_description_set_size(font_desc, font_size_pts * PANGO_SCALE); + vte_terminal_set_font(terminal, font_desc); + // VTE terminal takes its own copy of font_desc, so we can free ours. +} + +/** + * focus_once_and_remove: + * @user_data: The #GtkWidget (VTE terminal) to focus. + * + * Idle callback to grab focus for the terminal widget. + * Removes itself after execution. Used to ensure focus is set + * after other UI events might have settled. + * + * Returns: %G_SOURCE_REMOVE to ensure it runs only once. + */ +static gboolean +focus_once_and_remove(gpointer user_data) +{ + GtkWidget *widget_to_focus = GTK_WIDGET(user_data); + NemoTerminalWidget *self; + + if (GTK_IS_WIDGET(widget_to_focus) && gtk_widget_get_window(widget_to_focus)) { // Ensure widget is realized + gtk_widget_grab_focus(widget_to_focus); + } + + // Clear the timeout ID from the parent NemoTerminalWidget + self = NEMO_TERMINAL_WIDGET(gtk_widget_get_ancestor(widget_to_focus, NEMO_TYPE_TERMINAL_WIDGET)); + if (self && self->focus_timeout_id > 0) // Check if self is valid and ID matches + { + // This function is called by the timeout, so we can't remove by ID here. + // Instead, the source removes itself. We just clear our record. + self->focus_timeout_id = 0; + } + return G_SOURCE_REMOVE; +} + +/** + * reset_toggling_flag: + * @user_data: The #NemoTerminalWidget instance. + * + * Timeout callback to reset the `in_toggling` flag. This acts as a + * debounce mechanism for visibility toggling actions. + * + * Returns: %G_SOURCE_REMOVE to ensure it runs only once. + */ +static gboolean +reset_toggling_flag(gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + if (NEMO_IS_TERMINAL_WIDGET(self)) { + self->in_toggling = FALSE; + } + return G_SOURCE_REMOVE; +} + +/** + * apply_initial_size_idle: + * @user_data: The #NemoTerminalWidget instance. + * + * Idle callback to apply the terminal's saved height. This is typically + * called after the widget and its container paned are realized, ensuring + * dimensions are available. + * + * Returns: %G_SOURCE_REMOVE to ensure it runs only once. + */ +static gboolean +apply_initial_size_idle(gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + if (NEMO_IS_TERMINAL_WIDGET(self)) { + nemo_terminal_widget_apply_new_size(self); + } + return G_SOURCE_REMOVE; +} + +/** + * on_terminal_key_press: + * @widget: The #VteTerminal widget where the key press occurred. + * @event: The #GdkEventKey for the key press. + * @user_data: The #NemoTerminalWidget instance. + * + * Handles key press events within the terminal, primarily for implementing + * custom keyboard shortcuts (e.g., F4 to toggle visibility, Ctrl+Shift+C/V/A + * for copy/paste/select all, Ctrl+Shift+S for SSH connect). + * + * Returns: %TRUE if the event was handled, %FALSE otherwise. + */ +static gboolean +on_terminal_key_press(GtkWidget *widget, + GdkEventKey *event, + gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + guint keyval = event->keyval; + GdkModifierType state = event->state; // Use GdkEventKey->state for modifiers + + /* F4: Toggle terminal visibility */ + if (keyval == GDK_KEY_F4) + { + nemo_terminal_widget_toggle_visible(self); + return TRUE; // Event handled + } + + /* Standard terminal shortcuts (Ctrl+Shift+Letter) */ + if ((state & GDK_CONTROL_MASK) && (state & GDK_SHIFT_MASK)) + { + switch (keyval) + { + case GDK_KEY_C: // Ctrl+Shift+C for Copy + case GDK_KEY_c: + vte_terminal_copy_clipboard_format(self->terminal, VTE_FORMAT_TEXT); + return TRUE; + + case GDK_KEY_V: // Ctrl+Shift+V for Paste + case GDK_KEY_v: + vte_terminal_paste_clipboard(self->terminal); + return TRUE; + + case GDK_KEY_A: // Ctrl+Shift+A for Select All + case GDK_KEY_a: + vte_terminal_select_all(self->terminal); + return TRUE; + + case GDK_KEY_S: // Ctrl+Shift+S for SSH connect (if on SFTP path) + case GDK_KEY_s: + if (self->current_location && G_IS_FILE(self->current_location) && !self->in_ssh_mode) + { + g_autofree gchar *hostname = NULL; + g_autofree gchar *username = NULL; + g_autofree gchar *port = NULL; + + if (parse_gvfs_ssh_path(self->current_location, &hostname, &username, &port)) + { + // Default to SYNC_BOTH for keyboard shortcut initiated connections + _initiate_ssh_connection(self, hostname, username, port, NEMO_TERMINAL_SYNC_BOTH); + // hostname, username, port are freed by g_autofree + return TRUE; + } + } + break; + } + } + return FALSE; // Event not handled by this function +} + +/** + * on_terminal_directory_changed: + * @terminal: The #VteTerminal whose directory changed. + * @user_data: The #NemoTerminalWidget instance. + * + * Callback for VTE's "current-directory-uri-changed" (or equivalent) signal. + * When the terminal's CWD changes (e.g., user types `cd`), this function + * updates the file manager's location if synchronization from + * Terminal to File Manager is enabled. + */ +static void +on_terminal_directory_changed(VteTerminal *terminal, + gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + const char *cwd_uri = vte_terminal_get_current_directory_uri(terminal); + g_autoptr(GFile) new_gfile_location = NULL; + gboolean should_sync_to_fm = TRUE; // Assume sync unless checks determine otherwise + + if (!cwd_uri) return; // No CWD URI available + + // If ignore_next_terminal_cd_signal is set, it means this change was + // programmatically triggered (e.g., by FM changing location). + // We should update our internal current_location if it's a local shell + // to reflect this, but not sync back to FM. + if (self->ignore_next_terminal_cd_signal) { + self->ignore_next_terminal_cd_signal = FALSE; + if (!self->in_ssh_mode) { // Only update self->current_location for local shell initial cd + g_autoptr(GFile) temp_gfile = g_file_new_for_uri(cwd_uri); + if (temp_gfile) { + if (!self->current_location || !g_file_equal(temp_gfile, self->current_location)) { + g_set_object(&self->current_location, temp_gfile); + // No g_signal_emit here, as this is an internal sync from an explicit cd. + } + } + } + return; + } + + /* Determine if a sync to File Manager should occur */ + if (self->in_ssh_mode) + { + // In SSH mode, sync from Term to FM requires SYNC_BOTH or SYNC_TERM_TO_FM + if (self->ssh_sync_mode != NEMO_TERMINAL_SYNC_BOTH && + self->ssh_sync_mode != NEMO_TERMINAL_SYNC_TERM_TO_FM) + { + should_sync_to_fm = FALSE; + } + + if (should_sync_to_fm && g_str_has_prefix(cwd_uri, "file://")) // VTE gives local file:// URI + { + // Convert local path from terminal (e.g., /home/user) to an SFTP URI + g_autofree gchar *local_path_from_uri = g_filename_from_uri(cwd_uri, NULL, NULL); + if (local_path_from_uri && self->ssh_hostname) // Need hostname to build SFTP URI + { + g_autofree GString *sftp_uri_builder = g_string_new("sftp://"); + if (self->ssh_username && *self->ssh_username) { + g_string_append_printf(sftp_uri_builder, "%s@", self->ssh_username); + } + g_string_append(sftp_uri_builder, self->ssh_hostname); + if (self->ssh_port && *self->ssh_port) { + g_string_append_printf(sftp_uri_builder, ":%s", self->ssh_port); + } + // Ensure path starts with '/', g_filename_from_uri should provide absolute path + g_string_append(sftp_uri_builder, local_path_from_uri); + new_gfile_location = g_file_new_for_uri(sftp_uri_builder->str); + } + } + } + else // Not in SSH mode (local terminal) + { + // For local terminal, sync from Term to FM requires SYNC_BOTH or SYNC_TERM_TO_FM + if (self->local_sync_mode != NEMO_TERMINAL_SYNC_BOTH && + self->local_sync_mode != NEMO_TERMINAL_SYNC_TERM_TO_FM) + { + should_sync_to_fm = FALSE; + } + if (should_sync_to_fm) + { + new_gfile_location = g_file_new_for_uri(cwd_uri); + } + } + + if (!should_sync_to_fm || !new_gfile_location) + { + return; // Sync not enabled, or failed to create GFile for the new location + } + + // If the new location is different from the current one, update and emit signal + if (!self->current_location || !g_file_equal(new_gfile_location, self->current_location)) + { + g_set_object(&self->current_location, new_gfile_location); // Updates ref count + g_signal_emit_by_name(self, "change-directory", new_gfile_location); + + // If terminal had focus, try to maintain it after the directory change potentially re-renders UI + if (self->maintain_focus && gtk_widget_has_focus(GTK_WIDGET(self->terminal))) + { + if (self->focus_timeout_id > 0) // Cancel any pending focus attempt + { + g_source_remove(self->focus_timeout_id); + } + // Schedule a new focus attempt + self->focus_timeout_id = g_timeout_add(50, focus_once_and_remove, GTK_WIDGET(self->terminal)); + } + } + // new_gfile_location is unref'd by g_autoptr when it goes out of scope +} + +/** + * on_terminal_child_exited: + * @terminal: The #VteTerminal whose child process exited. + * @status: The exit status of the child process. + * @user_data: The #NemoTerminalWidget instance. + * + * Callback for VTE's "child-exited" signal. + * SSH session termination (e.g., connection drop, remote exit) and + * local shell exits. + * If an SSH session ends unexpectedly, it resets to a local terminal. + * If a local shell exits, it may respawn based on visibility. + */ +static void +on_terminal_child_exited(VteTerminal *terminal, + gint status, + gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + + // If we are explicitly exiting SSH (e.g., user clicked "Disconnect"), + // on_ssh_exit_activate handles the reset. Avoid double reset. + if (self->is_exiting_ssh) { + return; + } + + if (self->in_ssh_mode) { + // The shell process hosting the SSH client has exited. This could be due to + // network issues, the SSH client itself terminating, or the remote end closing. + g_warning("Shell hosting SSH session exited unexpectedly (status %d). Resetting to local terminal.", status); + self->is_exiting_ssh = TRUE; // Prevent re-entry during reset + reset_terminal_to_current_location(self); // Clears SSH state, spawns local shell + self->is_exiting_ssh = FALSE; + } else { + // Local shell exited. + // Respawn if the terminal widget is part of a window and is currently visible, + // or mark for respawn if it's hidden. + if (GTK_IS_WIDGET(self) && gtk_widget_get_ancestor(GTK_WIDGET(self), GTK_TYPE_WINDOW)) { + if (self->is_visible) { + spawn_terminal_in_widget(self); // Respawn local shell + } else { + self->needs_respawn = TRUE; // Mark to respawn when next shown + } + } + } +} + +/** + * on_container_size_changed: + * @paned: The #GtkPaned widget whose position (divider) changed. + * @pspec: The #GParamSpec of the property that changed (unused). + * @user_data: The #NemoTerminalWidget instance. + * + * Callback for the "notify::position" signal on the GtkPaned that + * contains the terminal. Saves the new height of the terminal pane + * when the user resizes it. + */ +static void +on_container_size_changed(GtkPaned *paned, + GParamSpec *pspec, + gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + // Basic validation + if (!self || !NEMO_IS_TERMINAL_WIDGET(self) || !GTK_IS_PANED(paned)) return; + if (!gtk_widget_get_realized(GTK_WIDGET(paned))) return; // Avoid acting on unrealized widgets + + int position = gtk_paned_get_position(paned); // Position of the divider + int total_height = gtk_widget_get_allocated_height(GTK_WIDGET(paned)); + + if (total_height <= 0) return; // Avoid division by zero or negative heights + + // For a GtkPaned with vertical orientation: + // Child1 (top) height = position + // Child2 (bottom, our terminal) height = total_height - position + int terminal_height = total_height - position; + + nemo_terminal_widget_save_height(self, terminal_height); +} + +/** + * on_terminal_button_press: + * @widget: The #VteTerminal widget where the button press occurred. + * @event: The #GdkEventButton for the button press. + * @user_data: The #NemoTerminalWidget instance. + * + * Handles button press events in the terminal, primarily to show the + * context menu on a secondary (right) click. + * + * Returns: %TRUE if the event was handled (menu shown), %FALSE otherwise. + */ +static gboolean +on_terminal_button_press(GtkWidget *widget, + GdkEventButton *event, + gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + + // Show context menu on right-click (button 3, or secondary button) + if (event->button == GDK_BUTTON_SECONDARY && event->type == GDK_BUTTON_PRESS) + { + GtkWidget *menu = create_terminal_popup_menu(self); + gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, + event->button, event->time); + return TRUE; + } + return FALSE; +} + +/** + * on_copy_activate: + * @action: The "copy" #GSimpleAction that was activated. + * @parameter: (Unused) Parameters for the action. + * @user_data: The #NemoTerminalWidget instance. + * + * Action handler for "copy". Copies selected text from the terminal to the clipboard. + */ +static void +on_copy_activate(GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + vte_terminal_copy_clipboard_format(VTE_TERMINAL(self->terminal), VTE_FORMAT_TEXT); +} + +/** + * on_paste_activate: + * @action: The "paste" #GSimpleAction that was activated. + * @parameter: (Unused) Parameters for the action. + * @user_data: The #NemoTerminalWidget instance. + * + * Action handler for "paste". Pastes text from the clipboard into the terminal. + */ +static void +on_paste_activate(GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + vte_terminal_paste_clipboard(self->terminal); +} + +/** + * on_select_all_activate: + * @action: The "select-all" #GSimpleAction that was activated. + * @parameter: (Unused) Parameters for the action. + * @user_data: The #NemoTerminalWidget instance. + * + * Action handler for "select-all". Selects all text in the terminal. + */ +static void +on_select_all_activate(GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + vte_terminal_select_all(self->terminal); +} + +/** + * on_font_size_changed: + * @widget: The #GtkRadioMenuItem for font size that was activated. + * @user_data: The #NemoTerminalWidget instance. + * + * Callback when a font size is selected from the context menu. + * Updates the terminal's font size and saves the setting. + */ +static void +on_font_size_changed(GtkWidget *widget, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + + // Only act if the radio item is being activated (not deactivated) + if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget))) + return; + + gpointer size_data = g_object_get_data(G_OBJECT(widget), DATA_KEY_FONT_SIZE); + if (size_data != NULL) + { + int font_size_pts = GPOINTER_TO_INT(size_data); + + g_autoptr(PangoFontDescription) font_desc = pango_font_description_copy( + vte_terminal_get_font(self->terminal)); + + pango_font_description_set_size(font_desc, font_size_pts * PANGO_SCALE); + vte_terminal_set_font(self->terminal, font_desc); + nemo_terminal_widget_save_font_size(self, font_size_pts); // Save the setting + } +} + +/** + * on_color_scheme_changed: + * @widget: The #GtkRadioMenuItem for color scheme that was activated. + * @user_data: The #NemoTerminalWidget instance. + * + * Callback when a color scheme is selected from the context menu. + * Sets the new color scheme for the terminal. + */ +static void +on_color_scheme_changed(GtkWidget *widget, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + + if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget))) + return; + + const gchar *scheme_name = g_object_get_data(G_OBJECT(widget), DATA_KEY_SCHEME_NAME); + if (scheme_name != NULL) + { + nemo_terminal_widget_set_color_scheme(self, scheme_name); + } +} + +/** + * on_local_sync_mode_changed: + * @widget: The #GtkRadioMenuItem for local sync mode that was activated. + * @user_data: The #NemoTerminalWidget instance. + * + * Callback when the local folder synchronization mode is changed via the menu. + * Updates the widget's state, saves the setting, and may respawn the terminal + * to apply new PROMPT_COMMAND if needed. + */ +static void +on_local_sync_mode_changed(GtkWidget *widget, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + + if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget))) + return; + + gpointer mode_data = g_object_get_data(G_OBJECT(widget), DATA_KEY_LOCAL_SYNC_MODE); + NemoTerminalSyncMode new_mode = GPOINTER_TO_INT(mode_data); + + if (self->local_sync_mode == new_mode) return; // No change + + self->local_sync_mode = new_mode; + g_settings_set_enum(nemo_window_state, "local-terminal-sync-mode", new_mode); + + // If not in SSH mode, changing local sync settings might require respawning + // the terminal to update PROMPT_COMMAND for OSC7. + if (!self->in_ssh_mode) + { + // Respawn to ensure PROMPT_COMMAND is correctly set/unset for OSC7. + // This provides immediate feedback of the new sync mode. + spawn_terminal_in_widget(self); + } +} + +/** + * on_sftp_auto_connect_behavior_changed: + * @widget: The #GtkRadioMenuItem for SSH auto-connect behavior that was activated. + * @user_data: The #NemoTerminalWidget instance. + * + * Callback when the SFTP/SSH auto-connect behavior is changed via the menu. + * Updates widget state, saves the setting, and may initiate an SSH connection + * if an auto-connect option is chosen and the current location is SFTP. + */ +static void +on_sftp_auto_connect_behavior_changed(GtkWidget *widget, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + + if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget))) + return; + + gpointer mode_data = g_object_get_data(G_OBJECT(widget), DATA_KEY_SFTP_AUTO_CONNECT_MODE); + NemoTerminalSshAutoConnectMode new_auto_mode = GPOINTER_TO_INT(mode_data); + + if (self->ssh_auto_connect_mode == new_auto_mode) return; // No change + + self->ssh_auto_connect_mode = new_auto_mode; + g_settings_set_enum(nemo_window_state, "ssh-terminal-auto-connect-mode", new_auto_mode); + + // If an auto-connect option was selected (not "OFF") and we are not already in SSH: + if (new_auto_mode != NEMO_TERMINAL_SSH_AUTOCONNECT_OFF && !self->in_ssh_mode) + { + // Retrieve SSH connection details stored on the menu item. + // These would have been populated if the current location was SFTP when the menu was built. + const gchar *hostname = g_object_get_data(G_OBJECT(widget), DATA_KEY_SSH_HOSTNAME); + const gchar *username = g_object_get_data(G_OBJECT(widget), DATA_KEY_SSH_USERNAME); + const gchar *port = g_object_get_data(G_OBJECT(widget), DATA_KEY_SSH_PORT); + + if (hostname) // Hostname is essential for connection + { + NemoTerminalSyncMode sync_mode_for_connection; + // Determine the sync mode based on the chosen auto-connect behavior + switch (new_auto_mode) + { + case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH: + sync_mode_for_connection = NEMO_TERMINAL_SYNC_BOTH; + break; + case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM: + sync_mode_for_connection = NEMO_TERMINAL_SYNC_FM_TO_TERM; + break; + case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM: + sync_mode_for_connection = NEMO_TERMINAL_SYNC_TERM_TO_FM; + break; + case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_NONE: + sync_mode_for_connection = NEMO_TERMINAL_SYNC_NONE; + break; + default: // Should not happen + g_warning("Unexpected SSH auto-connect mode for immediate connection: %d", new_auto_mode); + return; + } + _initiate_ssh_connection(self, hostname, username, port, sync_mode_for_connection); + } + // If hostname is NULL, it implies the menu was likely opened on a non-SFTP path, + // so no immediate connection is attempted. The setting is saved for future SFTP navigation. + } +} + +/** + * nemo_terminal_widget_class_init: + * @klass: The #NemoTerminalWidgetClass to initialize. + * + * GObject class initialization function. Sets up signals and properties for the widget. + */ +static void +nemo_terminal_widget_class_init(NemoTerminalWidgetClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + GParamFlags flags; + + /* Signals */ + signals[CHANGE_DIRECTORY] = + g_signal_new("change-directory", // Signal name + G_TYPE_FROM_CLASS(klass), // Owner class type + G_SIGNAL_RUN_LAST, // Default emission stage + 0, // Class offset (0 for default) + NULL, NULL, // Accumulator and marshaller data + g_cclosure_marshal_VOID__OBJECT, // Default C marshaller (void function, object param) + G_TYPE_NONE, // Return type + 1, // Number of parameters + G_TYPE_FILE); // Parameter 1 type: GFile* + + signals[TOGGLE_VISIBILITY] = + g_signal_new("toggle-visibility", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, + g_cclosure_marshal_VOID__BOOLEAN, // Default C marshaller (void function, boolean param) + G_TYPE_NONE, + 1, + G_TYPE_BOOLEAN); // Parameter 1 type: gboolean + + /* Properties */ + flags = G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY; + + properties[PROP_CURRENT_LOCATION] = + g_param_spec_object("current-location", // Property name + "Current Location", // Nickname + "The GFile representing the current directory.", // Blurb + G_TYPE_FILE, // Property type + flags); // Property flags + + g_object_class_install_property(object_class, PROP_CURRENT_LOCATION, properties[PROP_CURRENT_LOCATION]); + + /* Override finalize method */ + object_class->finalize = nemo_terminal_widget_finalize; +} + +/** + * nemo_terminal_widget_init: + * @self: The #NemoTerminalWidget instance to initialize. + * + * GObject instance initialization function. Sets up the widget's internal + * structure, VTE terminal, default settings, and connects signals. + */ +static void +nemo_terminal_widget_init(NemoTerminalWidget *self) +{ + GtkStyleContext *context; + GtkCssProvider *provider; + + // Initialize widget members + self->scrolled_window = gtk_scrolled_window_new(NULL, NULL); + gtk_widget_set_vexpand(self->scrolled_window, TRUE); + gtk_widget_set_hexpand(self->scrolled_window, TRUE); + + self->terminal = VTE_TERMINAL(vte_terminal_new()); + + // SSH indicator label + self->ssh_indicator = gtk_label_new("SSH"); + gtk_widget_set_name(self->ssh_indicator, "ssh-indicator"); // For CSS styling + gtk_widget_set_no_show_all(self->ssh_indicator, TRUE); // Initially hidden + gtk_widget_hide(self->ssh_indicator); + gtk_widget_set_vexpand(self->ssh_indicator, FALSE); + gtk_widget_set_hexpand(self->ssh_indicator, TRUE); // Allow to expand horizontally + gtk_label_set_xalign(GTK_LABEL(self->ssh_indicator), 0.5); // Center the text + + // Apply CSS to SSH indicator + provider = gtk_css_provider_new(); + // Basic styling for the SSH indicator label + const char *css = "label#ssh-indicator { background-color: #3465a4; color: white; padding: 2px 5px; margin: 0; font-weight: bold; }"; + gtk_css_provider_load_from_data(provider, css, -1, NULL); + context = gtk_widget_get_style_context(self->ssh_indicator); + gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_USER); + g_object_unref(provider); // Provider is now managed by style context + + // Initialize state flags + self->is_exiting_ssh = FALSE; + self->ignore_next_terminal_cd_signal = FALSE; + self->container_paned = NULL; // Will be set when integrated into UI + self->is_visible = FALSE; // Assume initially not visible until ensure_state + self->needs_respawn = FALSE; + self->in_toggling = FALSE; + self->focus_timeout_id = 0; + self->maintain_focus = TRUE; // Default to maintaining focus + + // Configure VTE terminal properties + vte_terminal_set_scroll_on_output(self->terminal, FALSE); + vte_terminal_set_scroll_on_keystroke(self->terminal, TRUE); + vte_terminal_set_scrollback_lines(self->terminal, 10000); // Generous scrollback + vte_terminal_set_allow_bold(self->terminal, TRUE); + // vte_terminal_set_mouse_autohide(self->terminal, TRUE); // Optional: auto-hide mouse cursor + + setup_terminal_font(self->terminal); // Set font based on settings + // Color scheme will be applied after self->color_scheme is initialized from settings + // nemo_terminal_widget_apply_color_scheme(self); // Deferred until color_scheme is loaded + + // Connect VTE terminal signals + g_signal_connect(self->terminal, "child-exited", G_CALLBACK(on_terminal_child_exited), self); + g_signal_connect(self->terminal, "button-press-event", G_CALLBACK(on_terminal_button_press), self); + g_signal_connect(self->terminal, "contents-changed", G_CALLBACK(on_terminal_contents_changed), self); + + // VTE signal for directory change can have different names in different versions + if (g_signal_lookup("current-directory-uri-changed", VTE_TYPE_TERMINAL)) { + g_signal_connect(self->terminal, "current-directory-uri-changed", G_CALLBACK(on_terminal_directory_changed), self); + } else if (g_signal_lookup("directory-uri-changed", VTE_TYPE_TERMINAL)) { // Older VTE versions + g_signal_connect(self->terminal, "directory-uri-changed", G_CALLBACK(on_terminal_directory_changed), self); + } else { + g_warning("Could not find a suitable directory change signal for VteTerminal."); + } + + // Layout: VBox contains SSH indicator (optional) and ScrolledWindow (for terminal) + GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_box_pack_start(GTK_BOX(vbox), self->ssh_indicator, FALSE, FALSE, 0); // Indicator at the top, no expand + gtk_container_add(GTK_CONTAINER(self->scrolled_window), GTK_WIDGET(self->terminal)); + gtk_box_pack_start(GTK_BOX(vbox), self->scrolled_window, TRUE, TRUE, 0); // Scrolled window takes remaining space + + // Add the VBox to this NemoTerminalWidget (which is a GtkBox itself) + gtk_box_pack_start(GTK_BOX(self), vbox, TRUE, TRUE, 0); + + // Load initial settings for sync modes and color scheme + self->color_scheme = NULL; // Will be loaded by get_color_scheme on demand + nemo_terminal_widget_get_color_scheme(self); // Ensure it's loaded + nemo_terminal_widget_apply_color_scheme(self); // Apply the loaded scheme + + self->ssh_sync_mode = NEMO_TERMINAL_SYNC_NONE; // Default for new SSH sessions, can be overridden + self->ssh_auto_connect_mode = g_settings_get_enum(nemo_window_state, "ssh-terminal-auto-connect-mode"); + self->local_sync_mode = g_settings_get_enum(nemo_window_state, "local-terminal-sync-mode"); + + // Event handling for key presses (also on scrolled window for focus reasons) + gtk_widget_set_can_focus(GTK_WIDGET(self->terminal), TRUE); // VTE terminal itself should be focusable + gtk_widget_set_can_focus(self->scrolled_window, FALSE); // Scrolled window usually not directly focusable + // but key events might bubble. + // Connect key press to terminal primarily, and to self (the GtkBox) as a fallback if needed. + // Or, let key events propagate from terminal. This seems fine for now. + g_signal_connect(self->terminal, "key-press-event", G_CALLBACK(on_terminal_key_press), self); + // Scrolled window might also need to forward some key events if terminal doesn't get them. + // g_signal_connect(self->scrolled_window, "key-press-event", G_CALLBACK(on_terminal_key_press), self); + + // Setup GActionGroup for standard actions (copy, paste, etc.) + self->action_group = g_simple_action_group_new(); + g_action_map_add_action_entries(G_ACTION_MAP(self->action_group), + terminal_entries, + G_N_ELEMENTS(terminal_entries), + self); // User data for actions is self + gtk_widget_insert_action_group(GTK_WIDGET(self), "terminal", G_ACTION_GROUP(self->action_group)); + + gtk_widget_show_all(GTK_WIDGET(self)); // Show internal components + gtk_widget_hide(GTK_WIDGET(self)); // But hide the whole widget initially; visibility managed by ensure_state. +} + +/*** Public functions ***/ + +/** + * spawn_terminal_in_widget: + * @self: The #NemoTerminalWidget instance. + * + * Spawns a new shell process inside the VTE terminal widget. + * It determines the shell to use (from $SHELL or defaults), sets the + * working directory based on `self->current_location` (if local and exists), + * and configures `PROMPT_COMMAND` for OSC7 terminal-to-FM synchronization + * if enabled for local terminals. + */ +void +spawn_terminal_in_widget(NemoTerminalWidget *self) +{ + g_autofree char **env = NULL; + g_autoptr(GError) error = NULL; + const char *shell_executable; + g_autofree gchar *working_directory = NULL; + GPid child_pid; // VTE handles reaping this PID + + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + + self->needs_respawn = FALSE; // Reset flag as we are attempting to spawn + + // Determine shell executable + shell_executable = g_getenv("SHELL"); + if (shell_executable == NULL || *shell_executable == '\0') + { + // Fallback to common default shells + const char *default_shells[] = {"/bin/bash", "/bin/sh", NULL}; + for (int i = 0; default_shells[i]; ++i) { + if (g_file_test(default_shells[i], G_FILE_TEST_IS_EXECUTABLE)) { + shell_executable = default_shells[i]; + break; + } + } + if (shell_executable == NULL || *shell_executable == '\0') { + shell_executable = "/bin/sh"; // Ultimate fallback + g_warning("SHELL environment variable not set, and common shells not found. Defaulting to /bin/sh."); + } + } + + // Determine working directory (only for local, non-SSH spawns) + // For SSH, the remote shell starts in its default (e.g., home) or handled by ssh_remote_path. + if (!self->in_ssh_mode && self->current_location != NULL) + { + if (G_IS_FILE(self->current_location)) + { + // Only use native paths for local shell's CWD + if (g_file_is_native(self->current_location) && + g_file_query_exists(self->current_location, NULL)) + { + working_directory = g_file_get_path(self->current_location); + } + else if (!g_file_is_native(self->current_location)) { + // Current location is remote (e.g. sftp://), spawn local shell in home. + g_warning("Current location is remote (%s) but attempting to spawn local shell. Using home directory.", + g_file_get_uri_scheme(self->current_location)); + // working_directory remains NULL, VTE will use default (usually home) + } + else // Native path but doesn't exist + { + g_autofree gchar *uri_for_warning = g_file_get_uri(self->current_location); + g_warning("Current local location %s no longer exists. Spawning terminal in home directory.", + uri_for_warning ? uri_for_warning : "(unknown URI)"); + g_set_object(&self->current_location, NULL); // Reset invalid location + // working_directory remains NULL + } + } + else // self->current_location is not a GFile (should not happen if logic is correct) + { + g_warning("self->current_location is not a GFile in spawn_terminal_in_widget. Spawning terminal in home directory."); + g_set_object(&self->current_location, NULL); // Reset invalid location + } + } + + char *argv[] = {(char *)shell_executable, NULL}; // Arguments for the shell + + // Spawn the shell process in the VTE terminal + vte_terminal_spawn_sync(self->terminal, + VTE_PTY_DEFAULT, // PTY flags + working_directory, // Working directory (can be NULL for default) + argv, // Command and arguments + (char **)env, // Environment variables (can be NULL for current) + G_SPAWN_SEARCH_PATH | G_SPAWN_CHILD_INHERITS_STDIN, // Spawn flags + NULL, NULL, // Child setup function and data (unused) + &child_pid, // Returns child PID (unused by us directly) + NULL, // Cancellable (unused) + &error); // GError for reporting issues + + if (error != NULL) + { + g_warning("Failed to spawn terminal (shell: %s, wd: %s): %s", + shell_executable, working_directory ? working_directory : "(default)", error->message); + } + // env is freed by g_autofree +} + +/** + * nemo_terminal_widget_get_default_height: + * + * Retrieves the default/saved height for the terminal widget from GSettings. + * + * Returns: The terminal height in pixels. Defaults to 300 if setting is invalid or too small. + */ +int +nemo_terminal_widget_get_default_height(void) +{ + int saved_height = g_settings_get_int(nemo_window_state, "terminal-height"); + // Ensure a minimum sensible height + return (saved_height > 50 && saved_height < 8000) ? saved_height : 300; +} + +/** + * nemo_terminal_widget_save_height: + * @self: The #NemoTerminalWidget instance. + * @height: The height in pixels to save. + * + * Saves the terminal's height. Updates the internal `self->height` and + * persists the value to GSettings if it's within a reasonable range. + */ +void +nemo_terminal_widget_save_height(NemoTerminalWidget *self, int height) +{ + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + + // Save only if height is within a reasonable range to avoid extreme values + if (height > 50 && height < 8000) // Min 50px, Max 8000px (arbitrary upper limit) + { + if (self->height != height) { // Only save if changed + self->height = height; + g_settings_set_int(nemo_window_state, "terminal-height", height); + } + } +} + +/** + * nemo_terminal_widget_apply_new_size: + * @self: The #NemoTerminalWidget instance. + * + * Applies the currently stored `self->height` to the #GtkPaned container + * that holds the terminal. This adjusts the paned's divider position. + * Should be called when the terminal is visible and the paned is realized. + */ +void +nemo_terminal_widget_apply_new_size(NemoTerminalWidget *self) +{ + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + + if (!self->container_paned || !GTK_IS_PANED(self->container_paned) || + !gtk_widget_get_realized(GTK_WIDGET(self->container_paned))) + return; // Paned not set, not a paned, or not realized + + int total_height = gtk_widget_get_allocated_height(GTK_WIDGET(self->container_paned)); + if (total_height > 0 && self->height > 0) + { + // Calculate new paned divider position. + // Terminal is pack2 (bottom pane). Its height is `self->height`. + // Position = total_height - terminal_height. + int new_pos = total_height - self->height; + + // Clamp position to be valid: 0 <= new_pos <= total_height - min_terminal_height (e.g. 50) + if (new_pos < 0) new_pos = 0; + // Ensure terminal retains a minimum height (e.g., 50px) + if (new_pos > total_height - 50) new_pos = total_height - 50; + + if (new_pos >= 0 && new_pos <= total_height) { // Double check validity + gtk_paned_set_position(GTK_PANED(self->container_paned), new_pos); + } + } +} + +/** + * on_paned_destroy: + * @widget: The #GtkPaned widget that is being destroyed. + * @user_data: The #NemoTerminalWidget instance. + * + * Callback for the "destroy" signal of the container paned. + * Clears the `self->container_paned` reference in the terminal widget + * to prevent dangling pointers if the paned is destroyed externally. + */ +static void +on_paned_destroy(GtkWidget *widget, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + + if (self && NEMO_IS_TERMINAL_WIDGET(self) && self->container_paned == widget) + { + // Paned is being destroyed, so nullify our reference to it. + // No need to disconnect signals here, as GTK does that on destroy. + self->container_paned = NULL; + } +} + +/** + * nemo_terminal_widget_initialize_in_paned: + * @self: The #NemoTerminalWidget instance. + * @unused_view_content: (Unused) Original content widget. The `view_overlay` is used instead. + * @view_overlay: The #GtkWidget (typically an overlay or main view area) that will + * become the top child of the new #GtkPaned. + * + * Integrates the terminal widget into the UI by creating a new #GtkPaned. + * The @view_overlay is reparented into the top part of the paned, and + * the terminal widget (#NemoTerminalWidget) is placed in the bottom part. + * The new paned then replaces @view_overlay in its original parent. + * + * Returns: %TRUE if initialization was successful, %FALSE otherwise. + */ +gboolean +nemo_terminal_widget_initialize_in_paned(NemoTerminalWidget *self, + GtkWidget *unused_view_content, + GtkWidget *view_overlay) +{ + g_return_val_if_fail(NEMO_IS_TERMINAL_WIDGET(self), FALSE); + + if (!view_overlay || !gtk_widget_get_parent(view_overlay)) + { + g_warning("Cannot add terminal: view_overlay is NULL or has no parent."); + return FALSE; + } + + GtkWidget *parent_container = gtk_widget_get_parent(view_overlay); + if (!GTK_IS_CONTAINER(parent_container)) { + g_warning("Cannot add terminal: parent of view_overlay is not a GtkContainer."); + return FALSE; + } + + // Create a new vertical paned + GtkWidget *vpaned = gtk_paned_new(GTK_ORIENTATION_VERTICAL); + self->container_paned = vpaned; // Store reference to the paned + + // Preserve packing properties if parent was a GtkBox + gint position_in_parent = -1; + gboolean box_expand = TRUE, box_fill = TRUE; // Defaults for GtkBox + guint box_padding = 0; + + if (GTK_IS_BOX(parent_container)) { + GtkBox *box_parent = GTK_BOX(parent_container); + gtk_box_query_child_packing(box_parent, view_overlay, &box_expand, &box_fill, &box_padding, NULL); + + // Get original position of view_overlay to reinsert paned at same spot + g_autoptr(GList) children = gtk_container_get_children(GTK_CONTAINER(parent_container)); + position_in_parent = g_list_index(children, view_overlay); + } + + // Reparent view_overlay into the paned + g_object_ref(view_overlay); // Increment ref before removing from old parent + gtk_container_remove(GTK_CONTAINER(parent_container), view_overlay); + + gtk_paned_pack1(GTK_PANED(vpaned), view_overlay, TRUE, FALSE); // view_overlay in top, resize=TRUE, shrink=FALSE + gtk_paned_pack2(GTK_PANED(vpaned), GTK_WIDGET(self), FALSE, TRUE); // terminal in bottom, resize=FALSE, shrink=TRUE + g_object_unref(view_overlay); // Decrement ref, paned now owns it + + // Add the new paned to the original parent container + if (GTK_IS_BOX(parent_container)) { + gtk_box_pack_start(GTK_BOX(parent_container), vpaned, box_expand, box_fill, box_padding); + if (position_in_parent != -1) { + gtk_box_reorder_child(GTK_BOX(parent_container), vpaned, position_in_parent); + } + } else { // For other container types (e.g., GtkOverlay, GtkGrid - though grid needs attach) + gtk_container_add(GTK_CONTAINER(parent_container), vpaned); + } + + // Connect signals to the paned + if (self->container_paned) { + g_signal_connect(self->container_paned, "notify::position", + G_CALLBACK(on_container_size_changed), self); + // Also connect destroy to clear our reference if paned is removed by other means + g_signal_connect(self->container_paned, "destroy", + G_CALLBACK(on_paned_destroy), self); + } + + gtk_widget_show_all(vpaned); // Show the paned and its children (terminal is initially hidden by ensure_state) + + // Apply initial size after widgets are realized (idle callback) + g_idle_add(apply_initial_size_idle, self); + nemo_terminal_widget_ensure_state(self); // Set initial visibility and size + + return TRUE; +} + +/** + * nemo_terminal_widget_get_visible: + * @self: The #NemoTerminalWidget instance. + * + * Checks if the terminal widget is currently considered visible. + * + * Returns: %TRUE if the terminal is marked as visible, %FALSE otherwise. + * Note: This reflects the intended state; the widget itself + * might still be hidden if its parent is hidden. + */ +gboolean +nemo_terminal_widget_get_visible(NemoTerminalWidget *self) +{ + g_return_val_if_fail(NEMO_IS_TERMINAL_WIDGET(self), FALSE); + return self->is_visible; +} + +/** + * nemo_terminal_widget_ensure_state: + * @self: The #NemoTerminalWidget instance. + * + * Ensures the terminal's visibility and height match the saved settings. + * This is typically called on startup or when the UI context changes. + * If the terminal should be visible but isn't, it's shown. + * If it should be hidden but isn't, it's hidden. + * The saved height is applied if visible. + */ +void +nemo_terminal_widget_ensure_state(NemoTerminalWidget *self) +{ + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + + gboolean should_be_visible = g_settings_get_boolean(nemo_window_state, "terminal-visible"); + self->height = nemo_terminal_widget_get_default_height(); // Load desired height + + if (should_be_visible != self->is_visible) + { + // Current visibility state doesn't match setting, toggle it (without saving back, as we are applying a setting) + // `is_manual_toggle = FALSE` because this is programmatic application of state + nemo_terminal_widget_toggle_visible_with_save(self, FALSE); + } + else if (should_be_visible) // Is visible and should be visible, ensure size is applied + { + gtk_widget_show(GTK_WIDGET(self)); // Ensure self (the GtkBox) is shown + if (self->container_paned && GTK_IS_WIDGET(self->container_paned)) { + if (gtk_widget_get_realized(GTK_WIDGET(self->container_paned))) { + nemo_terminal_widget_apply_new_size(self); + } else { + // If not realized, schedule size application for later + g_idle_add(apply_initial_size_idle, self); + } + } + if (self->needs_respawn) { // If shell exited while hidden + spawn_terminal_in_widget(self); + } + } else { // Is not visible and should not be visible + gtk_widget_hide(GTK_WIDGET(self)); + } +} + +/** + * nemo_terminal_widget_toggle_visible_with_save: + * @self: The #NemoTerminalWidget instance. + * @is_manual_toggle: %TRUE if the toggle was initiated by direct user action (e.g., F4 key), + * %FALSE if programmatic (e.g., applying settings). + * + * Toggles the visibility of the terminal widget. If becoming visible, + * applies its saved height and may attempt to grab focus if it's a manual toggle. + * The new visibility state is saved to GSettings. + * Emits the "toggle-visibility" signal. + */ +void +nemo_terminal_widget_toggle_visible_with_save(NemoTerminalWidget *self, + gboolean is_manual_toggle) +{ + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + + if (self->in_toggling) return; // Debounce: avoid rapid toggles + self->in_toggling = TRUE; + + self->is_visible = !self->is_visible; // Toggle the state + + if (self->is_visible) + { + gtk_widget_show(GTK_WIDGET(self)); // Show the terminal widget (the GtkBox) + if (self->container_paned && GTK_IS_WIDGET(self->container_paned)) { + // Apply size when shown + if (gtk_widget_get_realized(GTK_WIDGET(self->container_paned))) { + nemo_terminal_widget_apply_new_size(self); + } else { + g_idle_add(apply_initial_size_idle, self); // Apply after realization + } + } + + if (self->needs_respawn) { // If shell exited while hidden, respawn now + spawn_terminal_in_widget(self); + } + + if (is_manual_toggle) { // If user explicitly showed it, focus it + nemo_terminal_widget_ensure_terminal_focus(self); + } + } + else // Becoming hidden + { + gtk_widget_hide(GTK_WIDGET(self)); + } + + // Save the new visibility state to settings + g_settings_set_boolean(nemo_window_state, "terminal-visible", self->is_visible); + + // Emit signal about visibility change + g_signal_emit(self, signals[TOGGLE_VISIBILITY], 0, self->is_visible); + + // Reset toggling flag after a short delay to prevent rapid re-toggling + g_timeout_add(100, reset_toggling_flag, self); // 100ms debounce +} + +/** + * nemo_terminal_widget_toggle_visible: + * @self: The #NemoTerminalWidget instance. + * + * Convenience function to toggle terminal visibility, assuming it's a manual action. + * Calls `nemo_terminal_widget_toggle_visible_with_save()` with `is_manual_toggle = TRUE`. + */ +void +nemo_terminal_widget_toggle_visible(NemoTerminalWidget *self) +{ + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + nemo_terminal_widget_toggle_visible_with_save(self, TRUE); // Assume manual toggle +} + +/** + * nemo_terminal_widget_ensure_terminal_focus: + * @self: The #NemoTerminalWidget instance. + * + * Attempts to set keyboard focus to the VTE terminal widget. + * Uses an idle callback to ensure this happens after UI updates. + */ +void +nemo_terminal_widget_ensure_terminal_focus(NemoTerminalWidget *self) +{ + g_idle_add((GSourceFunc)gtk_widget_grab_focus, GTK_WIDGET(self->terminal)); +} + +/** + * nemo_terminal_widget_set_current_location: + * @self: The #NemoTerminalWidget instance. + * @location: The #GFile representing the new current location. Can be %NULL. + * + * Sets the terminal's current location. This may involve: + * 1. Updating `self->current_location` and notifying property changes. + * 2. If the new location is SFTP and auto-connect is enabled (and not already in SSH), + * an SSH connection might be initiated. + * 3. If not initiating SSH, and the location is different, it calls + * `change_directory_in_terminal()` to `cd` in the terminal (respecting sync modes). + * 4. If @location is %NULL and not in SSH, it might respawn the terminal in the home directory. + */ +void +nemo_terminal_widget_set_current_location(NemoTerminalWidget *self, + GFile *location) +{ + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + if (location != NULL) { // location can be NULL + g_return_if_fail(G_IS_FILE(location)); + } + + // Check if the location has logically changed (different GFile or one is NULL) + gboolean location_logically_changed = FALSE; + if ((self->current_location == NULL && location != NULL) || + (self->current_location != NULL && location == NULL) || + (self->current_location != NULL && location != NULL && !g_file_equal(self->current_location, location))) { + location_logically_changed = TRUE; + } + + // Update the internal GFile object for current_location + gboolean object_pointer_changed = g_set_object(&self->current_location, location); + + if (object_pointer_changed) // If the GFile object pointer itself changed + { + g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_CURRENT_LOCATION]); + } + + // If neither the object pointer nor the logical location changed, nothing more to do. + if (!location_logically_changed && !object_pointer_changed) { + return; + } + + // Handle SSH auto-connection if navigating to an SFTP path + if (!self->in_ssh_mode && location != NULL) + { + g_autofree gchar *uri = g_file_get_uri(location); + if (uri && g_str_has_prefix(uri, "sftp://")) + { + if (self->ssh_auto_connect_mode != NEMO_TERMINAL_SSH_AUTOCONNECT_OFF) + { + g_autofree gchar *hostname = NULL, *username = NULL, *port = NULL; + if (parse_gvfs_ssh_path(location, &hostname, &username, &port)) + { + NemoTerminalSyncMode sync_mode_for_auto_conn; + // Determine sync mode based on auto-connect setting + switch (self->ssh_auto_connect_mode) { + case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH: sync_mode_for_auto_conn = NEMO_TERMINAL_SYNC_BOTH; break; + case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM: sync_mode_for_auto_conn = NEMO_TERMINAL_SYNC_FM_TO_TERM; break; + case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM: sync_mode_for_auto_conn = NEMO_TERMINAL_SYNC_TERM_TO_FM; break; + case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_NONE: sync_mode_for_auto_conn = NEMO_TERMINAL_SYNC_NONE; break; + default: g_warning("Invalid SSH auto-connect mode: %d", self->ssh_auto_connect_mode); return; // Abort + } + _initiate_ssh_connection(self, hostname, username, port, sync_mode_for_auto_conn); + // SSH connection initiated, further 'cd' will be handled by SSH logic + return; // Don't fall through to change_directory_in_terminal for local + } + else { // Failed to parse SFTP path for auto-connect + g_warning("Failed to parse SFTP path for auto-connection: %s", uri); + // Proceed to treat as a GVFS mount path if local sync is on. + } + } + else // SSH auto-connect is OFF + { + // If local sync FM->Term is on, and this is a GVFS sftp mount, cd to the *local mount point*. + if (self->local_sync_mode == NEMO_TERMINAL_SYNC_BOTH || + self->local_sync_mode == NEMO_TERMINAL_SYNC_FM_TO_TERM) + { + g_autofree gchar *local_path = g_file_get_path(location); // Path to GVFS mount point + if (local_path && g_str_has_prefix(local_path, "/run/user/") && strstr(local_path, "/gvfs/sftp:host=")) + { + // This is a GVFS SFTP mount path. CD to it locally. + // Use the original 'location' GFile which represents this local mount point. + change_directory_in_terminal(self, location); + return; // Handled + } + } + // If not syncing locally or not a GVFS path, do nothing for SFTP if auto-connect is off. + return; + } + } + else // Not an SFTP URI, must be local or other non-SSH remote + { + if (location_logically_changed) { // Standard local directory change + change_directory_in_terminal(self, location); + } + } + } + else if (self->in_ssh_mode && location != NULL) // Already in SSH mode, FM location changed + { + if (location_logically_changed) { // If FM navigates while in SSH, sync if enabled + change_directory_in_terminal(self, location); + } + } + else if (!location) { // Location became NULL (e.g., navigating to "Computer://") + if (!self->in_ssh_mode && location_logically_changed) { + // If local terminal and location becomes invalid/null, reset to home by respawning. + spawn_terminal_in_widget(self); + } + // If in SSH mode and location becomes NULL, typically do nothing, keep SSH session as is. + } +} + +/** + * nemo_terminal_widget_new_with_location: + * @location: (Optional) The initial #GFile location for the terminal. + * If %NULL, the terminal will start in the default directory (e.g., home). + * + * Creates a new #NemoTerminalWidget. If @location is provided, it's set + * as the initial current location. The terminal spawns a shell process. + * + * Returns: A new #NemoTerminalWidget instance. The caller owns the returned object. + */ +NemoTerminalWidget * +nemo_terminal_widget_new_with_location(GFile *location) +{ + // Create instance using GObject new + NemoTerminalWidget *self = g_object_new(NEMO_TYPE_TERMINAL_WIDGET, NULL); + + if (location) + { + g_return_val_if_fail(G_IS_FILE(location), NULL); // Should not happen if caller is sane + // Set initial location without triggering full sync logic yet, as spawn will handle initial CWD. + g_set_object(&self->current_location, location); + } + + self->height = nemo_terminal_widget_get_default_height(); // Load default height + spawn_terminal_in_widget(self); // Spawn shell; uses self->current_location if set and local + // Initial visibility and placement are handled by ensure_state and initialize_in_paned. + + return self; +} + +/* Terminal color scheme definitions */ +typedef struct +{ + GdkRGBA foreground; + GdkRGBA background; + GdkRGBA palette[16]; // Standard 16 ANSI colors + gboolean use_system_colors; // If TRUE, VTE uses system theme colors +} NemoTerminalColorPalette; + +// "System" theme: delegates to VTE's default behavior (often respects GTK theme) +static const NemoTerminalColorPalette system_palette = { + .use_system_colors = TRUE +}; + +// A basic dark theme +static const NemoTerminalColorPalette dark_palette = { + .foreground = {.red = 0.9, .green = 0.9, .blue = 0.9, .alpha = 1.0}, // Light gray text + .background = {.red = 0.12, .green = 0.12, .blue = 0.12, .alpha = 1.0}, // Dark gray background + .palette = { // Standard 16 colors (8 normal, 8 bright) + {.red = 0.0, .green = 0.0, .blue = 0.0, .alpha = 1.0}, /* Black */ + {.red = 0.8, .green = 0.0, .blue = 0.0, .alpha = 1.0}, /* Red */ + {.red = 0.0, .green = 0.8, .blue = 0.0, .alpha = 1.0}, /* Green */ + {.red = 0.8, .green = 0.8, .blue = 0.0, .alpha = 1.0}, /* Yellow */ + {.red = 0.0, .green = 0.0, .blue = 0.8, .alpha = 1.0}, /* Blue */ + {.red = 0.8, .green = 0.0, .blue = 0.8, .alpha = 1.0}, /* Magenta */ + {.red = 0.0, .green = 0.8, .blue = 0.8, .alpha = 1.0}, /* Cyan */ + {.red = 0.8, .green = 0.8, .blue = 0.8, .alpha = 1.0}, /* White */ + {.red = 0.5, .green = 0.5, .blue = 0.5, .alpha = 1.0}, /* Bright Black (Grey) */ + {.red = 1.0, .green = 0.4, .blue = 0.4, .alpha = 1.0}, /* Bright Red */ + {.red = 0.4, .green = 1.0, .blue = 0.4, .alpha = 1.0}, /* Bright Green */ + {.red = 1.0, .green = 1.0, .blue = 0.4, .alpha = 1.0}, /* Bright Yellow */ + {.red = 0.4, .green = 0.4, .blue = 1.0, .alpha = 1.0}, /* Bright Blue */ + {.red = 1.0, .green = 0.4, .blue = 1.0, .alpha = 1.0}, /* Bright Magenta */ + {.red = 0.4, .green = 1.0, .blue = 1.0, .alpha = 1.0}, /* Bright Cyan */ + {.red = 1.0, .green = 1.0, .blue = 1.0, .alpha = 1.0} /* Bright White */ + }, + .use_system_colors = FALSE +}; + +// A basic light theme +static const NemoTerminalColorPalette light_palette = { + .foreground = {.red = 0.15, .green = 0.15, .blue = 0.15, .alpha = 1.0}, // Dark gray text + .background = {.red = 0.98, .green = 0.98, .blue = 0.98, .alpha = 1.0}, // Very light gray background + .palette = { + {.red = 0.2, .green = 0.2, .blue = 0.2, .alpha = 1.0}, /* Black */ + {.red = 0.8, .green = 0.2, .blue = 0.2, .alpha = 1.0}, /* Red */ + {.red = 0.1, .green = 0.6, .blue = 0.1, .alpha = 1.0}, /* Green */ + {.red = 0.7, .green = 0.6, .blue = 0.1, .alpha = 1.0}, /* Yellow */ + {.red = 0.2, .green = 0.4, .blue = 0.7, .alpha = 1.0}, /* Blue */ + {.red = 0.6, .green = 0.3, .blue = 0.5, .alpha = 1.0}, /* Magenta */ + {.red = 0.3, .green = 0.6, .blue = 0.7, .alpha = 1.0}, /* Cyan */ + {.red = 0.7, .green = 0.7, .blue = 0.7, .alpha = 1.0}, /* White */ + {.red = 0.4, .green = 0.4, .blue = 0.4, .alpha = 1.0}, /* Bright Black (Grey) */ + {.red = 0.9, .green = 0.3, .blue = 0.3, .alpha = 1.0}, /* Bright Red */ + {.red = 0.2, .green = 0.7, .blue = 0.2, .alpha = 1.0}, /* Bright Green */ + {.red = 0.8, .green = 0.7, .blue = 0.2, .alpha = 1.0}, /* Bright Yellow */ + {.red = 0.3, .green = 0.5, .blue = 0.8, .alpha = 1.0}, /* Bright Blue */ + {.red = 0.7, .green = 0.4, .blue = 0.6, .alpha = 1.0}, /* Bright Magenta */ + {.red = 0.4, .green = 0.7, .blue = 0.8, .alpha = 1.0}, /* Bright Cyan */ + {.red = 0.9, .green = 0.9, .blue = 0.9, .alpha = 1.0} /* Bright White */ + }, + .use_system_colors = FALSE +}; + +// Solarized Dark theme +static const NemoTerminalColorPalette solarized_dark_palette = { + .foreground = {.red = 0.8235, .green = 0.8588, .blue = 0.8706, .alpha = 1.0}, // base0 + .background = {.red = 0.0000, .green = 0.1686, .blue = 0.2118, .alpha = 1.0}, // base03 + .palette = { + {.red = 0.0275, .green = 0.2118, .blue = 0.2588, .alpha = 1.0}, // base02 + {.red = 0.8627, .green = 0.1961, .blue = 0.1843, .alpha = 1.0}, // red + {.red = 0.5216, .green = 0.6000, .blue = 0.0000, .alpha = 1.0}, // green + {.red = 0.7098, .green = 0.5412, .blue = 0.0000, .alpha = 1.0}, // yellow + {.red = 0.1490, .green = 0.5451, .blue = 0.8235, .alpha = 1.0}, // blue + {.red = 0.8275, .green = 0.2118, .blue = 0.5098, .alpha = 1.0}, // magenta + {.red = 0.1647, .green = 0.6314, .blue = 0.6000, .alpha = 1.0}, // cyan + {.red = 0.9294, .green = 0.9098, .blue = 0.8353, .alpha = 1.0}, // base2 + {.red = 0.0000, .green = 0.1686, .blue = 0.2118, .alpha = 1.0}, // base03 (Bright Black) + {.red = 0.8000, .green = 0.2588, .blue = 0.2078, .alpha = 1.0}, // orange (Bright Red) + {.red = 0.3725, .green = 0.4235, .blue = 0.4314, .alpha = 1.0}, // base01 (Bright Green) + {.red = 0.4078, .green = 0.4745, .blue = 0.4784, .alpha = 1.0}, // base00 (Bright Yellow) + {.red = 0.5137, .green = 0.5804, .blue = 0.5843, .alpha = 1.0}, // base0 (Bright Blue) + {.red = 0.4235, .green = 0.4431, .blue = 0.6118, .alpha = 1.0}, // violet (Bright Magenta) + {.red = 0.5804, .green = 0.6078, .blue = 0.5373, .alpha = 1.0}, // base1 (Bright Cyan) + {.red = 0.9922, .green = 0.9647, .blue = 0.8902, .alpha = 1.0} // base3 (Bright White) + }, + .use_system_colors = FALSE +}; + +// Solarized Light theme +static const NemoTerminalColorPalette solarized_light_palette = { + .foreground = {.red = 0.4000, .green = 0.4784, .blue = 0.5098, .alpha = 1.0}, // base00 + .background = {.red = 0.9922, .green = 0.9647, .blue = 0.8902, .alpha = 1.0}, // base3 + .palette = { + {.red = 0.0275, .green = 0.2118, .blue = 0.2588, .alpha = 1.0}, // base02 + {.red = 0.8627, .green = 0.1961, .blue = 0.1843, .alpha = 1.0}, // red + {.red = 0.5216, .green = 0.6000, .blue = 0.0000, .alpha = 1.0}, // green + {.red = 0.7098, .green = 0.5412, .blue = 0.0000, .alpha = 1.0}, // yellow + {.red = 0.1490, .green = 0.5451, .blue = 0.8235, .alpha = 1.0}, // blue + {.red = 0.8275, .green = 0.2118, .blue = 0.5098, .alpha = 1.0}, // magenta + {.red = 0.1647, .green = 0.6314, .blue = 0.6000, .alpha = 1.0}, // cyan + {.red = 0.9294, .green = 0.9098, .blue = 0.8353, .alpha = 1.0}, // base2 + {.red = 0.0000, .green = 0.1686, .blue = 0.2118, .alpha = 1.0}, // base03 (Bright Black) + {.red = 0.8000, .green = 0.2588, .blue = 0.2078, .alpha = 1.0}, // orange (Bright Red) + {.red = 0.3725, .green = 0.4235, .blue = 0.4314, .alpha = 1.0}, // base01 (Bright Green) + {.red = 0.4078, .green = 0.4745, .blue = 0.4784, .alpha = 1.0}, // base00 (Bright Yellow) + {.red = 0.5137, .green = 0.5804, .blue = 0.5843, .alpha = 1.0}, // base0 (Bright Blue) + {.red = 0.4235, .green = 0.4431, .blue = 0.6118, .alpha = 1.0}, // violet (Bright Magenta) + {.red = 0.5804, .green = 0.6078, .blue = 0.5373, .alpha = 1.0}, // base1 (Bright Cyan) + {.red = 0.8235, .green = 0.8588, .blue = 0.8706, .alpha = 1.0} // base0 (Bright White) + }, + .use_system_colors = FALSE +}; + +// Matrix theme (green on black) +static const NemoTerminalColorPalette matrix_palette = { + .foreground = {.red = 0.1, .green = 0.9, .blue = 0.1, .alpha = 1.0}, // Bright green text + .background = {.red = 0.0, .green = 0.0, .blue = 0.0, .alpha = 1.0}, // Pure black background + .palette = { + {.red = 0.0, .green = 0.0, .blue = 0.0, .alpha = 1.0}, /* Black */ + {.red = 0.0, .green = 0.5, .blue = 0.0, .alpha = 1.0}, /* Red (as dark green) */ + {.red = 0.0, .green = 0.8, .blue = 0.0, .alpha = 1.0}, /* Green */ + {.red = 0.1, .green = 0.6, .blue = 0.0, .alpha = 1.0}, /* Yellow (as yellow-green) */ + {.red = 0.0, .green = 0.4, .blue = 0.0, .alpha = 1.0}, /* Blue (as darker green) */ + {.red = 0.1, .green = 0.5, .blue = 0.1, .alpha = 1.0}, /* Magenta (as mid-green) */ + {.red = 0.0, .green = 0.7, .blue = 0.1, .alpha = 1.0}, /* Cyan (as cyan-green) */ + {.red = 0.1, .green = 0.9, .blue = 0.1, .alpha = 1.0}, /* White (as bright green) */ + {.red = 0.0, .green = 0.3, .blue = 0.0, .alpha = 1.0}, /* Bright Black (very dark green) */ + {.red = 0.0, .green = 0.6, .blue = 0.0, .alpha = 1.0}, /* Bright Red */ + {.red = 0.0, .green = 1.0, .blue = 0.0, .alpha = 1.0}, /* Bright Green (full green) */ + {.red = 0.2, .green = 0.7, .blue = 0.0, .alpha = 1.0}, /* Bright Yellow */ + {.red = 0.0, .green = 0.5, .blue = 0.0, .alpha = 1.0}, /* Bright Blue */ + {.red = 0.2, .green = 0.6, .blue = 0.2, .alpha = 1.0}, /* Bright Magenta */ + {.red = 0.0, .green = 0.8, .blue = 0.2, .alpha = 1.0}, /* Bright Cyan */ + {.red = 0.2, .green = 1.0, .blue = 0.2, .alpha = 1.0} /* Bright White (very bright green) */ + }, + .use_system_colors = FALSE +}; + +// One Half Dark theme (approximated from popular editor themes) +static const NemoTerminalColorPalette one_half_dark_palette = { + .foreground = {.red = 0.870, .green = 0.870, .blue = 0.870, .alpha = 1.0}, // abb2bf + .background = {.red = 0.157, .green = 0.168, .blue = 0.184, .alpha = 1.0}, // 282c34 + .palette = { + {.red = 0.157, .green = 0.168, .blue = 0.184, .alpha = 1.0}, /* Black (bg) 282c34 */ + {.red = 0.882, .green = 0.490, .blue = 0.470, .alpha = 1.0}, /* Red e06c75 */ + {.red = 0.560, .green = 0.749, .blue = 0.450, .alpha = 1.0}, /* Green 98c379 */ + {.red = 0.941, .green = 0.768, .blue = 0.470, .alpha = 1.0}, /* Yellow e5c07b */ + {.red = 0.400, .green = 0.627, .blue = 0.850, .alpha = 1.0}, /* Blue 61afef */ + {.red = 0.768, .green = 0.470, .blue = 0.800, .alpha = 1.0}, /* Magenta c678dd */ + {.red = 0.341, .green = 0.709, .blue = 0.729, .alpha = 1.0}, /* Cyan 56b6c2 */ + {.red = 0.870, .green = 0.870, .blue = 0.870, .alpha = 1.0}, /* White (fg) abb2bf */ + {.red = 0.400, .green = 0.450, .blue = 0.500, .alpha = 1.0}, /* Bright Black 5c6370 (comments) */ + {.red = 0.882, .green = 0.490, .blue = 0.470, .alpha = 1.0}, /* Bright Red (same as normal) */ + {.red = 0.560, .green = 0.749, .blue = 0.450, .alpha = 1.0}, /* Bright Green */ + {.red = 0.941, .green = 0.768, .blue = 0.470, .alpha = 1.0}, /* Bright Yellow */ + {.red = 0.400, .green = 0.627, .blue = 0.850, .alpha = 1.0}, /* Bright Blue */ + {.red = 0.768, .green = 0.470, .blue = 0.800, .alpha = 1.0}, /* Bright Magenta */ + {.red = 0.341, .green = 0.709, .blue = 0.729, .alpha = 1.0}, /* Bright Cyan */ + {.red = 0.970, .green = 0.970, .blue = 0.970, .alpha = 1.0} /* Bright White (lighter fg) */ + }, + .use_system_colors = FALSE +}; + +// One Half Light theme (approximated) +static const NemoTerminalColorPalette one_half_light_palette = { + .foreground = {.red = 0.220, .green = 0.240, .blue = 0.260, .alpha = 1.0}, // 383a42 (text) + .background = {.red = 0.980, .green = 0.980, .blue = 0.980, .alpha = 1.0}, //fafafa (bg) + .palette = { + {.red = 0.220, .green = 0.240, .blue = 0.260, .alpha = 1.0}, /* Black (fg) 383a42 */ + {.red = 0.858, .green = 0.200, .blue = 0.180, .alpha = 1.0}, /* Red e45649 */ + {.red = 0.310, .green = 0.600, .blue = 0.110, .alpha = 1.0}, /* Green 50a14f */ + {.red = 0.850, .green = 0.588, .blue = 0.100, .alpha = 1.0}, /* Yellow c18401 */ + {.red = 0.231, .green = 0.490, .blue = 0.749, .alpha = 1.0}, /* Blue 4078f2 */ + {.red = 0.670, .green = 0.270, .blue = 0.729, .alpha = 1.0}, /* Magenta a626a4 */ + {.red = 0.149, .green = 0.639, .blue = 0.678, .alpha = 1.0}, /* Cyan 0184bc */ + {.red = 0.800, .green = 0.800, .blue = 0.800, .alpha = 1.0}, /* White (light gray) a0a1a7 */ + {.red = 0.400, .green = 0.400, .blue = 0.400, .alpha = 1.0}, /* Bright Black (gray comments) 696c77 */ + {.red = 0.858, .green = 0.200, .blue = 0.180, .alpha = 1.0}, /* Bright Red */ + {.red = 0.310, .green = 0.600, .blue = 0.110, .alpha = 1.0}, /* Bright Green */ + {.red = 0.850, .green = 0.588, .blue = 0.100, .alpha = 1.0}, /* Bright Yellow */ + {.red = 0.231, .green = 0.490, .blue = 0.749, .alpha = 1.0}, /* Bright Blue */ + {.red = 0.670, .green = 0.270, .blue = 0.729, .alpha = 1.0}, /* Bright Magenta */ + {.red = 0.149, .green = 0.639, .blue = 0.678, .alpha = 1.0}, /* Bright Cyan */ + {.red = 0.080, .green = 0.080, .blue = 0.080, .alpha = 1.0} /* Bright White (darkest text) 14161a */ + }, + .use_system_colors = FALSE +}; + +// Monokai theme (classic approximation) +static const NemoTerminalColorPalette monokai_palette = { + .foreground = {.red = 0.929, .green = 0.925, .blue = 0.910, .alpha = 1.0}, // f8f8f2 + .background = {.red = 0.153, .green = 0.157, .blue = 0.149, .alpha = 1.0}, // 272822 + .palette = { + {.red = 0.153, .green = 0.157, .blue = 0.149, .alpha = 1.0}, /* Black (bg) 272822 */ + {.red = 0.980, .green = 0.149, .blue = 0.450, .alpha = 1.0}, /* Red f92672 */ + {.red = 0.650, .green = 0.890, .blue = 0.180, .alpha = 1.0}, /* Green a6e22e */ + {.red = 0.960, .green = 0.780, .blue = 0.310, .alpha = 1.0}, /* Yellow f4bf75 */ + {.red = 0.208, .green = 0.580, .blue = 0.839, .alpha = 1.0}, /* Blue 66d9ef (often cyan used as blue) */ + {.red = 0.670, .green = 0.380, .blue = 0.960, .alpha = 1.0}, /* Magenta ae81ff */ + {.red = 0.239, .green = 0.909, .blue = 0.920, .alpha = 1.0}, /* Cyan (using a brighter cyan) 3 sensación */ + {.red = 0.929, .green = 0.925, .blue = 0.910, .alpha = 1.0}, /* White (fg) f8f8f2 */ + {.red = 0.400, .green = 0.400, .blue = 0.400, .alpha = 1.0}, /* Bright Black (comments) 75715e */ + {.red = 0.980, .green = 0.149, .blue = 0.450, .alpha = 1.0}, /* Bright Red */ + {.red = 0.650, .green = 0.890, .blue = 0.180, .alpha = 1.0}, /* Bright Green */ + {.red = 0.960, .green = 0.780, .blue = 0.310, .alpha = 1.0}, /* Bright Yellow */ + {.red = 0.208, .green = 0.580, .blue = 0.839, .alpha = 1.0}, /* Bright Blue */ + {.red = 0.670, .green = 0.380, .blue = 0.960, .alpha = 1.0}, /* Bright Magenta */ + {.red = 0.400, .green = 0.950, .blue = 0.950, .alpha = 1.0}, /* Bright Cyan (very bright) */ + {.red = 1.000, .green = 1.000, .blue = 1.000, .alpha = 1.0} /* Bright White (pure white) */ + }, + .use_system_colors = FALSE +}; + +/** + * nemo_terminal_widget_get_color_scheme: + * @self: The #NemoTerminalWidget instance. + * + * Retrieves the name of the currently active color scheme. + * If not already loaded from GSettings, it loads it and defaults to "system" + * if the setting is missing or empty. + * + * Returns: A string representing the current color scheme name (e.g., "system", "dark"). + * This string is owned by the widget instance or is a literal and should not be freed by the caller. + */ +const gchar * +nemo_terminal_widget_get_color_scheme(NemoTerminalWidget *self) +{ + g_return_val_if_fail(NEMO_IS_TERMINAL_WIDGET(self), "system"); // Default "system" on failure + + if (self->color_scheme == NULL) // Lazy load from settings + { + self->color_scheme = g_settings_get_string(nemo_window_state, "terminal-color-scheme"); + // If setting is NULL, empty, or invalid, default to "system" + if (self->color_scheme == NULL || *self->color_scheme == '\0') { + g_free(self->color_scheme); // Safe if NULL + self->color_scheme = g_strdup("system"); // Ensure it's a valid, owned string + } + // Further validation against COLOR_SCHEME_ENTRIES could be done here if needed + } + return self->color_scheme; +} + +/** + * nemo_terminal_widget_set_color_scheme: + * @self: The #NemoTerminalWidget instance. + * @scheme_name: The name of the color scheme to set (e.g., "dark", "solarized-light"). + * + * Sets the terminal's color scheme. If the provided @scheme_name is different + * from the current one and is valid, it updates the internal state, saves the + * new scheme name to GSettings, and applies the scheme to the VTE terminal. + * If @scheme_name is invalid, it defaults to "system". + */ +void +nemo_terminal_widget_set_color_scheme(NemoTerminalWidget *self, const gchar *scheme_name) +{ + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + g_return_if_fail(scheme_name != NULL); + + // Validate the scheme name against known schemes + gboolean is_valid_scheme = FALSE; + for (gsize i = 0; i < G_N_ELEMENTS(COLOR_SCHEME_ENTRIES); ++i) { + if (g_strcmp0(scheme_name, COLOR_SCHEME_ENTRIES[i].id) == 0) { + is_valid_scheme = TRUE; + break; + } + } + + if (!is_valid_scheme) { + g_warning("Invalid terminal color scheme requested: '%s'. Defaulting to 'system'.", scheme_name); + scheme_name = "system"; // Fallback to a known default + } + + // Only update if the scheme has actually changed + if (g_strcmp0(nemo_terminal_widget_get_color_scheme(self), scheme_name) != 0) { + g_free(self->color_scheme); // Free old scheme name string + self->color_scheme = g_strdup(scheme_name); // Store new one + + g_settings_set_string(nemo_window_state, "terminal-color-scheme", self->color_scheme); + nemo_terminal_widget_apply_color_scheme(self); // Apply the new scheme visually + } +} + +/** + * nemo_terminal_widget_apply_color_scheme: + * @self: The #NemoTerminalWidget instance. + * + * Applies the currently selected color scheme (stored in `self->color_scheme`) + * to the VTE terminal widget. This involves setting foreground, background, + * and palette colors, or resetting to system colors if "system" scheme is chosen. + */ +void +nemo_terminal_widget_apply_color_scheme(NemoTerminalWidget *self) +{ + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + + const NemoTerminalColorPalette *palette_to_apply = NULL; + const gchar *current_scheme_name = nemo_terminal_widget_get_color_scheme(self); + + // Map scheme name to its corresponding palette definition + if (g_strcmp0(current_scheme_name, "dark") == 0) + palette_to_apply = &dark_palette; + else if (g_strcmp0(current_scheme_name, "light") == 0) + palette_to_apply = &light_palette; + else if (g_strcmp0(current_scheme_name, "solarized-dark") == 0) + palette_to_apply = &solarized_dark_palette; + else if (g_strcmp0(current_scheme_name, "solarized-light") == 0) + palette_to_apply = &solarized_light_palette; + else if (g_strcmp0(current_scheme_name, "matrix") == 0) + palette_to_apply = &matrix_palette; + else if (g_strcmp0(current_scheme_name, "one-half-dark") == 0) + palette_to_apply = &one_half_dark_palette; + else if (g_strcmp0(current_scheme_name, "one-half-light") == 0) + palette_to_apply = &one_half_light_palette; + else if (g_strcmp0(current_scheme_name, "monokai") == 0) + palette_to_apply = &monokai_palette; + else // Default to "system" scheme (includes explicit "system" or unrecognized) + palette_to_apply = &system_palette; + + // Apply the chosen palette to the VTE terminal + if (palette_to_apply->use_system_colors) + { + // Reset to VTE/system default colors + // Passing NULL or a zeroed GdkRGBA typically resets to defaults. + GdkRGBA default_color = {0}; // Zeroed structure + vte_terminal_set_color_background(self->terminal, &default_color); // Reset background + vte_terminal_set_color_foreground(self->terminal, &default_color); // Reset foreground + vte_terminal_set_colors(self->terminal, NULL, NULL, NULL, 0); // Reset palette + } + else + { + // Apply the custom foreground, background, and 16-color palette + vte_terminal_set_colors(self->terminal, + &palette_to_apply->foreground, + &palette_to_apply->background, + palette_to_apply->palette, // Array of GdkRGBA + G_N_ELEMENTS(palette_to_apply->palette)); // Count of palette colors + } +} + +/** + * nemo_terminal_widget_finalize: + * @object: The #NemoTerminalWidget GObject instance being finalized. + * + * GObject finalize function. Frees allocated resources associated with the + * widget instance, such as GFile objects, action groups, strings, and + * disconnects signals from external objects if necessary. + */ +static void +nemo_terminal_widget_finalize(GObject *object) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(object); + + // Disconnect signals connected to self->container_paned if it still exists + // This prevents callbacks on a partially destroyed 'self' if paned outlives 'self'. + // Note: GTK usually handles disconnection from destroyed objects, but explicit is safer for non-child objects. + if (self->container_paned && GTK_IS_WIDGET(self->container_paned)) + { + g_signal_handlers_disconnect_by_func(self->container_paned, G_CALLBACK(on_container_size_changed), self); + g_signal_handlers_disconnect_by_func(self->container_paned, G_CALLBACK(on_paned_destroy), self); + // Do not unref container_paned here, it's owned by its parent GTK container. + // on_paned_destroy should set self->container_paned to NULL if it's destroyed first. + } + self->container_paned = NULL; // Clear reference + + // Clean up GObject resources + g_clear_object(&self->current_location); + g_clear_object(&self->action_group); + + // Free allocated strings + g_free(self->color_scheme); + self->color_scheme = NULL; + + // Clear any remaining SSH state (important for freeing SSH-related strings) + clear_ssh_state(self); + + // Cancel any pending timeouts + if (self->focus_timeout_id > 0) { + g_source_remove(self->focus_timeout_id); + self->focus_timeout_id = 0; + } + // (reset_toggling_flag timeout should also be handled if it were stored with an ID) + + // Chain up to the parent class's finalize method + G_OBJECT_CLASS(nemo_terminal_widget_parent_class)->finalize(object); +} + +/** + * nemo_terminal_get_font_size: + * + * Retrieves the saved terminal font size (in points) from GSettings. + * + * Returns: The font size in points. Defaults to 12 if the setting is + * invalid or outside a reasonable range (6-72pt). + */ +static int +nemo_terminal_get_font_size(void) +{ + int saved_size_pts = g_settings_get_int(nemo_window_state, "terminal-font-size"); + // Validate saved size, provide a default if out of range + return (saved_size_pts >= 6 && saved_size_pts <= 72) ? saved_size_pts : 12; // Default 12pt +} + +/** + * nemo_terminal_widget_save_font_size: + * @self: The #NemoTerminalWidget instance. + * @font_size_pts: The font size in points to save. + * + * Saves the terminal's font size (in points) to GSettings, if it's within + * a reasonable range (6-72pt). + */ +static void +nemo_terminal_widget_save_font_size(NemoTerminalWidget *self, int font_size_pts) +{ + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + + // Persist only if font size is within a sensible range + if (font_size_pts >= 6 && font_size_pts <= 72) + { + // Only write to GSettings if it's different from current setting to avoid unnecessary writes. + if (g_settings_get_int(nemo_window_state, "terminal-font-size") != font_size_pts) { + g_settings_set_int(nemo_window_state, "terminal-font-size", font_size_pts); + } + } +} + +/** + * build_ssh_command_string: + * @hostname: The hostname for the SSH connection (mandatory). + * @username: (Optional) The username for SSH. + * @port: (Optional) The port number for SSH as a string. + * + * Constructs the basic SSH command line string (e.g., "ssh user@host -p 2222\n"). + * Username and hostname are shell-quoted. Port is validated to be numeric + * and within the valid port range. The command always ends with a newline + * character, suitable for direct feeding to `vte_terminal_feed_child` to execute. + * + * Returns: A newly allocated string containing the SSH command. + * The caller must free this string. Returns %NULL on failure (e.g. no hostname). + */ +static gchar * +build_ssh_command_string(const gchar *hostname, const gchar *username, const gchar *port) +{ + g_return_val_if_fail(hostname != NULL && *hostname != '\0', NULL); + + // GString struct itself is managed by the g_string_free call at the end when stealing the buffer. + // Do NOT use g_autofree on cmd_builder here, as g_string_free(..., FALSE) frees the struct. + GString *cmd_builder = g_string_new("ssh "); + + // Append username if provided + if (username != NULL && *username != '\0') + { + // g_shell_quote returns a new string that must be freed. g_autofree handles this. + g_autofree gchar *quoted_username = g_shell_quote(username); + g_string_append_printf(cmd_builder, "%s@", quoted_username); + } + + // Append hostname (mandatory) + g_autofree gchar *quoted_hostname = g_shell_quote(hostname); + g_string_append(cmd_builder, quoted_hostname); + + // Append port if provided and valid + if (port != NULL && *port != '\0') + { + gboolean is_numeric_port = TRUE; + for (const gchar *p_char = port; *p_char; ++p_char) { + if (!g_ascii_isdigit(*p_char)) { + is_numeric_port = FALSE; + break; + } + } + + if (is_numeric_port) { + long port_num_long = g_ascii_strtoll(port, NULL, 10); // Base 10 + if (port_num_long > 0 && port_num_long <= 65535) { // Valid TCP/UDP port range + // Port is numeric and in range, append it. No need to quote numeric port. + g_string_append_printf(cmd_builder, " -p %s", port); + } else { + g_warning("Invalid port number specified: %s. Port option will be omitted.", port); + } + } else { + g_warning("Non-numeric port specified: %s. Port option will be omitted.", port); + } + } + + g_string_append_c(cmd_builder, '\n'); // Add newline to execute command when fed + + // Frees the GString struct cmd_builder itself, and returns ownership of the internal char* buffer. + return g_string_free(cmd_builder, FALSE); +} + + +/** + * parse_gvfs_ssh_path: + * @location: The #GFile representing a location, potentially SFTP. + * @hostname: (Output) Pointer to store the extracted hostname. + * @username: (Output) Pointer to store the extracted username. + * @port: (Output) Pointer to store the extracted port string. + * + * Parses a #GFile's URI or path to extract SSH connection details (hostname, + * username, port) if it represents an SFTP location. + * Handles "sftp://" URIs directly. + * Also attempts to parse GVFS-style local mount paths for SFTP shares + * (e.g., "/run/user/UID/gvfs/sftp:host=example.com,user=me/path"). + * Output parameters are allocated by this function and must be freed by the caller + * if the function returns %TRUE. If %FALSE, their state is undefined but typically NULL. + * + * Returns: %TRUE if SSH details (at least hostname) were successfully parsed, %FALSE otherwise. + */ +static gboolean +parse_gvfs_ssh_path(GFile *location, gchar **hostname, gchar **username, gchar **port) +{ + g_return_val_if_fail(G_IS_FILE(location), FALSE); + g_return_val_if_fail(hostname != NULL && username != NULL && port != NULL, FALSE); + + // Initialize output parameters to NULL + *hostname = NULL; + *username = NULL; + *port = NULL; + + g_autofree gchar *uri_str = g_file_get_uri(location); + if (uri_str == NULL) return FALSE; // Cannot proceed without a URI + + gboolean success = FALSE; + + // Try parsing as a standard "sftp://" URI first + if (g_str_has_prefix(uri_str, "sftp://")) + { + g_autoptr(GUri) parsed_sftp_uri = g_uri_parse(uri_str, G_URI_FLAGS_NONE, NULL); + if (parsed_sftp_uri) { + const char *parsed_host_const = g_uri_get_host(parsed_sftp_uri); + if (parsed_host_const && *parsed_host_const != '\0') { + *hostname = g_strdup(parsed_host_const); + success = TRUE; // At least hostname is found + + // Get user info (can be "user" or "user:password") + // For SFTP, typically just "user". g_uri_get_user() is better if available (GLib >= 2.66) + const char *user_info_const = g_uri_get_userinfo(parsed_sftp_uri); + if (user_info_const && *user_info_const != '\0') { + // Assuming no password in userinfo for SFTP URIs from GVFS. + // If password could be present, strchr for ':' would be needed. + *username = g_strdup(user_info_const); + } + + int port_num_int = g_uri_get_port(parsed_sftp_uri); + // Only store port if it's non-standard (not 22) and valid. + if (port_num_int > 0 && port_num_int <= 65535 && port_num_int != 22) { + *port = g_strdup_printf("%d", port_num_int); + } + } + } + } + else // Fallback: Try parsing as a local GVFS mount path for SFTP + { + g_autofree gchar *local_fs_path = g_file_get_path(location); + if (local_fs_path) + { + // Example path: /run/user/1000/gvfs/sftp:host=example.com,user=testuser/remote/folder + // Look for the characteristic GVFS sftp mount string part. + const char *gvfs_sftp_marker_prefix = "/gvfs/sftp:host="; // A common pattern + char *sftp_details_start = strstr(local_fs_path, gvfs_sftp_marker_prefix); + + if (sftp_details_start) { + // Move past "/gvfs/" to the start of "sftp:host=..." or "host=..." + sftp_details_start += strlen("/gvfs/"); + + // The details (host, user, port) are comma-separated before the actual remote path part. + // Find end of connection details part (start of actual path, or end of string) + char *path_component_start = strchr(sftp_details_start, '/'); + g_autofree gchar *details_substring = NULL; + if (path_component_start) { + details_substring = g_strndup(sftp_details_start, path_component_start - sftp_details_start); + } else { + details_substring = g_strdup(sftp_details_start); + } + + g_auto(GStrv) parts = g_strsplit(details_substring, ",", -1); + for (gchar **part_iter = parts; part_iter && *part_iter; ++part_iter) + { + if (g_str_has_prefix(*part_iter, "sftp:host=")) { + g_free(*hostname); // Free previous if any (e.g. from "host=") + *hostname = g_strdup(*part_iter + strlen("sftp:host=")); + } else if (g_str_has_prefix(*part_iter, "host=") && *hostname == NULL) { // Only if sftp:host not found + *hostname = g_strdup(*part_iter + strlen("host=")); + } else if (g_str_has_prefix(*part_iter, "user=")) { + g_free(*username); + *username = g_strdup(*part_iter + strlen("user=")); + } else if (g_str_has_prefix(*part_iter, "port=")) { + g_free(*port); + *port = g_strdup(*part_iter + strlen("port=")); + } + } + // Success if hostname was found + if (*hostname != NULL && **hostname != '\0') { + success = TRUE; + } + } + } + } + + // If parsing failed but memory was allocated for outputs, free it. + if (!success) { + g_clear_pointer(hostname, g_free); + g_clear_pointer(username, g_free); + g_clear_pointer(port, g_free); + } + return success; +} + +/** + * on_terminal_contents_changed: + * @terminal: The #VteTerminal whose contents changed. + * @user_data: The #NemoTerminalWidget instance. + * + * Callback for VTE's "contents-changed" signal. + * This is used heuristically to detect when an SSH connection has likely + * become "live" (i.e., a shell prompt or login message appears). + * When `self->ssh_connecting` is TRUE, it scans recent terminal output + * for common prompt indicators. If found, it finalizes the SSH setup: + * - Sets up PROMPT_COMMAND for Term->FM sync if enabled. + * - `cd`s to the `ssh_remote_path` if set and FM->Term sync is enabled. + * - Grabs focus for the terminal. + */ +static void +on_terminal_contents_changed(VteTerminal *terminal, + gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + + // If we are in the process of establishing an SSH connection: + if (self->ssh_connecting) + { + // Heuristic: Check if a prompt has appeared, indicating connection established. + // Avoid checking if there's a selection, as that might be user activity. + if (vte_terminal_get_has_selection(terminal)) return; + + glong cursor_row, cursor_col; + vte_terminal_get_cursor_position(terminal, &cursor_col, &cursor_row); + + if (cursor_row < 0 || cursor_col < 0) return; // Cursor position not valid + + // Check a few lines of recent output for prompt-like strings. + // This is a heuristic and might not be 100% reliable for all SSH servers/shells. + glong start_scan_row = MAX(0, cursor_row - 5); // Scan last 5 lines approx. + glong terminal_cols = vte_terminal_get_column_count(terminal); + + // Get text from a range. VTE might return less if at start/end of buffer. + g_autofree gchar *recent_text = vte_terminal_get_text_range(terminal, + start_scan_row, 0, // Start row, col + cursor_row, terminal_cols, // End row, col + NULL, NULL, NULL); // Predicates unused + + if (recent_text) + { + // Common shell prompt indicators or SSH welcome messages + const char *prompt_indicators[] = { + "$ ", "# ", "% ", "> ", // Common shell prompts + "@", // Often part of user@host + "~]$", "~]#", // Common Bash/Zsh full prompts + "Last login:", "Welcome to", // SSH login messages + NULL // Terminator + }; + + gboolean prompt_likely_found = FALSE; + for (int i = 0; prompt_indicators[i]; ++i) { + if (strstr(recent_text, prompt_indicators[i])) { + prompt_likely_found = TRUE; + break; + } + } + + if (prompt_likely_found) + { + // SSH connection seems to be live + self->ssh_connecting = FALSE; // No longer in "connecting" state + + // If sync Term->FM is enabled for this SSH session, set up PROMPT_COMMAND on remote. + // This is a best-effort attempt; remote shell must support PROMPT_COMMAND (e.g., bash, zsh). + if (self->pending_ssh_sync_mode == NEMO_TERMINAL_SYNC_BOTH || + self->pending_ssh_sync_mode == NEMO_TERMINAL_SYNC_TERM_TO_FM) + { + // Simple PROMPT_COMMAND for OSC7. + const char *osc7_export_cmd = "export PROMPT_COMMAND='echo -en \"\\033]7;file://$PWD\\007\"'\n"; + vte_terminal_feed_child(self->terminal, osc7_export_cmd, -1); + } + + // If a remote path was stored and FM->Term sync is enabled, cd to it. + if (self->ssh_remote_path && *self->ssh_remote_path && + (self->pending_ssh_sync_mode == NEMO_TERMINAL_SYNC_BOTH || + self->pending_ssh_sync_mode == NEMO_TERMINAL_SYNC_FM_TO_TERM)) + { + self->ignore_next_terminal_cd_signal = TRUE; // We are initiating this cd + feed_cd_command(self->terminal, self->ssh_remote_path); + } + + // Connection established and initial commands sent, grab focus. + gtk_widget_grab_focus(GTK_WIDGET(self->terminal)); + } + } + } +} + +/** + * feed_cd_command: + * @terminal: The #VteTerminal to feed the command to. + * @path: The directory path to change to. + * + * Feeds a "cd /path/to/directory\r" command to the terminal. + * It attempts to preserve any text already typed by the user on the current + * command line by using shell control sequences (Ctrl+A, Ctrl+K, Ctrl+Y). + * This is a common technique to avoid disrupting user input, especially + * with shells that have auto-suggestion features (like fish, zsh with plugins). + * The path is shell-quoted. + */ +static void +feed_cd_command(VteTerminal *terminal, const char *path) +{ + g_return_if_fail(VTE_IS_TERMINAL(terminal)); + g_return_if_fail(path != NULL); + + g_autofree gchar *quoted_path = g_shell_quote(path); + // Use \r (carriage return) to execute, some shells might prefer \n. \r is common. + g_autofree gchar *cd_command_str = g_strdup_printf("cd %s\r", quoted_path); + + if (!cd_command_str) { + g_warning("feed_cd_command: Failed to create cd command string for path: %s", path); + return; + } + + // This sequence aims to preserve user's current input line: + // 1. \x01 (Ctrl+A): Move cursor to start of line. + // 2. " ": Insert a space. (Ensures Ctrl+K has something to cut if line was empty, and simplifies restoration). + // 3. \x01 (Ctrl+A): Move cursor to start of line again (before the space). + // 4. \x0B (Ctrl+K): Kill (cut) text from cursor to end of line. This saves it to the shell's kill-ring. + // 5. (feed cd command): Execute the `cd` command. + // 6. \x19 (Ctrl+Y): Yank (paste) the killed text back. + // 7. \x01 (Ctrl+A): Move cursor to start of line. + // 8. \033[3~ (Delete): Delete the leading space that was inserted. (Standard VT100/xterm delete char sequence) + // 9. \x05 (Ctrl+E): Move cursor to end of line. (Restores cursor position if user was typing at end) + + vte_terminal_feed_child(terminal, "\x01 ", -1); // Ctrl+A, space + vte_terminal_feed_child(terminal, "\x01", -1); // Ctrl+A + vte_terminal_feed_child(terminal, "\x0B", -1); // Ctrl+K (cut line) + vte_terminal_feed_child(terminal, cd_command_str, -1); // Feed "cd /new/path\r" + vte_terminal_feed_child(terminal, "\x19", -1); // Ctrl+Y (paste old line) + vte_terminal_feed_child(terminal, "\x01", -1); // Ctrl+A + vte_terminal_feed_child(terminal, "\033[3~", -1); // Delete char (the space) + vte_terminal_feed_child(terminal, "\x05", -1); // Ctrl+E (end of line) +} \ No newline at end of file diff --git a/src/nemo-terminal-widget.h b/src/nemo-terminal-widget.h new file mode 100644 index 000000000..442f78cfb --- /dev/null +++ b/src/nemo-terminal-widget.h @@ -0,0 +1,131 @@ +/* nemo-terminal-widget.h + + Copyright (C) 2025 + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public + License along with this program; if not, see . + + Author: Bruno Goncalves + */ + +#ifndef __NEMO_TERMINAL_WIDGET_H__ +#define __NEMO_TERMINAL_WIDGET_H__ + +#include +#include +#include + +G_BEGIN_DECLS + +typedef enum +{ + NEMO_TERMINAL_SYNC_NONE, + NEMO_TERMINAL_SYNC_FM_TO_TERM, + NEMO_TERMINAL_SYNC_TERM_TO_FM, + NEMO_TERMINAL_SYNC_BOTH +} NemoTerminalSyncMode; + +typedef enum +{ + NEMO_TERMINAL_SSH_AUTOCONNECT_OFF, + NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH, + NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM, + NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM, + NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_NONE +} NemoTerminalSshAutoConnectMode; + +#define NEMO_TYPE_TERMINAL_WIDGET (nemo_terminal_widget_get_type()) +#define NEMO_TERMINAL_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NEMO_TYPE_TERMINAL_WIDGET, NemoTerminalWidget)) +#define NEMO_TERMINAL_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), NEMO_TYPE_TERMINAL_WIDGET, NemoTerminalWidgetClass)) +#define NEMO_IS_TERMINAL_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NEMO_TYPE_TERMINAL_WIDGET)) +#define NEMO_IS_TERMINAL_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NEMO_TYPE_TERMINAL_WIDGET)) +#define NEMO_TERMINAL_WIDGET_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), NEMO_TYPE_TERMINAL_WIDGET, NemoTerminalWidgetClass)) + +typedef struct _NemoTerminalWidget NemoTerminalWidget; +typedef struct _NemoTerminalWidgetClass NemoTerminalWidgetClass; +typedef struct _NemoWindowPane NemoWindowPane; + +struct _NemoTerminalWidget +{ + GtkBox parent_instance; + + GtkWidget *scrolled_window; + VteTerminal *terminal; + GtkWidget *ssh_indicator; + GtkWidget *container_paned; + NemoWindowPane *pane; + + GSimpleActionGroup *action_group; + + gboolean is_visible; + gboolean maintain_focus; + gboolean in_toggling; + gboolean needs_respawn; + gboolean is_exiting_ssh; + gboolean ssh_connecting; + gboolean ignore_next_terminal_cd_signal; + + int height; + guint focus_timeout_id; + + GFile *current_location; + + gchar *color_scheme; + + gboolean in_ssh_mode; + NemoTerminalSyncMode ssh_sync_mode; + NemoTerminalSyncMode pending_ssh_sync_mode; + NemoTerminalSshAutoConnectMode ssh_auto_connect_mode; + gchar *ssh_hostname; + gchar *ssh_username; + gchar *ssh_port; + gchar *ssh_remote_path; + + NemoTerminalSyncMode local_sync_mode; +}; + +struct _NemoTerminalWidgetClass +{ + GtkBoxClass parent_class; +}; + +GType nemo_terminal_widget_get_type(void); + +NemoTerminalWidget *nemo_terminal_widget_new(void); +NemoTerminalWidget *nemo_terminal_widget_new_with_location(GFile *location); + +void spawn_terminal_in_widget(NemoTerminalWidget *self); +void nemo_terminal_widget_set_current_location(NemoTerminalWidget *self, GFile *location); +void nemo_terminal_widget_ensure_terminal_focus(NemoTerminalWidget *self); + +gboolean nemo_terminal_widget_initialize_in_paned(NemoTerminalWidget *self, + GtkWidget *unused_view_content, + GtkWidget *view_overlay); + +void nemo_terminal_widget_toggle_visible(NemoTerminalWidget *self); +void nemo_terminal_widget_toggle_visible_with_save(NemoTerminalWidget *self, + gboolean is_manual_toggle); +gboolean nemo_terminal_widget_get_visible(NemoTerminalWidget *self); +void nemo_terminal_widget_ensure_state(NemoTerminalWidget *self); + +void nemo_terminal_widget_apply_new_size(NemoTerminalWidget *self); +int nemo_terminal_widget_get_default_height(void); +void nemo_terminal_widget_save_height(NemoTerminalWidget *self, int height); + +const gchar *nemo_terminal_widget_get_color_scheme(NemoTerminalWidget *self); +void nemo_terminal_widget_set_color_scheme(NemoTerminalWidget *self, const gchar *scheme); +void nemo_terminal_widget_apply_color_scheme(NemoTerminalWidget *self); + +G_END_DECLS + +#endif \ No newline at end of file diff --git a/src/nemo-window-manage-views.c b/src/nemo-window-manage-views.c index c1bc864db..f286c38de 100644 --- a/src/nemo-window-manage-views.c +++ b/src/nemo-window-manage-views.c @@ -1557,6 +1557,11 @@ update_for_new_location (NemoWindowSlot *slot) nemo_window_slot_update_title (slot); nemo_window_slot_update_icon (slot); + + /* Update terminal location if it exists and is visible */ + if (slot->terminal_widget != NULL && slot->terminal_visible) { + nemo_window_slot_update_terminal_location (slot); + } if (slot == slot->pane->active_slot) { nemo_window_pane_sync_location_widgets (slot->pane); diff --git a/src/nemo-window-menus.c b/src/nemo-window-menus.c index 2cc9bdff7..936ab5b8b 100644 --- a/src/nemo-window-menus.c +++ b/src/nemo-window-menus.c @@ -70,6 +70,7 @@ #define MENU_PATH_EXTENSION_ACTIONS "/MenuBar/File/Extension Actions" #define POPUP_PATH_EXTENSION_ACTIONS "/background/Before Zoom Items/Extension Actions" #define MENU_BAR_PATH "/MenuBar" +#define NEMO_ACTION_SHOW_HIDE_TERMINAL "Show Hide Terminal" #define NETWORK_URI "network:" #define COMPUTER_URI "computer:" @@ -1329,6 +1330,16 @@ open_in_terminal_other (const gchar *path) g_free (argv); } +void +action_toggle_terminal_callback (GtkAction *action, gpointer callback_data) +{ + NemoWindow *window; + NemoWindowSlot *slot; + + window = NEMO_WINDOW (callback_data); + slot = nemo_window_get_active_slot (window); + nemo_window_slot_toggle_terminal (slot, TRUE); +} static void action_open_terminal_callback(GtkAction *action, gpointer callback_data) @@ -1547,6 +1558,11 @@ static const GtkToggleActionEntry main_toggle_entries[] = { /* tooltip */ N_("Change the default visibility of the menubar"), NULL, /* is_active */ TRUE }, + /* name, stock id */ { NEMO_ACTION_SHOW_HIDE_TERMINAL, NULL, + /* label, accelerator */ N_("Show Hide _Terminal"), "F4", + /* tooltip */ N_("Toggle the visibility of the embedded terminal"), + /* callback */ G_CALLBACK (action_toggle_terminal_callback), + /* default */ FALSE }, /* name, stock id */ { "Search", "edit-find-symbolic", /* label, accelerator */ N_("_Search for Files..."), "f", /* tooltip */ N_("Search documents and folders"), @@ -1942,6 +1958,14 @@ nemo_window_initialize_menus (NemoWindow *window) g_signal_handlers_unblock_by_func (action, action_show_hidden_files_callback, window); } + /* Initialize Show Embedded Terminal toggle state */ + action = gtk_action_group_get_action (action_group, NEMO_ACTION_SHOW_HIDE_TERMINAL); + g_signal_handlers_block_by_func (action, action_toggle_terminal_callback, window); + gtk_toggle_action_set_active (GTK_TOGGLE_ACTION (action), + g_settings_get_boolean (nemo_window_state, + "terminal-visible")); + g_signal_handlers_unblock_by_func (action, action_toggle_terminal_callback, window); + g_signal_connect_object ( NEMO_WINDOW (window), "notify::sidebar-view-id", G_CALLBACK (update_side_bar_radio_buttons), window, 0); diff --git a/src/nemo-window-slot.c b/src/nemo-window-slot.c index d6dfe880d..3ff8bc731 100644 --- a/src/nemo-window-slot.c +++ b/src/nemo-window-slot.c @@ -33,6 +33,7 @@ #include "nemo-window-manage-views.h" #include "nemo-window-types.h" #include "nemo-window-slot-dnd.h" +#include "nemo-terminal-widget.h" #include @@ -45,6 +46,8 @@ #include +void nemo_window_slot_ensure_terminal_state(NemoWindowSlot *slot); + G_DEFINE_TYPE (NemoWindowSlot, nemo_window_slot, GTK_TYPE_BOX); enum { @@ -345,6 +348,9 @@ nemo_window_slot_init (NemoWindowSlot *slot) slot->cache_bar = NULL; slot->title = g_strdup (_("Loading...")); + + slot->terminal_visible = g_settings_get_boolean (nemo_window_state, "terminal-visible"); + } static void @@ -653,6 +659,12 @@ nemo_window_slot_set_content_view_widget (NemoWindowSlot *slot, /* connect new view */ nemo_window_connect_content_view (window, new_view); + + /* If terminal-visible is enabled in config, ensure terminal is initialized and visible */ + gboolean terminal_should_be_visible = g_settings_get_boolean(nemo_window_state, "terminal-visible"); + if (terminal_should_be_visible) { + g_idle_add((GSourceFunc)nemo_window_slot_ensure_terminal_state, slot); + } } } @@ -959,3 +971,113 @@ nemo_window_slot_new (NemoWindowPane *pane) return slot; } + +static void +on_terminal_visibility_changed(NemoTerminalWidget *terminal, + gboolean visible, + NemoWindowSlot *slot) +{ + // Update slot visibility state + slot->terminal_visible = visible; +} + +static void +on_terminal_directory_changed(NemoTerminalWidget *terminal, + GFile *location, + NemoWindowSlot *slot) +{ + // Skip updating file manager location if the terminal is exiting SSH + if (terminal->is_exiting_ssh) { + return; + } + + // When terminal's directory changes, update the file browser location + if (location != NULL) { + nemo_window_slot_open_location(slot, location, 0); + } +} + +/* nemo_window_slot_init_terminal: + * @slot: a #NemoWindowSlot + * + * Initializes the terminal pane for the window slot. + */ +void +nemo_window_slot_init_terminal (NemoWindowSlot *slot) +{ + if (slot->terminal_widget != NULL) { + return; + } + + // Create the terminal widget with the current location + slot->terminal_widget = nemo_terminal_widget_new_with_location (slot->location); + + // Connect signals + g_signal_connect (slot->terminal_widget, "toggle-visibility", + G_CALLBACK (on_terminal_visibility_changed), slot); + g_signal_connect (slot->terminal_widget, "change-directory", + G_CALLBACK (on_terminal_directory_changed), slot); + + nemo_terminal_widget_initialize_in_paned( + slot->terminal_widget, + GTK_WIDGET(slot->content_view), + slot->view_overlay); +} + +/* nemo_window_slot_toggle_terminal: + * @slot: a #NemoWindowSlot + * @is_manual_toggle: whether this is a user-initiated toggle (TRUE) or an automatic one (FALSE) + * + * Toggles the visibility of the terminal pane for the window slot. + */ +void +nemo_window_slot_toggle_terminal (NemoWindowSlot *slot, gboolean is_manual_toggle) +{ + if (slot->terminal_widget == NULL) { + nemo_window_slot_init_terminal(slot); + } + + // Delegate toggle to the terminal widget + if (slot->terminal_widget != NULL) { + nemo_terminal_widget_toggle_visible_with_save(slot->terminal_widget, is_manual_toggle); + slot->terminal_visible = nemo_terminal_widget_get_visible(slot->terminal_widget); + + // If terminal is now visible, ensure it's at the same location as file manager + if (slot->terminal_visible && slot->location != NULL) { + nemo_terminal_widget_set_current_location(slot->terminal_widget, slot->location); + } + } +} + +/* nemo_window_slot_update_terminal_location: + * @slot: a #NemoWindowSlot + * + * Updates the terminal's working directory to match the current location + */ +void +nemo_window_slot_update_terminal_location (NemoWindowSlot *slot) +{ + if (slot->terminal_widget != NULL && slot->location != NULL) { + nemo_terminal_widget_set_current_location(slot->terminal_widget, slot->location); + } +} + +/* nemo_window_slot_ensure_terminal_state: + * @slot: a #NemoWindowSlot + * + * Ensures the terminal is properly positioned if it's already visible + * This function is called after the content view is initialized + */ +void +nemo_window_slot_ensure_terminal_state (NemoWindowSlot *slot) +{ + gboolean terminal_visible = g_settings_get_boolean (nemo_window_state, "terminal-visible"); + + if (terminal_visible && slot->terminal_widget == NULL) { + nemo_window_slot_init_terminal(slot); + } + else if (slot->terminal_widget != NULL) { + // Let the terminal widget handle state consistency + nemo_terminal_widget_ensure_state(slot->terminal_widget); + } +} diff --git a/src/nemo-window-slot.h b/src/nemo-window-slot.h index a79cbb4e7..9c9a32dcd 100644 --- a/src/nemo-window-slot.h +++ b/src/nemo-window-slot.h @@ -28,6 +28,7 @@ #include "nemo-view.h" #include "nemo-window-types.h" #include "nemo-query-editor.h" +#include "nemo-terminal-widget.h" #define NEMO_TYPE_WINDOW_SLOT (nemo_window_slot_get_type()) #define NEMO_WINDOW_SLOT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), NEMO_TYPE_WINDOW_SLOT, NemoWindowSlotClass)) @@ -71,6 +72,13 @@ struct NemoWindowSlot { GtkWidget *cache_bar; GtkWidget *no_search_results_box; + /* Terminal pane */ + GtkWidget *terminal_pane; + NemoTerminalWidget *terminal_widget; + GtkWidget *terminal_vpaned; + gboolean terminal_visible; + int terminal_height; + guint set_status_timeout_id; guint loading_timeout_id; @@ -191,4 +199,8 @@ void nemo_window_slot_check_bad_cache_bar (NemoWindowSlot *slot); void nemo_window_slot_set_show_thumbnails (NemoWindowSlot *slot, gboolean show_thumbnails); + +void nemo_window_slot_toggle_terminal (NemoWindowSlot *slot, gboolean is_manual_toggle); +void nemo_window_slot_update_terminal_location (NemoWindowSlot *slot); + #endif /* NEMO_WINDOW_SLOT_H */ From 748e2d4bacfa46744998267cc95e77c50f79336d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Gon=C3=A7alves?= Date: Fri, 6 Jun 2025 20:35:03 -0300 Subject: [PATCH 2/7] Add space before cd and before ssh, and change background color of monokai --- src/nemo-terminal-widget.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/nemo-terminal-widget.c b/src/nemo-terminal-widget.c index 023c3bcd0..d16de8eb5 100644 --- a/src/nemo-terminal-widget.c +++ b/src/nemo-terminal-widget.c @@ -2426,7 +2426,7 @@ static const NemoTerminalColorPalette one_half_light_palette = { // Monokai theme (classic approximation) static const NemoTerminalColorPalette monokai_palette = { .foreground = {.red = 0.929, .green = 0.925, .blue = 0.910, .alpha = 1.0}, // f8f8f2 - .background = {.red = 0.153, .green = 0.157, .blue = 0.149, .alpha = 1.0}, // 272822 + .background = {.red = 0, .green = 0, .blue = 0, .alpha = 1.0}, // 000000 .palette = { {.red = 0.153, .green = 0.157, .blue = 0.149, .alpha = 1.0}, /* Black (bg) 272822 */ {.red = 0.980, .green = 0.149, .blue = 0.450, .alpha = 1.0}, /* Red f92672 */ @@ -2681,7 +2681,7 @@ build_ssh_command_string(const gchar *hostname, const gchar *username, const gch // GString struct itself is managed by the g_string_free call at the end when stealing the buffer. // Do NOT use g_autofree on cmd_builder here, as g_string_free(..., FALSE) frees the struct. - GString *cmd_builder = g_string_new("ssh "); + GString *cmd_builder = g_string_new(" ssh "); // Append username if provided if (username != NULL && *username != '\0') @@ -2957,7 +2957,7 @@ feed_cd_command(VteTerminal *terminal, const char *path) g_autofree gchar *quoted_path = g_shell_quote(path); // Use \r (carriage return) to execute, some shells might prefer \n. \r is common. - g_autofree gchar *cd_command_str = g_strdup_printf("cd %s\r", quoted_path); + g_autofree gchar *cd_command_str = g_strdup_printf(" cd %s\r", quoted_path); if (!cd_command_str) { g_warning("feed_cd_command: Failed to create cd command string for path: %s", path); @@ -2983,4 +2983,4 @@ feed_cd_command(VteTerminal *terminal, const char *path) vte_terminal_feed_child(terminal, "\x01", -1); // Ctrl+A vte_terminal_feed_child(terminal, "\033[3~", -1); // Delete char (the space) vte_terminal_feed_child(terminal, "\x05", -1); // Ctrl+E (end of line) -} \ No newline at end of file +} From 5f1f7186684834bb5fb5c61e45eabf921f9cd9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Gon=C3=A7alves?= Date: Fri, 6 Jun 2025 21:24:10 -0300 Subject: [PATCH 3/7] Add space before automatic exit and export on ssh --- src/nemo-terminal-widget.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/nemo-terminal-widget.c b/src/nemo-terminal-widget.c index d16de8eb5..7d836068c 100644 --- a/src/nemo-terminal-widget.c +++ b/src/nemo-terminal-widget.c @@ -204,14 +204,14 @@ _initiate_ssh_connection(NemoTerminalWidget *self, { // Remove trailing '\n' from ssh_command_line_nl and append "; exit\n" GString *str_builder = g_string_new_len(ssh_command_line_nl, strlen(ssh_command_line_nl) - 1); - g_string_append(str_builder, "; exit\n"); + g_string_append(str_builder, "; exit\n"); final_command_to_feed = g_string_free(str_builder, FALSE); } else { // Fallback: Should not happen if build_ssh_command_string is consistent g_warning("_initiate_ssh_connection: ssh_command_line_nl did not end with newline as expected."); - final_command_to_feed = g_strconcat(ssh_command_line_nl, "; exit\n", NULL); + final_command_to_feed = g_strconcat(ssh_command_line_nl, "; exit\n", NULL); } g_free(ssh_command_line_nl); // Free the original command string @@ -344,7 +344,7 @@ on_ssh_exit_activate(GtkWidget *widget, gpointer user_data) // Send "exit\n" to the terminal. This should terminate the remote shell. // The "; exit" part of the original ssh command will then cause the local // child process (that ran ssh) to exit, triggering on_terminal_child_exited. - vte_terminal_feed_child(self->terminal, "exit\n", -1); + vte_terminal_feed_child(self->terminal, " exit\n", -1); // Proactively reset the terminal state. on_terminal_child_exited will see // is_exiting_ssh = TRUE and will not attempt another reset. @@ -2917,7 +2917,7 @@ on_terminal_contents_changed(VteTerminal *terminal, self->pending_ssh_sync_mode == NEMO_TERMINAL_SYNC_TERM_TO_FM) { // Simple PROMPT_COMMAND for OSC7. - const char *osc7_export_cmd = "export PROMPT_COMMAND='echo -en \"\\033]7;file://$PWD\\007\"'\n"; + const char *osc7_export_cmd = " export PROMPT_COMMAND='echo -en \"\\033]7;file://$PWD\\007\"'\n"; vte_terminal_feed_child(self->terminal, osc7_export_cmd, -1); } From 6f78407c09534ff961391d605bd799df05ef3f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Gon=C3=A7alves?= Date: Sun, 8 Jun 2025 05:44:51 -0300 Subject: [PATCH 4/7] Solve some Gtk WARNING --- src/nemo-terminal-widget.c | 91 +++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 27 deletions(-) diff --git a/src/nemo-terminal-widget.c b/src/nemo-terminal-widget.c index 7d836068c..fb38b37b1 100644 --- a/src/nemo-terminal-widget.c +++ b/src/nemo-terminal-widget.c @@ -131,9 +131,10 @@ static GtkWidget *_add_radio_menu_item_with_data(GtkMenuShell *menu_shell, gpointer user_data); /* GObject Lifecycle */ +static void nemo_terminal_widget_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec); +static void nemo_terminal_widget_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); static void nemo_terminal_widget_finalize(GObject *object); - /** * on_ssh_connect_activate: * @widget: The GtkMenuItem that was activated. @@ -1292,8 +1293,7 @@ on_terminal_button_press(GtkWidget *widget, if (event->button == GDK_BUTTON_SECONDARY && event->type == GDK_BUTTON_PRESS) { GtkWidget *menu = create_terminal_popup_menu(self); - gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, - event->button, event->time); + gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)event); return TRUE; } return FALSE; @@ -1514,42 +1514,79 @@ nemo_terminal_widget_class_init(NemoTerminalWidgetClass *klass) GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamFlags flags; - /* Signals */ + object_class->set_property = nemo_terminal_widget_set_property; + object_class->get_property = nemo_terminal_widget_get_property; + object_class->finalize = nemo_terminal_widget_finalize; + + flags = G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY; + + properties[PROP_CURRENT_LOCATION] = + g_param_spec_object("current-location", + "Current Location", + "The GFile representing the current directory.", + G_TYPE_FILE, + flags); + + g_object_class_install_property(object_class, PROP_CURRENT_LOCATION, properties[PROP_CURRENT_LOCATION]); + signals[CHANGE_DIRECTORY] = - g_signal_new("change-directory", // Signal name - G_TYPE_FROM_CLASS(klass), // Owner class type - G_SIGNAL_RUN_LAST, // Default emission stage - 0, // Class offset (0 for default) - NULL, NULL, // Accumulator and marshaller data - g_cclosure_marshal_VOID__OBJECT, // Default C marshaller (void function, object param) - G_TYPE_NONE, // Return type - 1, // Number of parameters - G_TYPE_FILE); // Parameter 1 type: GFile* + g_signal_new("change-directory", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, + G_TYPE_FILE); signals[TOGGLE_VISIBILITY] = g_signal_new("toggle-visibility", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, - g_cclosure_marshal_VOID__BOOLEAN, // Default C marshaller (void function, boolean param) + g_cclosure_marshal_VOID__BOOLEAN, G_TYPE_NONE, 1, - G_TYPE_BOOLEAN); // Parameter 1 type: gboolean + G_TYPE_BOOLEAN); +} - /* Properties */ - flags = G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY; +static void +nemo_terminal_widget_set_property(GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(object); - properties[PROP_CURRENT_LOCATION] = - g_param_spec_object("current-location", // Property name - "Current Location", // Nickname - "The GFile representing the current directory.", // Blurb - G_TYPE_FILE, // Property type - flags); // Property flags + switch (prop_id) + { + case PROP_CURRENT_LOCATION: + nemo_terminal_widget_set_current_location(self, g_value_get_object(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} - g_object_class_install_property(object_class, PROP_CURRENT_LOCATION, properties[PROP_CURRENT_LOCATION]); +static void +nemo_terminal_widget_get_property(GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(object); - /* Override finalize method */ - object_class->finalize = nemo_terminal_widget_finalize; + switch (prop_id) + { + case PROP_CURRENT_LOCATION: + g_value_set_object(value, self->current_location); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } } /** @@ -1750,7 +1787,7 @@ spawn_terminal_in_widget(NemoTerminalWidget *self) working_directory, // Working directory (can be NULL for default) argv, // Command and arguments (char **)env, // Environment variables (can be NULL for current) - G_SPAWN_SEARCH_PATH | G_SPAWN_CHILD_INHERITS_STDIN, // Spawn flags + G_SPAWN_SEARCH_PATH, // Spawn flags NULL, NULL, // Child setup function and data (unused) &child_pid, // Returns child PID (unused by us directly) NULL, // Cancellable (unused) From 23ff585f73b3e420f1478c211d36dc140142479b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Gon=C3=A7alves?= Date: Wed, 16 Jul 2025 12:16:58 -0300 Subject: [PATCH 5/7] Better code to implement embedded terminal --- libnemo-private/org.nemo.gschema.xml | 65 +- libnemo-private/org.nemo.gschema.xml.txt | 934 ++++++ src/nemo-terminal-widget.c | 3807 +++++++--------------- src/nemo-terminal-widget.h | 83 +- src/nemo-window-menus.c | 2 +- src/nemo-window-slot.c | 178 +- src/nemo-window-slot.h | 8 +- 7 files changed, 2219 insertions(+), 2858 deletions(-) create mode 100644 libnemo-private/org.nemo.gschema.xml.txt diff --git a/libnemo-private/org.nemo.gschema.xml b/libnemo-private/org.nemo.gschema.xml index d909407c7..04f0f109a 100644 --- a/libnemo-private/org.nemo.gschema.xml +++ b/libnemo-private/org.nemo.gschema.xml @@ -644,36 +644,6 @@ Whether the navigation window should be maximized. Whether the navigation window should be maximized by default. - - 'both' - Local terminal folder synchronization mode - Determines how the embedded terminal synchronizes its current directory with the file manager when navigating local folders. 'none': No synchronization. 'fm-to-term': File manager navigation changes terminal directory. 'term-to-fm': Terminal navigation changes file manager location. 'both': Synchronization works in both directions. - - - 'off' - SSH terminal auto-connection and synchronization mode preference - Determines behavior when an SFTP location is active. 'off': Do not connect automatically. 'sync-both': Connect and sync both ways. 'sync-fm-to-term': Connect and sync File Manager to Terminal. 'sync-term-to-fm': Connect and sync Terminal to File Manager. 'sync-none': Connect without folder synchronization. - - - 300 - Terminal panel height - Height of the terminal panel in pixels. - - - false - Terminal pane visibility - Whether the terminal pane should be visible. - - - 'system' - Terminal color scheme - The color scheme to use for the embedded terminal. Available values: 'system', 'dark', 'light', 'solarized-dark', 'solarized-light', 'matrix', 'one-half-dark', 'one-half-light', 'monokai', 'custom'. - - - 12 - Terminal font size - The font size to use for the embedded terminal in point units. - 170 Width of the side pane @@ -742,6 +712,41 @@ Side pane view The side pane view to show in newly opened windows. + + false + Terminal pane visibility + Whether the terminal pane should be visible. + + + 200 + Terminal panel height + Height of the terminal panel in pixels. + + + '' + Terminal font + The font to use for the terminal. If empty, the system monospace font is used. + + + 12 + Terminal font size + The font size to use for the embedded terminal in point units. + + + 'system' + Terminal color scheme + The color scheme to use for the embedded terminal. Available values: 'system', 'dark', 'light', 'solarized-dark', 'solarized-light', 'matrix', 'one-half-dark', 'one-half-light', 'monokai'. + + + 'both' + Local terminal folder synchronization mode + Determines how the embedded terminal synchronizes its current directory with the file manager when navigating local folders. 'none': No synchronization. 'fm-to-term': File manager navigation changes terminal directory. 'term-to-fm': Terminal navigation changes file manager location. 'both': Synchronization works in both directions. + + + 'off' + SSH terminal auto-connection and synchronization mode preference + Determines behavior when an SFTP location is active. 'off': Do not connect automatically. 'sync-both': Connect and sync both ways. 'sync-fm-to-term': Connect and sync File Manager to Terminal. 'sync-term-to-fm': Connect and sync Terminal to File Manager. 'sync-none': Connect without folder synchronization. + diff --git a/libnemo-private/org.nemo.gschema.xml.txt b/libnemo-private/org.nemo.gschema.xml.txt new file mode 100644 index 000000000..d909407c7 --- /dev/null +++ b/libnemo-private/org.nemo.gschema.xml.txt @@ -0,0 +1,934 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'after-current-tab' + Where to position newly open tabs in browser windows. + If set to "after-current-tab", then new tabs are inserted after the current tab. If set to "end", then new tabs are appended to the end of the tab list. + + + true + Enables the classic Nemo behavior, where all windows are browsers + If set to true, then all Nemo windows will be browser windows. This is how Nemo used to behave before version 2.6, and some people prefer this behavior. + + + false + Enables renaming of icons by two times clicking with pause between clicks + If set to true, then icons in all Nemo windows will be able to get renamed quickly. Users should click two times on icons with a pause time more than double-click time of their system. + + + true + During drag-and-drop operations, automatically expand rows when hovering them briefly + + + false + Show the location entry by default + If set to true, then Nemo browser windows will show a textual input entry for the location toolbar. + + + true + Show Previous button in nemo toolbar + If set to true, then Nemo browser windows will show the button. + + + true + Show Next button in nemo toolbar + If set to true, then Nemo browser windows will show the button. + + + true + Show Up button in nemo toolbar + If set to true, then Nemo browser windows will show the button. + + + false + Show refresh button in nemo toolbar + If set to true, then Nemo browser windows will show the button. + + + true + Show toggle button location entry/pathbar + If set to true, then Nemo browser windows will show the button. + + + false + Show Home button in nemo toolbar + If set to true, then Nemo browser windows will show the button. + + + false + Show Computer button in nemo toolbar + If set to true, then Nemo browser windows will show the button. + + + true + Show Search button in nemo toolbar + If set to true, then Nemo browser windows will show the button. + + + false + Show new folder button in nemo toolbar + If set to true, then Nemo browser windows will show the button. + + + false + Show open in terminal in the nemo toolbar + If set to true, then Nemo browser windows will show the button. + + + true + Show Icon View button in nemo toolbar + If set to true, then Nemo browser windows will show the button. + + + true + Show List View button in nemo toolbar + If set to true, then Nemo browser windows will show the button. + + + true + Show Compact View button in nemo toolbar + If set to true, then Nemo browser windows will show the button. + + + false + Show Thumbnails button in nemo toolbar + If set to true, then Nemo browser windows will show the button. + + + true + Show warning when opening as root + If set to true, then Nemo show warning message when run Nemo as root user. + + + false + Whether to ask for confirmation when moving files to Trash + If set to true, then Nemo will ask for confirmation when you attempt to move files to the Trash. + + + true + Whether to ask for confirmation when deleting files, or emptying Trash + If set to true, then Nemo will ask for confirmation when you attempt to delete files, or empty the Trash. + + + true + Whether to enable immediate deletion + If set to true, then Nemo will have a feature allowing you to delete a file immediately and in-place, instead of moving it to the trash. This feature can be dangerous, so use caution. + + + false + Whether to swap the hotkeys for Trash and Delete + If set to true, the Delete key will permanently delete a file, and the Shift-Delete key will only trash a file. + + + + 'local-only' + When to show number of items in a folder + Speed tradeoff for when to show the number of items in a folder. If set to "always" then always show item counts, even if the folder is on a remote server. If set to "local-only" then only show counts for local file systems. If set to "never" then never bother to compute item counts. + + + 'double' + Type of click used to launch/open files + Possible values are "single" to launch files on a single click, or "double" to launch them on a double click. + + + 'ask' + What to do with executable text files when activated + What to do with executable text files when they are activated (single or double clicked). Possible values are "launch" to launch them as programs, "ask" to ask what to do via a dialog, and "display" to display them as text files. + + + ['xviewer','feh','sxiv'] + Image viewer executables to pass sort order to + When opening a single image with an application in this list, allow that viewer to display other images in the current nemo view, in the presented order (including searches). Other image viewers can be added, but are likely to ignore the order. The viewer Exec line must accept a list of files (either %U or %F). + + + true + Use extra mouse button events in Nemo' browser window + For users with mice that have "Forward" and "Back" buttons, this key will determine if any action is taken inside of Nemo when either is pressed. + + + 9 + Mouse button to activate the "Forward" command in browser window + For users with mice that have buttons for "Forward" and "Back", this key will set which button activates the "Forward" command in a browser window. Possible values range between 6 and 14. + + + 8 + Mouse button to activate the "Back" command in browser window + For users with mice that have buttons for "Forward" and "Back", this key will set which button activates the "Back" command in a browser window. Possible values range between 6 and 14. + + + + 'local-only' + When to show thumbnails of image files + Speed tradeoff for when to show an image file as a thumbnail. If set to "always" then always thumbnail, even if the folder is on a remote server. If set to "local-only" then only show thumbnails for local file systems. If set to "never" then never bother to thumbnail images, just use a generic icon. + + + false + Inherit thumbnail visibility from parent + If set, folders will inherit their thumbnail visibility from their parents + + + 1048576 + Maximum image size for thumbnailing + Images over this size (in bytes) won't be thumbnailed. The purpose of this setting is to avoid thumbnailing large images that may take a long time to load or use lots of memory. + + + false + Show advanced permissions in the file property dialog + If set to true, then Nemo lets you edit and display file permissions in a more unix-like way, accessing some more esoteric options. + + + true + Show folders first in windows + If set to true, then Nemo shows folders prior to showing files in the icon and list views. + + + true + Show favorites first in windows + If set to true, then Nemo shows favorites prior to other files in the icon and list views. + + + + + + 'name' + Default sort order + The default sort-order for items in the icon view. Possible values are "name", "size", "type" and "mtime". + + + false + Reverse sort order in new windows + If true, files in new windows will be sorted in reverse order. ie, if sorted by name, then instead of sorting the files from "a" to "z", they will be sorted from "z" to "a"; if sorted by size, instead of being incrementally they will be sorted decrementally. + + + false + Nemo uses the users home folder as the desktop + If set to true, then Nemo will use the user's home folder as the desktop. If it is false, then it will use ~/Desktop as the desktop. + + + + + + + + 'icon-view' + Default folder viewer + When a folder is visited this viewer is used unless you have selected another view for that particular folder. Possible values are "list-view", "icon-view" and "compact-view". + + + false + Inherit the view type (icon, compact, list) from parent to children + When a folder is visited the viewer is inherited from that folder's parent unless you have selected another view for that particular folder. + + + 'locale' + Date Format + The format of file dates. Possible values are "locale", "iso", and "informal". + + + 'auto-mono' + The font to use for the date/time columns. + The format of file dates. Possible values are "auto-mono" (best effort to match the application font), "system-mono" (use the current system mono font), and "no-mono" (use a normal font). + + + false + Whether to show hidden files + If set to true, then hidden files are shown by default in the file manager. Hidden files are either dotfiles, listed in the folder's .hidden file or backup files ending with a tilde (~). + + + false + Whether to show the full path of the current view in the title bar and tab bars + If set to true, will show the normal title of a window or tab, followed by the full path to that location in parentheses. + + + [] + Bulk rename utility + If set, Nemo will append URIs of selected files and treat the result as a command line for bulk renaming. Bulk rename applications can register themselves in this key by setting the key to a space-separated string of their executable name and any command line options. If the executable name is not set to a full path, it will be searched for in the search path. + + + 'base-10' + Prefixes used for file sizes + Determines whether Nemo uses base-10, base-10 long, base-2 or base-2 long file size prefixes + + + false + Whether to close a view of a removeable device instead of navigating Home + If set to true, a view open for a removeable device will be closed instead of sent Home if the device is ejected + + + false + Whether to default to showing dual-pane view when a new window is opened + If set to true, new Nemo windows will default to showing two panes + + + false + Whether to ignore folder metadata for view zoom levels and layouts + If set to true, views will not change according to their metadata, but stay consistent for the life of that window + + + true + Whether to list bookmarks in the Move To/Copy To menus + If set to true, bookmarks will be listed in the MoveTo/CopyTo menus + + + true + Whether to list places in the Move To/Copy To menus + If set to true, places will be listed in the MoveTo/CopyTo menus + + + false + deprecated - no longer used + + + false + Show tooltips for desktop items + If true, tooltips will be displayed for desktop items. + + + false + Show tooltips when hovering on items in an icon or compact view + If true, tooltips will be displayed for icon and compact view items + + + false + Show tooltips when hovering on items in a list view + If true, tooltips will be displayed for list view items + + + false + Show detailed file type in tooltip + If true, tooltips will show a detailed file type. + + + false + Show file modified date in tooltip + If true, tooltips will show their modified date. + + + false + Show file accessed date in tooltip + If true, tooltips will show their accessed date. + + + false + Show file creation (birth) date in tooltip + If true, tooltips will show their creation date. + + + false + Show full path in tooltip + If true, tooltips will show the file's full path. + + + false + Don't show the explainer message when turning off the main menu + If true, you will no longer recieve a popup explaining how to reactivate the main menu once you've hidden it + + + 2 + Last server connect method used + + + false + If true, all file operations will start immediately + + + false + If true, double click left on blank area will go to parent folder + + + false + Display the 'Make executable and run' button in the mime-action dialog (open an unknown filetype) + + + 150 + Maximum number of files to preload deferred attributes for when opening a directory + Certain file attributes (like thumbnail and extension info) are deferred until a folder finishes loading. This number specifies how many files to skip this behavior on so that smaller folders won't have an obvious delay when loading these attributes. + + + false + Suppress any safeguards when running nemo/nemo-desktop as the root user. For some systems there is only a root user. + + + true + If true, enable detection of the type of content of a mounted media and display a suggested application to open the media. + + + -1 + Number of threads to dedicate to thumbnailing. -1 to let the program decide. The maximum allowed threads is half the number of logical processors, regardless of what is set here. If you change this setting you must restart Nemo for it to take effect. + + + + + + [ 'none', 'size', 'date_modified' ] + List of possible captions on icons + A list of captions below an icon in the icon view and + the desktop. The actual number of captions shown depends on + the zoom level. Some possible values are: + "size", "type", "date_modified", "date_changed", "date_accessed", "owner", + "group", "permissions", "octal_permissions" and "mime_type". + + + false + deprecated - not used + + + false + Put labels beside icons + If true, labels will be placed beside icons rather than underneath them. + + + 'standard' + Default icon zoom level + Default zoom level used by the icon view. + + + 64 + Default Thumbnail Icon Size + The default size of an icon for a thumbnail in the icon view. + + + [ '3' ] + Text Ellipsis Limit + A string specifying how parts of overlong file names + should be replaced by ellipses, depending on the zoom + level. + Each of the list entries is of the form "Zoom Level:Integer". + For each specified zoom level, if the given integer is + larger than 0, the file name will not exceed the given number of lines. + If the integer is 0 or smaller, no limit is imposed on the specified zoom level. + A default entry of the form "Integer" without any specified zoom level + is also allowed. It defines the maximum number of lines for all other zoom levels. + Examples: + 0 - always display overlong file names; + 3 - shorten file names if they exceed three lines; + smallest:5,smaller:4,0 - shorten file names if they exceed five lines + for zoom level "smallest". Shorten file names if they exceed four lines + for zoom level "smaller". Do not shorten file names for other zoom levels. + + Available zoom levels: + smallest (33%), smaller (50%), small (66%), standard (100%), large (150%), + larger (200%), largest (400%) + + + + + + 'standard' + Default compact view zoom level + Default zoom level used by the compact view. + + + false + All columns have same width + If this preference is set, all columns in the compact view have the same width. Otherwise, the width of each column is determined seperately. + + + + + + 'smaller' + Default list zoom level + Default zoom level used by the list view. + + + [ 'name', 'size', 'type', 'date_modified' ] + Default list of columns visible in the list view + Default list of columns visible in the list view. + + + [ 'name', 'size', 'type', 'date_modified' ] + Default column order in the list view + Default column order in the list view. + + + false + If true, allow folders with content to be expanded in the current view. + + + + + + + + + + true + Only show folders in the tree side pane + If set to true, Nemo will only show folders in the tree side pane. Otherwise it will show both folders and files. + + + + + + 'Noto Sans 10' + Desktop font + The font description used for the icons on the desktop. + + + "true::false" + Desktop layout + Format bool:bool, show desktop folder on primary monitor:show desktop on remaining monitors + + + true + Whether to show icons from inactive monitors on another monitor + + + true + Deprecated: Allow Nemo to manage the desktop + Deprecated: If this is set to true, Nemo will autostart and manage the desktop + + + true + Which desktop view type to use + If true, the new desktop grid view will be used by nemo-desktop, otherwise the legacy view will be used. + + + 1.0 + Vertical desktop grid adjustment + Overrides the standard vertical spacing for the desktop grid, in situation where default spacing is not ideal due to label customizations. This is a value from 0.5 to 1.5, with 1.0 being no adjustment, and 0.5 being half the default spacing. + + + 1.0 + Horizontal desktop grid adjustment + Overrides the standard horizontal spacing for the desktop grid, in situation where default spacing is not ideal due to label customizations. This is a value from 0.5 to 1.5, with 1.0 being no adjustment, and 0.5 being half the default spacing. + + + false + Home icon visible on desktop + If this is set to true, an icon linking to the home folder will be put on the desktop. + + + false + Computer icon visible on desktop + If this is set to true, an icon linking to the computer location will be put on the desktop. + + + false + Trash icon visible on desktop + If this is set to true, an icon linking to the trash will be put on the desktop. + + + false + Show mounted volumes on the desktop + If this is set to true, icons linking to mounted volumes will be put on the desktop. + + + false + Network Servers icon visible on the desktop + If this is set to true, an icon linking to the Network Servers view will be put on the desktop. + + + 2 + Text Ellipsis Limit + An integer specifying how parts of overlong file names should be replaced by ellipses on the desktop. If the number is larger than 0, the file name will not exceed the given number of lines. If the number is 0 or smaller, no limit is imposed on the number of displayed lines. + + + ['conky', 'csd-background'] + List of desktop-handling to ignore when determining whether or not to manager the desktop. + Nemo checks for _NET_WM_WINDOW_TYPE_DESKTOP-type windows, and skips managing the desktop if any others are detected. Add potential names to this list to ignore when performing this check. This means that you want nemo to create its own desktop window(s) even though something else already seems to. The check is based on a program's WM_CLASS. + + + true + Fade the background on change + If set to true, then Nemo will use a fade effect to change the desktop background. + + + + + + '' + The geometry string for a navigation window. + A string containing the saved geometry and coordinates string for navigation windows. + + + false + Whether the navigation window should be maximized. + Whether the navigation window should be maximized by default. + + + 'both' + Local terminal folder synchronization mode + Determines how the embedded terminal synchronizes its current directory with the file manager when navigating local folders. 'none': No synchronization. 'fm-to-term': File manager navigation changes terminal directory. 'term-to-fm': Terminal navigation changes file manager location. 'both': Synchronization works in both directions. + + + 'off' + SSH terminal auto-connection and synchronization mode preference + Determines behavior when an SFTP location is active. 'off': Do not connect automatically. 'sync-both': Connect and sync both ways. 'sync-fm-to-term': Connect and sync File Manager to Terminal. 'sync-term-to-fm': Connect and sync Terminal to File Manager. 'sync-none': Connect without folder synchronization. + + + 300 + Terminal panel height + Height of the terminal panel in pixels. + + + false + Terminal pane visibility + Whether the terminal pane should be visible. + + + 'system' + Terminal color scheme + The color scheme to use for the embedded terminal. Available values: 'system', 'dark', 'light', 'solarized-dark', 'solarized-light', 'matrix', 'one-half-dark', 'one-half-light', 'monokai', 'custom'. + + + 12 + Terminal font size + The font size to use for the embedded terminal in point units. + + + 170 + Width of the side pane + The default width of the side pane in new windows. + + + -1 + Index of the bookmark list to jump to the dedicated sidebar bookmark section + This is an internal setting for the sidebar that tracks the index in the bookmark list that separates bookmarks in the Computer section from bookmarks in the Bookmark section. + + + true + Show toolbar in new windows + If set to true, newly opened windows will have toolbars visible. + + + true + Show location bar in new windows + If set to true, newly opened windows will have the location bar visible. + + + true + Show status bar in new windows + If set to true, newly opened windows will have the status bar visible. + + + true + Show side pane in new windows + If set to true, newly opened windows will have the side pane visible. + + + true + Show menu bar in new windows + If set to true, newly opened windows will have the menu bar visible. + + + true + Expand My Computer section in places sidebar + View state storage for My Computer in places sidebar + + + true + Expand Bookmark section in places sidebar + View state storage for Bookmarks in places sidebar + + + true + Expand Devices section in places sidebar + View state storage for My Computer in places sidebar + + + true + Expand Network section in places sidebar + View state storage for My Computer in places sidebar + + + + + + + + + + + 'places' + Side pane view + The side pane view to show in newly opened windows. + + + + + + [] + List of extensions -not- to load. + List of extension names you do -not- want to load. This maintains the behavior of an installed extension being enabled by default. + + + [] + List of NemoActions -not- to load. + List of action files you do -not- want loaded. This maintains the behavior of an installed action being enabled by default. + + + [] + List of scripts -not- to load. + List of script files you do -not- want loaded. This maintains the behavior of an installed script being enabled by default. + + + + + + + true + Show the selection context menu's Open item. + + + true + Show the selection context menu's Open in New Tab item. + + + true + Show the selection context menu's Open in New Window item. + + + true + Show the selection context menu's Scripts submenu. + + + true + Show the selection context menu's Cut item. + + + true + Show the selection context menu's Copy item. + + + true + Show the selection context menu's Paste item. + + + false + Show the selection context menu's Duplicate item. + + + true + Show the selection context menu's Pin/Unpin item. + + + true + Show the selection context menu's Favorite/Unfavorite item. + + + false + Show the selection context menu's Create Link item. + + + true + Show the selection context menu's Rename item. + + + false + Show the selection context menu's Copy To submenu. + + + false + Show the selection context menu's Move To submenu. + + + true + Show the selection context menu's Open in Terminal item. + + + true + Show the selection context menu's Open As Root item. + + + true + Show the selection context menu's Move to Trash item. + + + true + Show the selection context menu's Properties item. + + + + + true + Show the background context menu's Create New Folder item. + + + true + Show the background context menu's Scripts submenu. + + + true + Show the background context menu's Open in Terminal item. + + + true + Show the background context menu's Open as Root item. + + + true + Show the background context menu's Show Hidden Files item. + + + true + Show the background context menu's Paste item. + + + true + Show the background context menu's Properties item. + + + + + true + Show the background context menu's Arrange Items submenu (icon view only). + + + true + Show the background context menu's Organize by Name item (icon view only). + + + + + true + Show the background context menu's Customize item (new-style desktop only). + + + + + + false + Stores the most recent state of the file search regex toggle + + + false + Stores the most recent state of the content search regex toggle + + + 'pcre' + valid formats: pcre, javascript + + + false + Treat patterns as raw bytes, not utf-8 + + + false + Stores the most recent state of the file search case toggle + + + false + Stores the most recent state of the content search case toggle + + + ['/dev', '/proc', '/sys', 'dosdevices', '.git'] + Paths or folder names to never recurse into when searching + List of locations that the search engine will never enter when looking for matches. These can be absolute or simply folder names (like .git). You can still enter those folders and search inside of them, however. + + + true + Recurse into subfolders when performing a search + + + [] + Saved list of columns visible in the search view. + + + '' + Column to sort on when viewing search results + + + false + Reverse the direction of the sort when viewing search results + + + diff --git a/src/nemo-terminal-widget.c b/src/nemo-terminal-widget.c index fb38b37b1..418321ccf 100644 --- a/src/nemo-terminal-widget.c +++ b/src/nemo-terminal-widget.c @@ -18,1527 +18,367 @@ Author: Bruno Goncalves */ - #include "nemo-terminal-widget.h" - -#include -#include -#include -#include -#include "nemo-window.h" #include "nemo-window-slot.h" +#include "nemo-global-preferences.h" -/* Data keys for g_object_set_data / g_object_get_data */ -static const gchar * const DATA_KEY_SSH_HOSTNAME = "ssh-hostname"; -static const gchar * const DATA_KEY_SSH_USERNAME = "ssh-username"; -static const gchar * const DATA_KEY_SSH_PORT = "ssh-port"; -static const gchar * const DATA_KEY_SSH_SYNC_MODE = "ssh-sync-mode"; -static const gchar * const DATA_KEY_SCHEME_NAME = "scheme-name"; -static const gchar * const DATA_KEY_FONT_SIZE = "font-size"; -static const gchar * const DATA_KEY_LOCAL_SYNC_MODE = "local-sync-mode"; -static const gchar * const DATA_KEY_SFTP_AUTO_CONNECT_MODE = "sftp-auto-connect-mode"; - -/* Forward declarations */ - -/* Action Callbacks */ -static void on_copy_activate (GSimpleAction *action, - GVariant *parameter, - gpointer user_data); -static void on_paste_activate (GSimpleAction *action, - GVariant *parameter, - gpointer user_data); -static void on_select_all_activate (GSimpleAction *action, - GVariant *parameter, - gpointer user_data); - -/* Terminal Event Callbacks */ -static gboolean on_terminal_button_press(GtkWidget *widget, - GdkEventButton *event, - gpointer user_data); -static gboolean on_terminal_key_press(GtkWidget *widget, - GdkEventKey *event, - gpointer user_data); -static void on_terminal_contents_changed(VteTerminal *terminal, - gpointer user_data); -static void on_terminal_directory_changed(VteTerminal *terminal, - gpointer user_data); -static void on_terminal_child_exited(VteTerminal *terminal, - gint status, - gpointer user_data); - -/* Menu Item Callbacks */ -static void on_color_scheme_changed(GtkWidget *widget, - gpointer user_data); -static void on_font_size_changed(GtkWidget *widget, - gpointer user_data); -static void on_local_sync_mode_changed(GtkWidget *widget, - gpointer user_data); -static void on_sftp_auto_connect_behavior_changed(GtkWidget *widget, - gpointer user_data); -static void on_ssh_connect_activate(GtkWidget *widget, /* GtkMenuItem */ - gpointer user_data); -static void on_ssh_exit_activate(GtkWidget *widget, /* GtkMenuItem */ - gpointer user_data); -static void _on_menu_item_activate_widget_action (GtkMenuItem *menuitem, - gpointer user_data); - -/* SSH Helper Functions */ -static void _initiate_ssh_connection(NemoTerminalWidget *self, - const gchar *hostname, - const gchar *username, - const gchar *port, - NemoTerminalSyncMode sync_mode); -static void clear_ssh_state(NemoTerminalWidget *self); -static gchar *build_ssh_command_string(const gchar *hostname, - const gchar *username, - const gchar *port); -static gboolean parse_gvfs_ssh_path(GFile *location, - gchar **hostname, - gchar **username, - gchar **port); -static gchar *get_remote_path_from_sftp_gfile(GFile *location); - -/* Directory and Command Helper Functions */ -static void change_directory_in_terminal(NemoTerminalWidget *self, GFile *location); -static void feed_cd_command(VteTerminal *terminal, const char *path); - -/* UI and State Helper Functions */ -static void setup_terminal_font(VteTerminal *terminal); -static int nemo_terminal_get_font_size(void); -static void nemo_terminal_widget_save_font_size(NemoTerminalWidget *self, int font_size); -static void reset_terminal_to_current_location(NemoTerminalWidget *self); -static gboolean focus_once_and_remove(gpointer widget_data); -static gboolean reset_toggling_flag(gpointer user_data); -static GtkWidget * create_terminal_popup_menu(NemoTerminalWidget *self); -static void on_container_size_changed(GtkPaned *paned, GParamSpec *pspec, gpointer user_data); -static void on_paned_destroy(GtkWidget *widget, gpointer user_data); -static gboolean apply_initial_size_idle(gpointer user_data); -static void _add_action_menu_item_compat(GtkMenuShell *menu_shell, - NemoTerminalWidget *self, - const gchar *label, - const gchar *detailed_action_name); -static void _add_callback_menu_item(GtkMenuShell *menu_shell, - const gchar *label, - GCallback callback, - gpointer user_data); -static GtkWidget *_add_radio_menu_item_with_data(GtkMenuShell *menu_shell, - GSList **radio_group, - const gchar *label, - const gchar *data_key, - gpointer data_value, - gboolean is_active, - GCallback activate_callback, - gpointer user_data); - -/* GObject Lifecycle */ -static void nemo_terminal_widget_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec); -static void nemo_terminal_widget_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); -static void nemo_terminal_widget_finalize(GObject *object); - -/** - * on_ssh_connect_activate: - * @widget: The GtkMenuItem that was activated. - * @user_data: The #NemoTerminalWidget instance. - * - * Callback for when an SSH connection menu item is activated. - * Retrieves SSH connection details from the menu item's data and - * initiates the SSH connection. - */ -static void -on_ssh_connect_activate(GtkWidget *widget, - gpointer user_data) -{ - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); - - // Retrieve connection parameters stored on the menu item - const gchar *hostname = g_object_get_data(G_OBJECT(widget), DATA_KEY_SSH_HOSTNAME); - const gchar *username = g_object_get_data(G_OBJECT(widget), DATA_KEY_SSH_USERNAME); - const gchar *port = g_object_get_data(G_OBJECT(widget), DATA_KEY_SSH_PORT); - NemoTerminalSyncMode sync_mode = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), DATA_KEY_SSH_SYNC_MODE)); - - if (hostname != NULL) - { - _initiate_ssh_connection(self, hostname, username, port, sync_mode); - } - else - { - g_warning("Hostname not found on SSH menu item for manual connection."); - } -} - -/** - * _initiate_ssh_connection: - * @self: The #NemoTerminalWidget instance. - * @hostname: The hostname to connect to. - * @username: (Optional) The username for the SSH connection. - * @port: (Optional) The port for the SSH connection. - * @sync_mode: The directory synchronization mode to use for this SSH session. - * - * Builds and executes the SSH command in the terminal. Sets up SSH state - * variables and prepares for directory synchronization if configured. - * The command fed to the terminal includes "; exit" to ensure the shell - * process hosting the ssh client exits, triggering on_terminal_child_exited. - */ -static void -_initiate_ssh_connection(NemoTerminalWidget *self, - const gchar *hostname, - const gchar *username, - const gchar *port, - NemoTerminalSyncMode sync_mode) -{ - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); - g_return_if_fail(hostname != NULL && *hostname != '\0'); - - // Build the basic "ssh user@host -p port\n" command - gchar *ssh_command_line_nl = build_ssh_command_string(hostname, username, port); - if (!ssh_command_line_nl) - { - g_warning("Failed to build SSH command string."); - return; - } - - // Append "; exit" to the SSH command. - // This ensures that when the ssh client process finishes, the local shell - // running it also exits, which correctly triggers 'on_terminal_child_exited'. - g_autofree gchar *final_command_to_feed = NULL; - if (g_str_has_suffix(ssh_command_line_nl, "\n")) - { - // Remove trailing '\n' from ssh_command_line_nl and append "; exit\n" - GString *str_builder = g_string_new_len(ssh_command_line_nl, strlen(ssh_command_line_nl) - 1); - g_string_append(str_builder, "; exit\n"); - final_command_to_feed = g_string_free(str_builder, FALSE); - } - else - { - // Fallback: Should not happen if build_ssh_command_string is consistent - g_warning("_initiate_ssh_connection: ssh_command_line_nl did not end with newline as expected."); - final_command_to_feed = g_strconcat(ssh_command_line_nl, "; exit\n", NULL); - } - g_free(ssh_command_line_nl); // Free the original command string - - if (!final_command_to_feed) { - g_warning("Failed to build final SSH command string with ; exit."); - return; - } - - // Mark that we're in the process of connecting via SSH. - // Full setup (like cd to remote path) happens after connection is established (see on_terminal_contents_changed). - self->ssh_connecting = TRUE; - self->pending_ssh_sync_mode = sync_mode; - - // Set SSH mode and store connection details - self->in_ssh_mode = TRUE; - self->ssh_sync_mode = sync_mode; // Set the determined sync mode for this session - g_free(self->ssh_hostname); - self->ssh_hostname = g_strdup(hostname); - g_free(self->ssh_username); - self->ssh_username = g_strdup(username); - g_free(self->ssh_port); - self->ssh_port = g_strdup(port); - - // If current location is SFTP, try to get remote path for potential `cd` after connection - if (self->current_location && G_IS_FILE(self->current_location)) - { - g_free(self->ssh_remote_path); - self->ssh_remote_path = get_remote_path_from_sftp_gfile(self->current_location); - } - - // Show SSH indicator in the UI - if (self->ssh_indicator) - { - gtk_widget_show(self->ssh_indicator); - } - - // Feed the complete command (e.g., "ssh user@host; exit\n") to the terminal - vte_terminal_feed_child(self->terminal, final_command_to_feed, -1); -} - -/** - * clear_ssh_state: - * @self: The #NemoTerminalWidget instance. - * - * Clears all SSH-related state variables in the widget, - * effectively ending the SSH mode. - */ -static void -clear_ssh_state(NemoTerminalWidget *self) -{ - if (self->in_ssh_mode || self->ssh_connecting) - { - self->in_ssh_mode = FALSE; - self->ssh_sync_mode = NEMO_TERMINAL_SYNC_NONE; // Reset to default - g_clear_pointer(&self->ssh_hostname, g_free); - g_clear_pointer(&self->ssh_username, g_free); - g_clear_pointer(&self->ssh_port, g_free); - g_clear_pointer(&self->ssh_remote_path, g_free); - self->ssh_connecting = FALSE; // Ensure this is reset - self->pending_ssh_sync_mode = NEMO_TERMINAL_SYNC_NONE; - } -} - -/** - * reset_terminal_to_current_location: - * @self: The #NemoTerminalWidget instance. - * - * Resets the terminal state, typically after an SSH session ends. - * It clears SSH state, hides the SSH indicator, updates the terminal's - * current location to match the file manager's active native location, - * and spawns a new local shell. - */ -static void -reset_terminal_to_current_location(NemoTerminalWidget *self) -{ - NemoWindowSlot *slot = NULL; - NemoWindow *win = NULL; - - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); - - // Ignore the next cd signal that might be emitted by the shell startup - self->ignore_next_terminal_cd_signal = TRUE; - - // Hide SSH indicator - if (self->ssh_indicator) { - gtk_widget_hide(self->ssh_indicator); - } - - // Attempt to get the active Nemo window and slot to find the current FM location - if (self->container_paned) { - GtkWidget *toplevel = gtk_widget_get_toplevel(GTK_WIDGET(self->container_paned)); - if (toplevel && NEMO_IS_WINDOW(toplevel)) { - win = NEMO_WINDOW(toplevel); - slot = nemo_window_get_active_slot(win); - } - } - - // Update current_location to the file manager's active native path - if (slot && slot->location && G_IS_FILE(slot->location) && g_file_is_native(slot->location)) { - g_set_object(&self->current_location, slot->location); - } else { - // Fallback to no specific location (will use home or default) - g_set_object(&self->current_location, NULL); - } - - clear_ssh_state(self); // Crucial: resets SSH mode flags and data - spawn_terminal_in_widget(self); // Spawn a new local shell -} - -/** - * on_ssh_exit_activate: - * @widget: The GtkMenuItem that was activated ("Disconnect from SSH"). - * @user_data: The #NemoTerminalWidget instance. - * - * Handles the user's request to disconnect from an active SSH session. - * Feeds an "exit" command to the terminal (intended for the remote shell) - * and then resets the terminal to a local state. - */ -static void -on_ssh_exit_activate(GtkWidget *widget, gpointer user_data) -{ - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); - - if (!self->in_ssh_mode) return; // Not in SSH mode, nothing to exit - - self->is_exiting_ssh = TRUE; // Flag to manage state in on_terminal_child_exited - self->ignore_next_terminal_cd_signal = TRUE; // Ignore cd from shell startup - - // Send "exit\n" to the terminal. This should terminate the remote shell. - // The "; exit" part of the original ssh command will then cause the local - // child process (that ran ssh) to exit, triggering on_terminal_child_exited. - vte_terminal_feed_child(self->terminal, " exit\n", -1); - - // Proactively reset the terminal state. on_terminal_child_exited will see - // is_exiting_ssh = TRUE and will not attempt another reset. - reset_terminal_to_current_location(self); - - self->is_exiting_ssh = FALSE; // Reset the flag -} - -/* Action entries for the terminal (copy, paste, select-all) */ -static GActionEntry terminal_entries[] = { - {"copy", on_copy_activate, NULL, NULL, NULL}, - {"paste", on_paste_activate, NULL, NULL, NULL}, - {"select-all", on_select_all_activate, NULL, NULL, NULL}, -}; - -/* GObject properties */ -enum -{ - PROP_0, - PROP_CURRENT_LOCATION, - N_PROPS -}; - -static GParamSpec *properties[N_PROPS]; - -/* GObject signals */ -enum -{ - CHANGE_DIRECTORY, - TOGGLE_VISIBILITY, - LAST_SIGNAL -}; - -static guint signals[LAST_SIGNAL]; - -G_DEFINE_TYPE(NemoTerminalWidget, nemo_terminal_widget, GTK_TYPE_BOX) - -/*** Helper structs for menu creation ***/ -typedef struct { - const gchar *id; // Internal identifier for the scheme - const gchar *label_pot; // Translatable label (e.g., N_("System")) -} MenuSchemeEntry; - -static const MenuSchemeEntry COLOR_SCHEME_ENTRIES[] = { - {"system", N_("System")}, - {"dark", N_("Dark")}, - {"light", N_("Light")}, - {"solarized-dark", N_("Solarized Dark")}, - {"solarized-light", N_("Solarized Light")}, - {"matrix", N_("Matrix")}, - {"one-half-dark", N_("One Half Dark")}, - {"one-half-light", N_("One Half Light")}, - {"monokai", N_("Monokai")}, -}; - -typedef struct { - int size_pts; // Font size in points -} MenuFontSizeEntry; - -static const MenuFontSizeEntry FONT_SIZE_ENTRIES[] = { - {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}, - {17}, {18}, {20}, {22}, {24}, {28}, {32}, {36}, {40}, {48} -}; - -typedef struct { - NemoTerminalSyncMode mode; // Sync mode enum value - const gchar *label_pot; // Translatable label -} MenuSyncModeEntry; - -static const MenuSyncModeEntry LOCAL_SYNC_MODE_ENTRIES[] = { - {NEMO_TERMINAL_SYNC_BOTH, N_("Sync Both Ways")}, - {NEMO_TERMINAL_SYNC_FM_TO_TERM, N_("Sync File Manager → Terminal")}, - {NEMO_TERMINAL_SYNC_TERM_TO_FM, N_("Sync Terminal → File Manager")}, - {NEMO_TERMINAL_SYNC_NONE, N_("No Sync")} -}; - -typedef struct { - NemoTerminalSshAutoConnectMode mode; // Auto-connect mode enum - const gchar *label_pot; // Translatable label - NemoTerminalSyncMode sync_mode_for_connection; // Sync mode to use if auto-connecting -} MenuSshAutoConnectEntry; - -static const MenuSshAutoConnectEntry SFTP_AUTO_CONNECT_ENTRIES[] = { - {NEMO_TERMINAL_SSH_AUTOCONNECT_OFF, N_("Do not connect automatically"), NEMO_TERMINAL_SYNC_NONE}, - {NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH, N_("Automatically connect and sync both ways"), NEMO_TERMINAL_SYNC_BOTH}, - {NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM, N_("Automatically connect and sync: File Manager → Terminal"), NEMO_TERMINAL_SYNC_FM_TO_TERM}, - {NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM, N_("Automatically connect and sync: Terminal → File Manager"), NEMO_TERMINAL_SYNC_TERM_TO_FM}, - {NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_NONE, N_("Automatically connect without syncing"), NEMO_TERMINAL_SYNC_NONE} -}; - -static const MenuSyncModeEntry MANUAL_SSH_SYNC_ENTRIES[] = { - {NEMO_TERMINAL_SYNC_BOTH, N_("Sync folder both ways")}, - {NEMO_TERMINAL_SYNC_FM_TO_TERM, N_("Sync folder from File Manager → Terminal")}, - {NEMO_TERMINAL_SYNC_TERM_TO_FM, N_("Sync folder from Terminal → File Manager")}, - {NEMO_TERMINAL_SYNC_NONE, N_("No folder sync")} -}; - -/** - * _on_menu_item_activate_widget_action: - * @menuitem: The #GtkMenuItem that was activated. - * @user_data: Unused in this callback. - * - * Compatibility handler to bridge GtkMenuItem's "activate" signal to - * a GAction registered on the widget. The widget instance and action name - * are retrieved from data set on the menu item. - */ -static void -_on_menu_item_activate_widget_action (GtkMenuItem *menuitem, - gpointer user_data) -{ - // Retrieve the NemoTerminalWidget instance and the detailed action name stored on the menu item. - NemoTerminalWidget *self = (NemoTerminalWidget *)g_object_get_data(G_OBJECT(menuitem), "ntw-self"); - const gchar *detailed_action_name = (const gchar *)g_object_get_data(G_OBJECT(menuitem), "ntw-action-name"); - - if (self && detailed_action_name) { - // Parse the action name from the detailed_action_name (e.g., "terminal.copy" -> "copy"). - // The detailed_action_name includes a prefix like "widget." or "terminal.". - const gchar *dot = strchr(detailed_action_name, '.'); - if (dot && self->action_group) { - const gchar *action_name = dot + 1; // Get the action name part after the dot. - g_action_group_activate_action(G_ACTION_GROUP(self->action_group), action_name, NULL); - } else { - g_warning("Could not parse action name or action group not found for menu item: %s", detailed_action_name); - } - } -} - -/** - * _add_action_menu_item_compat: - * @menu_shell: The #GtkMenuShell to add the item to. - * @self: The #NemoTerminalWidget instance (used for context in the action). - * @label: The translatable label for the menu item. - * @detailed_action_name: The full action name (e.g., "terminal.copy") to activate. - * - * Helper to create a #GtkMenuItem that, when activated, triggers a GAction - * on the widget. This is for compatibility where direct GAction use in menus - * might be problematic or for older GTK versions/styles. - */ -static void -_add_action_menu_item_compat(GtkMenuShell *menu_shell, - NemoTerminalWidget *self, - const gchar *label, - const gchar *detailed_action_name) -{ - GtkWidget *item = gtk_menu_item_new_with_label(label); - // Store necessary context on the GtkMenuItem itself for the callback. - g_object_set_data(G_OBJECT(item), "ntw-self", self); - g_object_set_data(G_OBJECT(item), "ntw-action-name", (gpointer)detailed_action_name); // Cast is okay for const gchar* - g_signal_connect(item, "activate", G_CALLBACK(_on_menu_item_activate_widget_action), NULL); // user_data for signal is NULL - gtk_menu_shell_append(menu_shell, item); -} - -/** - * _add_callback_menu_item: - * @menu_shell: The #GtkMenuShell to add the item to. - * @label: The translatable label for the menu item. - * @callback: The GCallback function to invoke on activation. - * @user_data: User data to pass to the callback. - * - * Helper to create a #GtkMenuItem that calls a specific C callback function - * when activated. - */ -static void -_add_callback_menu_item(GtkMenuShell *menu_shell, - const gchar *label, - GCallback callback, - gpointer user_data) -{ - GtkWidget *item = gtk_menu_item_new_with_label(label); - g_signal_connect(item, "activate", callback, user_data); - gtk_menu_shell_append(menu_shell, item); -} - -/** - * _add_radio_menu_item_with_data: - * @menu_shell: The #GtkMenuShell to add the item to. - * @radio_group: Pointer to the GSList representing the radio group. - * @label: The translatable label for the menu item. - * @data_key: The key for attaching data_value to the item. - * @data_value: The data to associate with the menu item (e.g., an enum or string). - * @is_active: Whether this radio item should be initially active. - * @activate_callback: The GCallback function to invoke on activation. - * @user_data: User data to pass to the callback. - * - * Helper to create a #GtkRadioMenuItem, associate data with it, - * and connect its "activate" signal. - * - * Returns: The created #GtkWidget (the radio menu item). - */ -static GtkWidget * -_add_radio_menu_item_with_data(GtkMenuShell *menu_shell, - GSList **radio_group, - const gchar *label, - const gchar *data_key, - gpointer data_value, - gboolean is_active, - GCallback activate_callback, - gpointer user_data) -{ - GtkWidget *item = gtk_radio_menu_item_new_with_label(*radio_group, label); - if (*radio_group == NULL) { // First item in the group - *radio_group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item)); - } - - g_object_set_data(G_OBJECT(item), data_key, data_value); - - if (is_active) { - gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE); - } - g_signal_connect(item, "activate", activate_callback, user_data); - gtk_menu_shell_append(menu_shell, item); - return item; -} - -/** - * create_terminal_popup_menu: - * @self: The #NemoTerminalWidget instance. - * - * Creates and populates the context menu for the terminal widget. - * Includes options for copy/paste, color schemes, font sizes, - * synchronization modes, and SSH connection management. - * - * Returns: A new #GtkWidget (the #GtkMenu). The caller does not own the GtkWidget. - * The menu will be shown via gtk_menu_popup. - */ -static GtkWidget * -create_terminal_popup_menu(NemoTerminalWidget *self) -{ - GtkWidget *menu, *menu_item, *submenu; - gboolean is_sftp_location = FALSE; - g_autofree gchar *current_uri = NULL; - - menu = gtk_menu_new(); - - /* Standard Edit Actions: Copy, Paste, Select All */ - _add_action_menu_item_compat(GTK_MENU_SHELL(menu), self, _("Copy"), "terminal.copy"); - _add_action_menu_item_compat(GTK_MENU_SHELL(menu), self, _("Paste"), "terminal.paste"); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - _add_action_menu_item_compat(GTK_MENU_SHELL(menu), self, _("Select All"), "terminal.select-all"); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - - /* Color Scheme Submenu */ - menu_item = gtk_menu_item_new_with_label(_("Color Scheme")); - submenu = gtk_menu_new(); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); - - GSList *color_scheme_radio_group = NULL; - const gchar *current_scheme_val = nemo_terminal_widget_get_color_scheme(self); - for (gsize i = 0; i < G_N_ELEMENTS(COLOR_SCHEME_ENTRIES); ++i) { - _add_radio_menu_item_with_data(GTK_MENU_SHELL(submenu), &color_scheme_radio_group, - _(COLOR_SCHEME_ENTRIES[i].label_pot), - DATA_KEY_SCHEME_NAME, (gpointer)COLOR_SCHEME_ENTRIES[i].id, - (g_strcmp0(current_scheme_val, COLOR_SCHEME_ENTRIES[i].id) == 0), - G_CALLBACK(on_color_scheme_changed), self); - } - - /* Font Size Submenu */ - menu_item = gtk_menu_item_new_with_label(_("Font Size")); - submenu = gtk_menu_new(); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); - - // Get current font size to pre-select the closest one in the menu - g_autoptr(PangoFontDescription) current_font_desc = pango_font_description_copy( - vte_terminal_get_font(self->terminal)); - int current_size_pts = pango_font_description_get_size(current_font_desc) / PANGO_SCALE; - - // Find the closest predefined font size to the current one - int closest_size_idx = 0; - if (G_N_ELEMENTS(FONT_SIZE_ENTRIES) > 0) { - int min_diff = abs(FONT_SIZE_ENTRIES[0].size_pts - current_size_pts); - for (gsize i = 1; i < G_N_ELEMENTS(FONT_SIZE_ENTRIES); i++) { - int diff = abs(FONT_SIZE_ENTRIES[i].size_pts - current_size_pts); - if (diff < min_diff) { - min_diff = diff; - closest_size_idx = i; - } - } - } - - GSList *font_size_radio_group = NULL; - for (gsize i = 0; i < G_N_ELEMENTS(FONT_SIZE_ENTRIES); ++i) { - g_autofree gchar *label = g_strdup_printf("%d", FONT_SIZE_ENTRIES[i].size_pts); - _add_radio_menu_item_with_data(GTK_MENU_SHELL(submenu), &font_size_radio_group, - label, DATA_KEY_FONT_SIZE, - GINT_TO_POINTER(FONT_SIZE_ENTRIES[i].size_pts), - (i == closest_size_idx), // Check if this is the closest size - G_CALLBACK(on_font_size_changed), self); - } - gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - - // Determine if the current location is an SFTP path - if (self->current_location != NULL && G_IS_FILE(self->current_location)) { - current_uri = g_file_get_uri(self->current_location); - if (current_uri && g_str_has_prefix(current_uri, "sftp://")) { - is_sftp_location = TRUE; - } - } else if (self->current_location != NULL) { - // This case should ideally not happen if current_location is always GFile or NULL - g_warning("self->current_location is not a GFile in create_terminal_popup_menu"); - } - - /* Local Folder Sync Submenu (only shown if not in SSH mode) */ - if (!self->in_ssh_mode) { - menu_item = gtk_menu_item_new_with_label(_("Local Folder Sync")); - GtkWidget *local_sync_submenu = gtk_menu_new(); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), local_sync_submenu); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); - - GSList *local_sync_radio_group = NULL; - for (gsize i = 0; i < G_N_ELEMENTS(LOCAL_SYNC_MODE_ENTRIES); ++i) { - _add_radio_menu_item_with_data(GTK_MENU_SHELL(local_sync_submenu), &local_sync_radio_group, - _(LOCAL_SYNC_MODE_ENTRIES[i].label_pot), - DATA_KEY_LOCAL_SYNC_MODE, GINT_TO_POINTER(LOCAL_SYNC_MODE_ENTRIES[i].mode), - (self->local_sync_mode == LOCAL_SYNC_MODE_ENTRIES[i].mode), - G_CALLBACK(on_local_sync_mode_changed), self); - } - } - - /* SSH Auto-Connect Submenu (only shown if not in SSH mode) */ - if (!self->in_ssh_mode) { - menu_item = gtk_menu_item_new_with_label(_("SSH Auto-Connect")); - GtkWidget *sftp_auto_connect_submenu = gtk_menu_new(); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), sftp_auto_connect_submenu); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); - - GSList *sftp_auto_radio_group = NULL; - for (gsize i = 0; i < G_N_ELEMENTS(SFTP_AUTO_CONNECT_ENTRIES); ++i) { - g_autofree gchar *label = g_strdup(_(SFTP_AUTO_CONNECT_ENTRIES[i].label_pot)); - GtkWidget *auto_item_widget = _add_radio_menu_item_with_data( - GTK_MENU_SHELL(sftp_auto_connect_submenu), &sftp_auto_radio_group, - label, DATA_KEY_SFTP_AUTO_CONNECT_MODE, - GINT_TO_POINTER(SFTP_AUTO_CONNECT_ENTRIES[i].mode), - (self->ssh_auto_connect_mode == SFTP_AUTO_CONNECT_ENTRIES[i].mode), - G_CALLBACK(on_sftp_auto_connect_behavior_changed), self); - - // If current location is SFTP, attach its details to the auto-connect menu item - // This allows immediate connection if an auto-connect option is chosen. - if (is_sftp_location && self->current_location && G_IS_FILE(self->current_location) && auto_item_widget) { - gchar *h = NULL, *u = NULL, *p = NULL; - if (parse_gvfs_ssh_path(self->current_location, &h, &u, &p)) { - g_object_set_data_full(G_OBJECT(auto_item_widget), DATA_KEY_SSH_HOSTNAME, h, (GDestroyNotify)g_free); - g_object_set_data_full(G_OBJECT(auto_item_widget), DATA_KEY_SSH_USERNAME, u, (GDestroyNotify)g_free); - g_object_set_data_full(G_OBJECT(auto_item_widget), DATA_KEY_SSH_PORT, p, (GDestroyNotify)g_free); - } else { - // Free if parse_gvfs_ssh_path allocated them but returned FALSE - g_free(h); g_free(u); g_free(p); - } - } - } - } - - /* SSH Connection Management: Disconnect (if in SSH) or Manual Connect (if on SFTP path) */ - if (self->in_ssh_mode) { - // Option to disconnect from the current SSH session - _add_callback_menu_item(GTK_MENU_SHELL(menu), _("Disconnect from SSH"), - G_CALLBACK(on_ssh_exit_activate), self); - } else if (is_sftp_location && self->current_location && G_IS_FILE(self->current_location)) { - // Option to manually connect to the current SFTP location via SSH - gchar *hostname = NULL, *username = NULL, *port = NULL; - gboolean can_connect_ssh = parse_gvfs_ssh_path(self->current_location, &hostname, &username, &port); - - if (can_connect_ssh && hostname != NULL) { - gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - - // Build a descriptive label for the SSH connection submenu - g_autofree GString *label_gstr = g_string_new(_("SSH Connection to ")); - if (username != NULL && *username != '\0') { - g_string_append_printf(label_gstr, "%s@", username); - } - g_string_append(label_gstr, hostname); - if (port != NULL && *port != '\0') { - g_string_append_printf(label_gstr, ":%s", port); - } - - menu_item = gtk_menu_item_new_with_label(label_gstr->str); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); - - // Submenu for choosing sync mode for manual SSH connection - GtkWidget *ssh_manual_connect_menu = gtk_menu_new(); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), ssh_manual_connect_menu); - - for (gsize i = 0; i < G_N_ELEMENTS(MANUAL_SSH_SYNC_ENTRIES); ++i) { - GtkWidget *sync_item = gtk_menu_item_new_with_label(_(MANUAL_SSH_SYNC_ENTRIES[i].label_pot)); - // Store SSH details on the menu item for the callback - g_object_set_data_full(G_OBJECT(sync_item), DATA_KEY_SSH_HOSTNAME, g_strdup(hostname), (GDestroyNotify)g_free); - g_object_set_data_full(G_OBJECT(sync_item), DATA_KEY_SSH_USERNAME, g_strdup(username), (GDestroyNotify)g_free); - g_object_set_data_full(G_OBJECT(sync_item), DATA_KEY_SSH_PORT, g_strdup(port), (GDestroyNotify)g_free); - g_object_set_data(G_OBJECT(sync_item), DATA_KEY_SSH_SYNC_MODE, GINT_TO_POINTER(MANUAL_SSH_SYNC_ENTRIES[i].mode)); - g_signal_connect(sync_item, "activate", G_CALLBACK(on_ssh_connect_activate), self); - gtk_menu_shell_append(GTK_MENU_SHELL(ssh_manual_connect_menu), sync_item); - } - } - g_free(hostname); g_free(username); g_free(port); - } - - gtk_widget_show_all(menu); - return menu; -} - - -/** - * change_directory_in_terminal: - * @self: The #NemoTerminalWidget instance. - * @location: The #GFile representing the new directory. - * - * Changes the current working directory in the VTE terminal to match the - * provided @location. This function respects the configured synchronization - * mode (local or SSH) and only performs the `cd` if synchronization from - * File Manager to Terminal is enabled. - */ -static void -change_directory_in_terminal(NemoTerminalWidget *self, GFile *location) -{ - g_autofree char *path = NULL; - gboolean should_sync = TRUE; // Assume sync unless checks determine otherwise - - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); - g_return_if_fail(location != NULL && G_IS_FILE(location)); - - /* Determine if a 'cd' command should be sent based on current mode and sync settings */ - if (self->in_ssh_mode) - { - // In SSH mode, sync from FM to Terminal requires SYNC_BOTH or SYNC_FM_TO_TERM - if (self->ssh_sync_mode != NEMO_TERMINAL_SYNC_BOTH && - self->ssh_sync_mode != NEMO_TERMINAL_SYNC_FM_TO_TERM) - { - should_sync = FALSE; - } - - if (should_sync) - { - // For SSH, get the remote path from the SFTP GFile - path = get_remote_path_from_sftp_gfile(location); - if (!path) - { - g_warning("Failed to get remote path for SSH cd from GFile URI: %s", - g_file_peek_path(location) ? g_file_peek_path(location) : "(unknown)"); - } - else if (path[0] == '\0') // Empty path typically means root - { - g_free(path); - path = g_strdup("/"); - } - } - } - else // Not in SSH mode (local terminal) - { - // For local terminal, sync from FM to Terminal requires SYNC_BOTH or SYNC_FM_TO_TERM - if (self->local_sync_mode != NEMO_TERMINAL_SYNC_BOTH && - self->local_sync_mode != NEMO_TERMINAL_SYNC_FM_TO_TERM) - { - should_sync = FALSE; - } - } - - if (!should_sync) - { - return; // Sync not enabled for this direction - } - - // If not in SSH mode and sync is enabled, get the local file path - if (!self->in_ssh_mode) // Implies should_sync is TRUE here for local - { - if (g_file_query_exists(location, NULL)) - { - path = g_file_get_path(location); - } - else - { - // Target location doesn't exist, warn and abort cd - g_autofree gchar *uri_for_warning = g_file_get_uri(location); - g_warning("Target local location %s for cd no longer exists. Aborting cd.", - uri_for_warning ? uri_for_warning : "(unknown URI)"); - path = NULL; // Ensure path is NULL so no cd command is fed - } - } - - // If a valid path was determined, feed the cd command - if (path != NULL && *path != '\0') // Ensure path is not NULL or empty - { - // Tell terminal to ignore its own "directory-changed" signal for this explicit cd - self->ignore_next_terminal_cd_signal = TRUE; - feed_cd_command(VTE_TERMINAL(self->terminal), path); - } - else if (should_sync) // Path is NULL but we intended to sync - { - g_warning("Path for cd command is NULL, aborting cd. Location: %s", - g_file_peek_path(location) ? g_file_peek_path(location) : "(unknown)"); - } -} - -/** - * get_remote_path_from_sftp_gfile: - * @location: A #GFile, presumably an SFTP location. - * - * Extracts the absolute remote path from an SFTP #GFile. - * It first tries to parse the URI (e.g., "sftp://host/path"). - * As a fallback for GVFS-mounted SFTP locations that might appear as local - * file paths (e.g., "/run/user/UID/gvfs/sftp:host=.../remote/path"), - * it attempts to parse these paths. - * - * Returns: A newly allocated string containing the remote path (e.g., "/home/user/docs"), - * or "/" if the path component is empty. Returns %NULL on failure. - * The caller must free the returned string. - */ -static gchar * -get_remote_path_from_sftp_gfile(GFile *location) -{ - g_return_val_if_fail(G_IS_FILE(location), NULL); - - gchar *remote_path = NULL; - g_autofree gchar *uri = g_file_get_uri(location); - - if (uri && g_str_has_prefix(uri, "sftp://")) - { - // Unescape the URI to handle special characters in path components - g_autofree gchar *decoded_uri = g_uri_unescape_string(uri, NULL); - if (decoded_uri) { - // Find the path part: sftp://[userinfo@]host[:port]/path - // Add 7 to skip "sftp://". - const char *host_part_end = strstr(decoded_uri + 7, "/"); - if (host_part_end) { // Found a '/' after the host part - remote_path = g_strdup(host_part_end); - } else { // No path component after host, implies root directory on server - remote_path = g_strdup("/"); - } - } - } - else // Fallback: Try to parse as a GVFS local mount path for SFTP - { - g_autofree gchar *path = g_file_get_path(location); - // Example GVFS path: /run/user/1000/gvfs/sftp:host=example.com,user=myuser/actual/remote/path - // We need to extract "/actual/remote/path" - if (path && g_str_has_prefix(path, "/run/user/") && strstr(path, "/gvfs/sftp:host=")) - { - // Find the start of the GVFS SFTP details part - char *sftp_details_part = strstr(path, "/gvfs/sftp:host="); - if (sftp_details_part) - { - // The actual remote path starts after the GVFS connection string part. - // e.g. "sftp:host=...,user=..." or "sftp:host=..." - // The first '/' after this connection string segment marks the start of the remote path. - char *path_start = strchr(sftp_details_part + strlen("/gvfs/"), '/'); // Search for '/' after "/gvfs/" - if (path_start) - { - remote_path = g_strdup(path_start); - } - else // No further '/' means it's the root of the SFTP mount - { - remote_path = g_strdup("/"); - } - } - } - } - return remote_path; -} - -/** - * setup_terminal_font: - * @terminal: The #VteTerminal widget. - * - * Configures the font for the VTE terminal. It uses the system's monospace - * font setting ("org.gnome.desktop.interface monospace-font-name") and - * a font size retrieved via `nemo_terminal_get_font_size()`. - */ -static void -setup_terminal_font(VteTerminal *terminal) -{ - g_autoptr(PangoFontDescription) font_desc = NULL; - g_autoptr(GSettings) interface_settings = NULL; - g_autofree gchar *font_name = NULL; - int font_size_pts; - - // Get system monospace font name - interface_settings = g_settings_new("org.gnome.desktop.interface"); - font_name = g_settings_get_string(interface_settings, "monospace-font-name"); - - // Get saved/default font size for the terminal - font_size_pts = nemo_terminal_get_font_size(); - - if (font_name && *font_name) - { - font_desc = pango_font_description_from_string(font_name); - } - else // Fallback if system font name is not set - { - font_desc = pango_font_description_new(); - pango_font_description_set_family(font_desc, "Monospace"); // Default to generic "Monospace" - } - - pango_font_description_set_size(font_desc, font_size_pts * PANGO_SCALE); - vte_terminal_set_font(terminal, font_desc); - // VTE terminal takes its own copy of font_desc, so we can free ours. -} - -/** - * focus_once_and_remove: - * @user_data: The #GtkWidget (VTE terminal) to focus. - * - * Idle callback to grab focus for the terminal widget. - * Removes itself after execution. Used to ensure focus is set - * after other UI events might have settled. - * - * Returns: %G_SOURCE_REMOVE to ensure it runs only once. - */ -static gboolean -focus_once_and_remove(gpointer user_data) -{ - GtkWidget *widget_to_focus = GTK_WIDGET(user_data); - NemoTerminalWidget *self; - - if (GTK_IS_WIDGET(widget_to_focus) && gtk_widget_get_window(widget_to_focus)) { // Ensure widget is realized - gtk_widget_grab_focus(widget_to_focus); - } - - // Clear the timeout ID from the parent NemoTerminalWidget - self = NEMO_TERMINAL_WIDGET(gtk_widget_get_ancestor(widget_to_focus, NEMO_TYPE_TERMINAL_WIDGET)); - if (self && self->focus_timeout_id > 0) // Check if self is valid and ID matches - { - // This function is called by the timeout, so we can't remove by ID here. - // Instead, the source removes itself. We just clear our record. - self->focus_timeout_id = 0; - } - return G_SOURCE_REMOVE; -} - -/** - * reset_toggling_flag: - * @user_data: The #NemoTerminalWidget instance. - * - * Timeout callback to reset the `in_toggling` flag. This acts as a - * debounce mechanism for visibility toggling actions. - * - * Returns: %G_SOURCE_REMOVE to ensure it runs only once. - */ -static gboolean -reset_toggling_flag(gpointer user_data) -{ - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); - if (NEMO_IS_TERMINAL_WIDGET(self)) { - self->in_toggling = FALSE; - } - return G_SOURCE_REMOVE; -} +#include +#include +#include +#include +#include +#include +#include + +/* UI constants */ +#define MIN_MAIN_VIEW_HEIGHT 200 +#define MIN_TERMINAL_HEIGHT 50 +#define MIN_FONT_SIZE 6 +#define MAX_FONT_SIZE 72 -/** - * apply_initial_size_idle: - * @user_data: The #NemoTerminalWidget instance. - * - * Idle callback to apply the terminal's saved height. This is typically - * called after the widget and its container paned are realized, ensuring - * dimensions are available. - * - * Returns: %G_SOURCE_REMOVE to ensure it runs only once. - */ -static gboolean -apply_initial_size_idle(gpointer user_data) +/* GObject properties */ +enum { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); - if (NEMO_IS_TERMINAL_WIDGET(self)) { - nemo_terminal_widget_apply_new_size(self); - } - return G_SOURCE_REMOVE; -} + PROP_0, + PROP_CURRENT_LOCATION, + N_PROPS +}; -/** - * on_terminal_key_press: - * @widget: The #VteTerminal widget where the key press occurred. - * @event: The #GdkEventKey for the key press. - * @user_data: The #NemoTerminalWidget instance. - * - * Handles key press events within the terminal, primarily for implementing - * custom keyboard shortcuts (e.g., F4 to toggle visibility, Ctrl+Shift+C/V/A - * for copy/paste/select all, Ctrl+Shift+S for SSH connect). - * - * Returns: %TRUE if the event was handled, %FALSE otherwise. - */ -static gboolean -on_terminal_key_press(GtkWidget *widget, - GdkEventKey *event, - gpointer user_data) +/* GObject signals */ +enum { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); - guint keyval = event->keyval; - GdkModifierType state = event->state; // Use GdkEventKey->state for modifiers - - /* F4: Toggle terminal visibility */ - if (keyval == GDK_KEY_F4) - { - nemo_terminal_widget_toggle_visible(self); - return TRUE; // Event handled - } - - /* Standard terminal shortcuts (Ctrl+Shift+Letter) */ - if ((state & GDK_CONTROL_MASK) && (state & GDK_SHIFT_MASK)) - { - switch (keyval) - { - case GDK_KEY_C: // Ctrl+Shift+C for Copy - case GDK_KEY_c: - vte_terminal_copy_clipboard_format(self->terminal, VTE_FORMAT_TEXT); - return TRUE; - - case GDK_KEY_V: // Ctrl+Shift+V for Paste - case GDK_KEY_v: - vte_terminal_paste_clipboard(self->terminal); - return TRUE; - - case GDK_KEY_A: // Ctrl+Shift+A for Select All - case GDK_KEY_a: - vte_terminal_select_all(self->terminal); - return TRUE; - - case GDK_KEY_S: // Ctrl+Shift+S for SSH connect (if on SFTP path) - case GDK_KEY_s: - if (self->current_location && G_IS_FILE(self->current_location) && !self->in_ssh_mode) - { - g_autofree gchar *hostname = NULL; - g_autofree gchar *username = NULL; - g_autofree gchar *port = NULL; - - if (parse_gvfs_ssh_path(self->current_location, &hostname, &username, &port)) - { - // Default to SYNC_BOTH for keyboard shortcut initiated connections - _initiate_ssh_connection(self, hostname, username, port, NEMO_TERMINAL_SYNC_BOTH); - // hostname, username, port are freed by g_autofree - return TRUE; - } - } - break; - } - } - return FALSE; // Event not handled by this function -} + CHANGE_DIRECTORY, + TOGGLE_VISIBILITY, + LAST_SIGNAL +}; -/** - * on_terminal_directory_changed: - * @terminal: The #VteTerminal whose directory changed. - * @user_data: The #NemoTerminalWidget instance. +/* + * NemoTerminalState: + * @NEMO_TERMINAL_STATE_LOCAL: The terminal is running a local shell. + * @NEMO_TERMINAL_STATE_IN_SSH: The terminal is in an active SSH session. * - * Callback for VTE's "current-directory-uri-changed" (or equivalent) signal. - * When the terminal's CWD changes (e.g., user types `cd`), this function - * updates the file manager's location if synchronization from - * Terminal to File Manager is enabled. + * Represents the operational state of the terminal widget. This is crucial + * for determining how directory synchronization and commands are handled. */ -static void -on_terminal_directory_changed(VteTerminal *terminal, - gpointer user_data) +typedef enum { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); - const char *cwd_uri = vte_terminal_get_current_directory_uri(terminal); - g_autoptr(GFile) new_gfile_location = NULL; - gboolean should_sync_to_fm = TRUE; // Assume sync unless checks determine otherwise - - if (!cwd_uri) return; // No CWD URI available - - // If ignore_next_terminal_cd_signal is set, it means this change was - // programmatically triggered (e.g., by FM changing location). - // We should update our internal current_location if it's a local shell - // to reflect this, but not sync back to FM. - if (self->ignore_next_terminal_cd_signal) { - self->ignore_next_terminal_cd_signal = FALSE; - if (!self->in_ssh_mode) { // Only update self->current_location for local shell initial cd - g_autoptr(GFile) temp_gfile = g_file_new_for_uri(cwd_uri); - if (temp_gfile) { - if (!self->current_location || !g_file_equal(temp_gfile, self->current_location)) { - g_set_object(&self->current_location, temp_gfile); - // No g_signal_emit here, as this is an internal sync from an explicit cd. - } - } - } - return; - } - - /* Determine if a sync to File Manager should occur */ - if (self->in_ssh_mode) - { - // In SSH mode, sync from Term to FM requires SYNC_BOTH or SYNC_TERM_TO_FM - if (self->ssh_sync_mode != NEMO_TERMINAL_SYNC_BOTH && - self->ssh_sync_mode != NEMO_TERMINAL_SYNC_TERM_TO_FM) - { - should_sync_to_fm = FALSE; - } - - if (should_sync_to_fm && g_str_has_prefix(cwd_uri, "file://")) // VTE gives local file:// URI - { - // Convert local path from terminal (e.g., /home/user) to an SFTP URI - g_autofree gchar *local_path_from_uri = g_filename_from_uri(cwd_uri, NULL, NULL); - if (local_path_from_uri && self->ssh_hostname) // Need hostname to build SFTP URI - { - g_autofree GString *sftp_uri_builder = g_string_new("sftp://"); - if (self->ssh_username && *self->ssh_username) { - g_string_append_printf(sftp_uri_builder, "%s@", self->ssh_username); - } - g_string_append(sftp_uri_builder, self->ssh_hostname); - if (self->ssh_port && *self->ssh_port) { - g_string_append_printf(sftp_uri_builder, ":%s", self->ssh_port); - } - // Ensure path starts with '/', g_filename_from_uri should provide absolute path - g_string_append(sftp_uri_builder, local_path_from_uri); - new_gfile_location = g_file_new_for_uri(sftp_uri_builder->str); - } - } - } - else // Not in SSH mode (local terminal) - { - // For local terminal, sync from Term to FM requires SYNC_BOTH or SYNC_TERM_TO_FM - if (self->local_sync_mode != NEMO_TERMINAL_SYNC_BOTH && - self->local_sync_mode != NEMO_TERMINAL_SYNC_TERM_TO_FM) - { - should_sync_to_fm = FALSE; - } - if (should_sync_to_fm) - { - new_gfile_location = g_file_new_for_uri(cwd_uri); - } - } - - if (!should_sync_to_fm || !new_gfile_location) - { - return; // Sync not enabled, or failed to create GFile for the new location - } - - // If the new location is different from the current one, update and emit signal - if (!self->current_location || !g_file_equal(new_gfile_location, self->current_location)) - { - g_set_object(&self->current_location, new_gfile_location); // Updates ref count - g_signal_emit_by_name(self, "change-directory", new_gfile_location); + NEMO_TERMINAL_STATE_LOCAL, + NEMO_TERMINAL_STATE_IN_SSH, +} NemoTerminalState; - // If terminal had focus, try to maintain it after the directory change potentially re-renders UI - if (self->maintain_focus && gtk_widget_has_focus(GTK_WIDGET(self->terminal))) - { - if (self->focus_timeout_id > 0) // Cancel any pending focus attempt - { - g_source_remove(self->focus_timeout_id); - } - // Schedule a new focus attempt - self->focus_timeout_id = g_timeout_add(50, focus_once_and_remove, GTK_WIDGET(self->terminal)); - } - } - // new_gfile_location is unref'd by g_autoptr when it goes out of scope -} - -/** - * on_terminal_child_exited: - * @terminal: The #VteTerminal whose child process exited. - * @status: The exit status of the child process. - * @user_data: The #NemoTerminalWidget instance. - * - * Callback for VTE's "child-exited" signal. - * SSH session termination (e.g., connection drop, remote exit) and - * local shell exits. - * If an SSH session ends unexpectedly, it resets to a local terminal. - * If a local shell exits, it may respawn based on visibility. - */ -static void -on_terminal_child_exited(VteTerminal *terminal, - gint status, - gpointer user_data) +struct _NemoTerminalWidgetPrivate { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + /* Child widgets */ + GtkWidget *scrolled_window; /* The GtkScrolledWindow that makes the terminal scrollable. */ + VteTerminal *terminal; /* The core VTE terminal widget. */ + GtkWidget *ssh_indicator; /* A label shown at the top to indicate an active SSH session. */ + + /* State management */ + NemoTerminalState state; /* The current operational state (local shell or remote SSH session). */ + gboolean is_visible; /* Tracks if the terminal widget is currently shown to the user. */ + gboolean in_toggling; /* A re-entrancy guard for the visibility toggle function to prevent rapid, repeated calls. */ + gboolean needs_respawn; /* Flag indicating if the terminal's child process needs to be respawned (e.g., after being hidden and shown again). */ + gboolean ignore_next_terminal_cd_signal; /* A flag to prevent feedback loops when the file manager programmatically changes the terminal's directory. */ + guint focus_timeout_id; /* The ID for a timeout used to ensure the terminal gets focus after certain operations. */ + GPid child_pid; /* The process ID of the shell or SSH client running in the terminal. -1 if no process is running. */ + GCancellable *spawn_cancellable; /* A GCancellable object to allow cancelling an asynchronous terminal spawn operation. */ + GWeakRef paned_weak_ref; /* A weak reference to the parent GtkPaned to avoid circular references and allow size adjustments. */ + + /* Preferences */ + gchar *color_scheme; /* The name of the current color scheme (e.g., "dark", "solarized-light"). */ + NemoTerminalSyncMode local_sync_mode; /* The directory synchronization mode for local shell sessions. */ + NemoTerminalSshAutoConnectMode ssh_auto_connect_mode; /* The auto-connection behavior when navigating to SFTP locations. */ + + /* Location and SSH details */ + GFile *current_location; /* The GFile representing the current directory displayed in the file manager view. */ + + gchar *ssh_hostname; /* The hostname for the current SSH connection. */ + gchar *ssh_username; /* The username for the current SSH connection. */ + gchar *ssh_port; /* The port for the current SSH connection. */ + gchar *ssh_remote_path; /* The remote path to change to after an SSH connection is established. */ + NemoTerminalSyncMode ssh_sync_mode; /* The directory synchronization mode for the current SSH session. */ + + /* Pending SSH connection details, used when a location is set before the terminal is spawned */ + gchar *pending_ssh_hostname; /* The hostname for a pending SSH connection, to be used after the local shell spawns. */ + gchar *pending_ssh_username; /* The username for a pending SSH connection. */ + gchar *pending_ssh_port; /* The port for a pending SSH connection. */ + NemoTerminalSyncMode pending_ssh_sync_mode; /* The sync mode for a pending SSH connection. */ +}; - // If we are explicitly exiting SSH (e.g., user clicked "Disconnect"), - // on_ssh_exit_activate handles the reset. Avoid double reset. - if (self->is_exiting_ssh) { - return; - } +/* Data keys for g_object_set_data() */ +static const gchar *const DATA_KEY_SSH_HOSTNAME = "ntw-ssh-hostname"; +static const gchar *const DATA_KEY_SSH_USERNAME = "ntw-ssh-username"; +static const gchar *const DATA_KEY_SSH_PORT = "ntw-ssh-port"; +static const gchar *const DATA_KEY_SSH_SYNC_MODE = "ntw-ssh-sync-mode"; - if (self->in_ssh_mode) { - // The shell process hosting the SSH client has exited. This could be due to - // network issues, the SSH client itself terminating, or the remote end closing. - g_warning("Shell hosting SSH session exited unexpectedly (status %d). Resetting to local terminal.", status); - self->is_exiting_ssh = TRUE; // Prevent re-entry during reset - reset_terminal_to_current_location(self); // Clears SSH state, spawns local shell - self->is_exiting_ssh = FALSE; - } else { - // Local shell exited. - // Respawn if the terminal widget is part of a window and is currently visible, - // or mark for respawn if it's hidden. - if (GTK_IS_WIDGET(self) && gtk_widget_get_ancestor(GTK_WIDGET(self), GTK_TYPE_WINDOW)) { - if (self->is_visible) { - spawn_terminal_in_widget(self); // Respawn local shell - } else { - self->needs_respawn = TRUE; // Mark to respawn when next shown - } - } - } -} +/* Shell control sequences for preserving user input during programmatic 'cd' */ +static const gchar *const SHELL_CTRL_A = "\x01"; /* Move cursor to beginning of line */ +static const gchar *const SHELL_CTRL_K = "\x0B"; /* Kill (cut) from cursor to end of line */ +static const gchar *const SHELL_CTRL_Y = "\x19"; /* Yank (paste) killed text */ +static const gchar *const SHELL_CTRL_E = "\x05"; /* Move cursor to end of line */ +static const gchar *const SHELL_DELETE = "\033[3~"; /* Delete character under cursor */ -/** - * on_container_size_changed: - * @paned: The #GtkPaned widget whose position (divider) changed. - * @pspec: The #GParamSpec of the property that changed (unused). - * @user_data: The #NemoTerminalWidget instance. - * - * Callback for the "notify::position" signal on the GtkPaned that - * contains the terminal. Saves the new height of the terminal pane - * when the user resizes it. - */ -static void -on_container_size_changed(GtkPaned *paned, - GParamSpec *pspec, - gpointer user_data) +typedef struct { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); - // Basic validation - if (!self || !NEMO_IS_TERMINAL_WIDGET(self) || !GTK_IS_PANED(paned)) return; - if (!gtk_widget_get_realized(GTK_WIDGET(paned))) return; // Avoid acting on unrealized widgets - - int position = gtk_paned_get_position(paned); // Position of the divider - int total_height = gtk_widget_get_allocated_height(GTK_WIDGET(paned)); + GdkRGBA foreground; + GdkRGBA background; + GdkRGBA palette[16]; + gboolean use_system_colors; +} NemoTerminalColorPalette; - if (total_height <= 0) return; // Avoid division by zero or negative heights +#define RGB(r, g, b) ((GdkRGBA) { .red = (r), .green = (g), .blue = (b), .alpha = 1.0 }) - // For a GtkPaned with vertical orientation: - // Child1 (top) height = position - // Child2 (bottom, our terminal) height = total_height - position - int terminal_height = total_height - position; +typedef struct +{ + const gchar *id; + const gchar *label_pot; + const NemoTerminalColorPalette palette; +} MenuSchemeEntry; - nemo_terminal_widget_save_height(self, terminal_height); -} +/* clang-format off */ +static const MenuSchemeEntry COLOR_SCHEME_ENTRIES[] = { + { "system", N_("System"), .palette = { .use_system_colors = TRUE } }, + { "dark", N_("Dark"), .palette = { + .foreground = RGB(0.9, 0.9, 0.9), .background = RGB(0.12, 0.12, 0.12), + .palette = { + RGB(0.0, 0.0, 0.0), RGB(0.8, 0.0, 0.0), RGB(0.0, 0.8, 0.0), RGB(0.8, 0.8, 0.0), + RGB(0.0, 0.0, 0.8), RGB(0.8, 0.0, 0.8), RGB(0.0, 0.8, 0.8), RGB(0.8, 0.8, 0.8), + RGB(0.5, 0.5, 0.5), RGB(1.0, 0.4, 0.4), RGB(0.4, 1.0, 0.4), RGB(1.0, 1.0, 0.4), + RGB(0.4, 0.4, 1.0), RGB(1.0, 0.4, 1.0), RGB(0.4, 1.0, 1.0), RGB(1.0, 1.0, 1.0) + } + } + }, + { "light", N_("Light"), .palette = { + .foreground = RGB(0.15, 0.15, 0.15), .background = RGB(0.98, 0.98, 0.98), + .palette = { + RGB(0.2, 0.2, 0.2), RGB(0.8, 0.2, 0.2), RGB(0.1, 0.6, 0.1), RGB(0.7, 0.6, 0.1), + RGB(0.2, 0.4, 0.7), RGB(0.6, 0.3, 0.5), RGB(0.3, 0.6, 0.7), RGB(0.7, 0.7, 0.7), + RGB(0.4, 0.4, 0.4), RGB(0.9, 0.3, 0.3), RGB(0.2, 0.7, 0.2), RGB(0.8, 0.7, 0.2), + RGB(0.3, 0.5, 0.8), RGB(0.7, 0.4, 0.6), RGB(0.4, 0.7, 0.8), RGB(0.9, 0.9, 0.9) + } + } + }, + { "solarized-dark", N_("Solarized Dark"), .palette = { + .foreground = RGB(0.8235, 0.8588, 0.8706), .background = RGB(0.0000, 0.1686, 0.2118), + .palette = { + RGB(0.0275, 0.2118, 0.2588), RGB(0.8627, 0.1961, 0.1843), RGB(0.5216, 0.6000, 0.0000), RGB(0.7098, 0.5412, 0.0000), + RGB(0.1490, 0.5451, 0.8235), RGB(0.8275, 0.2118, 0.5098), RGB(0.1647, 0.6314, 0.6000), RGB(0.9294, 0.9098, 0.8353), + RGB(0.0000, 0.1686, 0.2118), RGB(0.8000, 0.2588, 0.2078), RGB(0.3725, 0.4235, 0.4314), RGB(0.4078, 0.4745, 0.4784), + RGB(0.5137, 0.5804, 0.5843), RGB(0.4235, 0.4431, 0.6118), RGB(0.5804, 0.6078, 0.5373), RGB(0.9922, 0.9647, 0.8902) + } + } + }, + { "solarized-light", N_("Solarized Light"), .palette = { + .foreground = RGB(0.4000, 0.4784, 0.5098), .background = RGB(0.9922, 0.9647, 0.8902), + .palette = { + RGB(0.0275, 0.2118, 0.2588), RGB(0.8627, 0.1961, 0.1843), RGB(0.5216, 0.6000, 0.0000), RGB(0.7098, 0.5412, 0.0000), + RGB(0.1490, 0.5451, 0.8235), RGB(0.8275, 0.2118, 0.5098), RGB(0.1647, 0.6314, 0.6000), RGB(0.9294, 0.9098, 0.8353), + RGB(0.0000, 0.1686, 0.2118), RGB(0.8000, 0.2588, 0.2078), RGB(0.3725, 0.4235, 0.4314), RGB(0.4078, 0.4745, 0.4784), + RGB(0.5137, 0.5804, 0.5843), RGB(0.4235, 0.4431, 0.6118), RGB(0.5804, 0.6078, 0.5373), RGB(0.8235, 0.8588, 0.8706) + } + } + }, + { "matrix", N_("Matrix"), .palette = { + .foreground = RGB(0.1, 0.9, 0.1), .background = RGB(0.0, 0.0, 0.0), + .palette = { + RGB(0.0, 0.0, 0.0), RGB(0.0, 0.5, 0.0), RGB(0.0, 0.8, 0.0), RGB(0.1, 0.6, 0.0), + RGB(0.0, 0.4, 0.0), RGB(0.1, 0.5, 0.1), RGB(0.0, 0.7, 0.1), RGB(0.1, 0.9, 0.1), + RGB(0.0, 0.3, 0.0), RGB(0.0, 0.6, 0.0), RGB(0.0, 1.0, 0.0), RGB(0.2, 0.7, 0.0), + RGB(0.0, 0.5, 0.0), RGB(0.2, 0.6, 0.2), RGB(0.0, 0.8, 0.2), RGB(0.2, 1.0, 0.2) + } + } + }, + { "one-half-dark", N_("One Half Dark"), .palette = { + .foreground = RGB(0.870, 0.870, 0.870), .background = RGB(0.157, 0.168, 0.184), + .palette = { + RGB(0.157, 0.168, 0.184), RGB(0.882, 0.490, 0.470), RGB(0.560, 0.749, 0.450), RGB(0.941, 0.768, 0.470), + RGB(0.400, 0.627, 0.850), RGB(0.768, 0.470, 0.800), RGB(0.341, 0.709, 0.729), RGB(0.870, 0.870, 0.870), + RGB(0.400, 0.450, 0.500), RGB(0.882, 0.490, 0.470), RGB(0.560, 0.749, 0.450), RGB(0.941, 0.768, 0.470), + RGB(0.400, 0.627, 0.850), RGB(0.768, 0.470, 0.800), RGB(0.341, 0.709, 0.729), RGB(0.970, 0.970, 0.970) + } + } + }, + { "one-half-light", N_("One Half Light"), .palette = { + .foreground = RGB(0.220, 0.240, 0.260), .background = RGB(0.980, 0.980, 0.980), + .palette = { + RGB(0.220, 0.240, 0.260), RGB(0.858, 0.200, 0.180), RGB(0.310, 0.600, 0.110), RGB(0.850, 0.588, 0.100), + RGB(0.231, 0.490, 0.749), RGB(0.670, 0.270, 0.729), RGB(0.149, 0.639, 0.678), RGB(0.800, 0.800, 0.800), + RGB(0.400, 0.400, 0.400), RGB(0.858, 0.200, 0.180), RGB(0.310, 0.600, 0.110), RGB(0.850, 0.588, 0.100), + RGB(0.231, 0.490, 0.749), RGB(0.670, 0.270, 0.729), RGB(0.149, 0.639, 0.678), RGB(0.080, 0.080, 0.080) + } + } + }, + { "monokai", N_("Monokai"), .palette = { + .foreground = RGB(0.929, 0.925, 0.910), .background = RGB(0, 0, 0), + .palette = { + RGB(0.153, 0.157, 0.149), RGB(0.980, 0.149, 0.450), RGB(0.650, 0.890, 0.180), RGB(0.960, 0.780, 0.310), + RGB(0.208, 0.580, 0.839), RGB(0.670, 0.380, 0.960), RGB(0.400, 0.950, 0.950), RGB(0.929, 0.925, 0.910), + RGB(0.400, 0.400, 0.400), RGB(0.980, 0.149, 0.450), RGB(0.650, 0.890, 0.180), RGB(0.960, 0.780, 0.310), + RGB(0.208, 0.580, 0.839), RGB(0.670, 0.380, 0.960), RGB(0.400, 0.950, 0.950), RGB(1.000, 1.000, 1.000) + } + } + }, +}; +/* clang-format on */ -/** - * on_terminal_button_press: - * @widget: The #VteTerminal widget where the button press occurred. - * @event: The #GdkEventButton for the button press. - * @user_data: The #NemoTerminalWidget instance. - * - * Handles button press events in the terminal, primarily to show the - * context menu on a secondary (right) click. - * - * Returns: %TRUE if the event was handled (menu shown), %FALSE otherwise. - */ -static gboolean -on_terminal_button_press(GtkWidget *widget, - GdkEventButton *event, - gpointer user_data) +typedef struct { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + int size_pts; +} MenuFontSizeEntry; - // Show context menu on right-click (button 3, or secondary button) - if (event->button == GDK_BUTTON_SECONDARY && event->type == GDK_BUTTON_PRESS) - { - GtkWidget *menu = create_terminal_popup_menu(self); - gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)event); - return TRUE; - } - return FALSE; -} +static const MenuFontSizeEntry FONT_SIZE_ENTRIES[] = { + { 9 }, { 10 }, { 11 }, { 12 }, { 13 }, { 14 }, { 15 }, { 16 }, + { 17 }, { 18 }, { 20 }, { 22 }, { 24 }, { 28 }, { 32 }, { 36 }, { 40 }, { 48 } +}; -/** - * on_copy_activate: - * @action: The "copy" #GSimpleAction that was activated. - * @parameter: (Unused) Parameters for the action. - * @user_data: The #NemoTerminalWidget instance. - * - * Action handler for "copy". Copies selected text from the terminal to the clipboard. - */ -static void -on_copy_activate(GSimpleAction *action, - GVariant *parameter, - gpointer user_data) +typedef struct { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); - vte_terminal_copy_clipboard_format(VTE_TERMINAL(self->terminal), VTE_FORMAT_TEXT); -} + NemoTerminalSyncMode mode; + const gchar *label_pot; +} MenuSyncModeEntry; -/** - * on_paste_activate: - * @action: The "paste" #GSimpleAction that was activated. - * @parameter: (Unused) Parameters for the action. - * @user_data: The #NemoTerminalWidget instance. - * - * Action handler for "paste". Pastes text from the clipboard into the terminal. - */ -static void -on_paste_activate(GSimpleAction *action, - GVariant *parameter, - gpointer user_data) -{ - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); - vte_terminal_paste_clipboard(self->terminal); -} +static const MenuSyncModeEntry LOCAL_SYNC_MODE_ENTRIES[] = { + { NEMO_TERMINAL_SYNC_BOTH, N_("Sync Both Ways") }, + { NEMO_TERMINAL_SYNC_FM_TO_TERM, N_("Sync File Manager → Terminal") }, + { NEMO_TERMINAL_SYNC_TERM_TO_FM, N_("Sync Terminal → File Manager") }, + { NEMO_TERMINAL_SYNC_NONE, N_("No Sync") } +}; -/** - * on_select_all_activate: - * @action: The "select-all" #GSimpleAction that was activated. - * @parameter: (Unused) Parameters for the action. - * @user_data: The #NemoTerminalWidget instance. - * - * Action handler for "select-all". Selects all text in the terminal. - */ -static void -on_select_all_activate(GSimpleAction *action, - GVariant *parameter, - gpointer user_data) +typedef struct { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); - vte_terminal_select_all(self->terminal); -} + NemoTerminalSshAutoConnectMode mode; + const gchar *label_pot; +} MenuSshAutoConnectEntry; -/** - * on_font_size_changed: - * @widget: The #GtkRadioMenuItem for font size that was activated. - * @user_data: The #NemoTerminalWidget instance. - * - * Callback when a font size is selected from the context menu. - * Updates the terminal's font size and saves the setting. - */ -static void -on_font_size_changed(GtkWidget *widget, gpointer user_data) -{ - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); +static const MenuSshAutoConnectEntry SFTP_AUTO_CONNECT_ENTRIES[] = { + { NEMO_TERMINAL_SSH_AUTOCONNECT_OFF, N_("Do not connect automatically") }, + { NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH, N_("Automatically connect and sync both ways") }, + { NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM, N_("Automatically connect and sync: File Manager → Terminal") }, + { NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM, N_("Automatically connect and sync: Terminal → File Manager") }, + { NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_NONE, N_("Automatically connect without syncing") } +}; - // Only act if the radio item is being activated (not deactivated) - if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget))) - return; +static const MenuSyncModeEntry MANUAL_SSH_SYNC_ENTRIES[] = { + { NEMO_TERMINAL_SYNC_BOTH, N_("Sync folder both ways") }, + { NEMO_TERMINAL_SYNC_FM_TO_TERM, N_("Sync folder from File Manager → Terminal") }, + { NEMO_TERMINAL_SYNC_TERM_TO_FM, N_("Sync folder from Terminal → File Manager") }, + { NEMO_TERMINAL_SYNC_NONE, N_("No folder sync") } +}; - gpointer size_data = g_object_get_data(G_OBJECT(widget), DATA_KEY_FONT_SIZE); - if (size_data != NULL) - { - int font_size_pts = GPOINTER_TO_INT(size_data); +static const gchar *const DATA_KEY_VALUE = "ntw-value"; - g_autoptr(PangoFontDescription) font_desc = pango_font_description_copy( - vte_terminal_get_font(self->terminal)); +/* Forward declarations */ +static void spawn_terminal_async(NemoTerminalWidget *self); +static void on_terminal_child_exited(VteTerminal *terminal, gint status, gpointer user_data); +static gboolean on_terminal_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data); +static gboolean on_terminal_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data); +static void on_terminal_directory_changed(VteTerminal *terminal, gpointer user_data); +static void on_color_scheme_changed(GtkCheckMenuItem *menuitem, gpointer user_data); +static void on_font_size_changed(GtkCheckMenuItem *menuitem, gpointer user_data); +static void on_enum_pref_changed(GtkCheckMenuItem *menuitem, gpointer user_data); +static void on_terminal_preference_changed(GSettings *settings, const gchar *key, gpointer user_data); +static void setup_terminal_font(VteTerminal *terminal); +static void nemo_terminal_widget_apply_color_scheme(NemoTerminalWidget *self); +static void _initiate_ssh_connection(NemoTerminalWidget *self, const gchar *hostname, const gchar *username, const gchar *port, NemoTerminalSyncMode sync_mode); +static gboolean parse_gvfs_ssh_path(GFile *location, gchar **hostname, gchar **username, gchar **port); +static void change_directory_in_terminal(NemoTerminalWidget *self, GFile *location); +static void _clear_ssh_connection_data(NemoTerminalWidgetPrivate *priv); +static void _reset_to_local_state(NemoTerminalWidget *self); +static const gchar * nemo_terminal_widget_get_color_scheme(NemoTerminalWidget *self); +static void nemo_terminal_widget_set_color_scheme(NemoTerminalWidget *self, const gchar *scheme); - pango_font_description_set_size(font_desc, font_size_pts * PANGO_SCALE); - vte_terminal_set_font(self->terminal, font_desc); - nemo_terminal_widget_save_font_size(self, font_size_pts); // Save the setting - } -} +static GParamSpec *properties[N_PROPS]; +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE_WITH_CODE(NemoTerminalWidget, nemo_terminal_widget, GTK_TYPE_BOX, + G_ADD_PRIVATE(NemoTerminalWidget)) -/** - * on_color_scheme_changed: - * @widget: The #GtkRadioMenuItem for color scheme that was activated. - * @user_data: The #NemoTerminalWidget instance. - * - * Callback when a color scheme is selected from the context menu. - * Sets the new color scheme for the terminal. - */ static void -on_color_scheme_changed(GtkWidget *widget, gpointer user_data) +nemo_terminal_widget_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); - - if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget))) - return; + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(object); - const gchar *scheme_name = g_object_get_data(G_OBJECT(widget), DATA_KEY_SCHEME_NAME); - if (scheme_name != NULL) + switch (prop_id) { - nemo_terminal_widget_set_color_scheme(self, scheme_name); + case PROP_CURRENT_LOCATION: + nemo_terminal_widget_set_current_location(self, g_value_get_object(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; } } -/** - * on_local_sync_mode_changed: - * @widget: The #GtkRadioMenuItem for local sync mode that was activated. - * @user_data: The #NemoTerminalWidget instance. - * - * Callback when the local folder synchronization mode is changed via the menu. - * Updates the widget's state, saves the setting, and may respawn the terminal - * to apply new PROMPT_COMMAND if needed. - */ static void -on_local_sync_mode_changed(GtkWidget *widget, gpointer user_data) +nemo_terminal_widget_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); - - if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget))) - return; - - gpointer mode_data = g_object_get_data(G_OBJECT(widget), DATA_KEY_LOCAL_SYNC_MODE); - NemoTerminalSyncMode new_mode = GPOINTER_TO_INT(mode_data); - - if (self->local_sync_mode == new_mode) return; // No change - - self->local_sync_mode = new_mode; - g_settings_set_enum(nemo_window_state, "local-terminal-sync-mode", new_mode); + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(object); + NemoTerminalWidgetPrivate *priv = self->priv; - // If not in SSH mode, changing local sync settings might require respawning - // the terminal to update PROMPT_COMMAND for OSC7. - if (!self->in_ssh_mode) + switch (prop_id) { - // Respawn to ensure PROMPT_COMMAND is correctly set/unset for OSC7. - // This provides immediate feedback of the new sync mode. - spawn_terminal_in_widget(self); + case PROP_CURRENT_LOCATION: + g_value_set_object(value, priv->current_location); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; } } -/** - * on_sftp_auto_connect_behavior_changed: - * @widget: The #GtkRadioMenuItem for SSH auto-connect behavior that was activated. - * @user_data: The #NemoTerminalWidget instance. - * - * Callback when the SFTP/SSH auto-connect behavior is changed via the menu. - * Updates widget state, saves the setting, and may initiate an SSH connection - * if an auto-connect option is chosen and the current location is SFTP. - */ static void -on_sftp_auto_connect_behavior_changed(GtkWidget *widget, gpointer user_data) +nemo_terminal_widget_finalize(GObject *object) { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(object); + NemoTerminalWidgetPrivate *priv = self->priv; + g_autoptr(GtkWidget) paned = g_weak_ref_get(&priv->paned_weak_ref); - if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget))) - return; + g_signal_handlers_disconnect_by_data(nemo_window_state, self); - gpointer mode_data = g_object_get_data(G_OBJECT(widget), DATA_KEY_SFTP_AUTO_CONNECT_MODE); - NemoTerminalSshAutoConnectMode new_auto_mode = GPOINTER_TO_INT(mode_data); + if (paned) + { + g_signal_handlers_disconnect_by_data(paned, self); + } + g_weak_ref_clear(&priv->paned_weak_ref); - if (self->ssh_auto_connect_mode == new_auto_mode) return; // No change + if (priv->focus_timeout_id > 0) + g_source_remove(priv->focus_timeout_id); - self->ssh_auto_connect_mode = new_auto_mode; - g_settings_set_enum(nemo_window_state, "ssh-terminal-auto-connect-mode", new_auto_mode); + g_cancellable_cancel(priv->spawn_cancellable); + g_clear_object(&priv->spawn_cancellable); + g_clear_object(&priv->current_location); + g_clear_pointer(&priv->color_scheme, g_free); - // If an auto-connect option was selected (not "OFF") and we are not already in SSH: - if (new_auto_mode != NEMO_TERMINAL_SSH_AUTOCONNECT_OFF && !self->in_ssh_mode) - { - // Retrieve SSH connection details stored on the menu item. - // These would have been populated if the current location was SFTP when the menu was built. - const gchar *hostname = g_object_get_data(G_OBJECT(widget), DATA_KEY_SSH_HOSTNAME); - const gchar *username = g_object_get_data(G_OBJECT(widget), DATA_KEY_SSH_USERNAME); - const gchar *port = g_object_get_data(G_OBJECT(widget), DATA_KEY_SSH_PORT); + _clear_ssh_connection_data(priv); - if (hostname) // Hostname is essential for connection - { - NemoTerminalSyncMode sync_mode_for_connection; - // Determine the sync mode based on the chosen auto-connect behavior - switch (new_auto_mode) - { - case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH: - sync_mode_for_connection = NEMO_TERMINAL_SYNC_BOTH; - break; - case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM: - sync_mode_for_connection = NEMO_TERMINAL_SYNC_FM_TO_TERM; - break; - case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM: - sync_mode_for_connection = NEMO_TERMINAL_SYNC_TERM_TO_FM; - break; - case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_NONE: - sync_mode_for_connection = NEMO_TERMINAL_SYNC_NONE; - break; - default: // Should not happen - g_warning("Unexpected SSH auto-connect mode for immediate connection: %d", new_auto_mode); - return; - } - _initiate_ssh_connection(self, hostname, username, port, sync_mode_for_connection); - } - // If hostname is NULL, it implies the menu was likely opened on a non-SFTP path, - // so no immediate connection is attempted. The setting is saved for future SFTP navigation. - } + G_OBJECT_CLASS(nemo_terminal_widget_parent_class)->finalize(object); } -/** - * nemo_terminal_widget_class_init: - * @klass: The #NemoTerminalWidgetClass to initialize. - * - * GObject class initialization function. Sets up signals and properties for the widget. - */ static void nemo_terminal_widget_class_init(NemoTerminalWidgetClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); - GParamFlags flags; object_class->set_property = nemo_terminal_widget_set_property; object_class->get_property = nemo_terminal_widget_get_property; object_class->finalize = nemo_terminal_widget_finalize; - flags = G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY; - properties[PROP_CURRENT_LOCATION] = g_param_spec_object("current-location", "Current Location", "The GFile representing the current directory.", G_TYPE_FILE, - flags); + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); - g_object_class_install_property(object_class, PROP_CURRENT_LOCATION, properties[PROP_CURRENT_LOCATION]); + g_object_class_install_properties(object_class, N_PROPS, properties); signals[CHANGE_DIRECTORY] = g_signal_new("change-directory", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, - 0, - NULL, NULL, + 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, - G_TYPE_NONE, - 1, - G_TYPE_FILE); + G_TYPE_NONE, 1, G_TYPE_FILE); signals[TOGGLE_VISIBILITY] = g_signal_new("toggle-visibility", @@ -1546,1478 +386,1045 @@ nemo_terminal_widget_class_init(NemoTerminalWidgetClass *klass) G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__BOOLEAN, - G_TYPE_NONE, - 1, - G_TYPE_BOOLEAN); + G_TYPE_NONE, 1, G_TYPE_BOOLEAN); } static void -nemo_terminal_widget_set_property(GObject *object, - guint prop_id, - const GValue *value, - GParamSpec *pspec) +nemo_terminal_widget_init(NemoTerminalWidget *self) { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(object); + NemoTerminalWidgetPrivate *priv = nemo_terminal_widget_get_instance_private(self); + GtkStyleContext *context = NULL; + g_autoptr(GtkCssProvider) provider = NULL; + GtkWidget *vbox; + + self->priv = priv; + + priv->state = NEMO_TERMINAL_STATE_LOCAL; + priv->needs_respawn = TRUE; + priv->child_pid = -1; + priv->spawn_cancellable = g_cancellable_new(); + g_weak_ref_init(&priv->paned_weak_ref, NULL); + + priv->scrolled_window = gtk_scrolled_window_new(NULL, NULL); + gtk_widget_set_vexpand(priv->scrolled_window, TRUE); + gtk_widget_set_hexpand(priv->scrolled_window, TRUE); + + priv->terminal = VTE_TERMINAL(vte_terminal_new()); + vte_terminal_set_scroll_on_output(priv->terminal, FALSE); + vte_terminal_set_scroll_on_keystroke(priv->terminal, TRUE); + vte_terminal_set_scrollback_lines(priv->terminal, 10000); + gtk_widget_set_can_focus(GTK_WIDGET(priv->terminal), TRUE); + + priv->ssh_indicator = gtk_label_new("SSH"); + gtk_widget_set_name(priv->ssh_indicator, "ssh-indicator"); + gtk_widget_set_no_show_all(priv->ssh_indicator, TRUE); + gtk_widget_hide(priv->ssh_indicator); + gtk_widget_set_vexpand(priv->ssh_indicator, FALSE); + gtk_widget_set_hexpand(priv->ssh_indicator, TRUE); + gtk_label_set_xalign(GTK_LABEL(priv->ssh_indicator), 0.5); - switch (prop_id) + provider = gtk_css_provider_new(); + gtk_css_provider_load_from_data(provider, "label#ssh-indicator { background-color: #3465a4; color: white; padding: 2px 5px; margin: 0; font-weight: bold; }", -1, NULL); + context = gtk_widget_get_style_context(priv->ssh_indicator); + gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_USER); + + vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_box_pack_start(GTK_BOX(vbox), priv->ssh_indicator, FALSE, FALSE, 0); + gtk_container_add(GTK_CONTAINER(priv->scrolled_window), GTK_WIDGET(priv->terminal)); + gtk_box_pack_start(GTK_BOX(vbox), priv->scrolled_window, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(self), vbox, TRUE, TRUE, 0); + + priv->local_sync_mode = g_settings_get_enum(nemo_window_state, "local-terminal-sync-mode"); + priv->ssh_auto_connect_mode = g_settings_get_enum(nemo_window_state, "ssh-terminal-auto-connect-mode"); + + setup_terminal_font(priv->terminal); + nemo_terminal_widget_get_color_scheme(self); + nemo_terminal_widget_apply_color_scheme(self); + + g_signal_connect(nemo_window_state, "changed", G_CALLBACK(on_terminal_preference_changed), self); + + g_signal_connect(priv->terminal, "child-exited", G_CALLBACK(on_terminal_child_exited), self); + g_signal_connect(priv->terminal, "button-press-event", G_CALLBACK(on_terminal_button_press), self); + g_signal_connect(priv->terminal, "key-press-event", G_CALLBACK(on_terminal_key_press), self); + g_signal_connect(priv->terminal, "current-directory-uri-changed", G_CALLBACK(on_terminal_directory_changed), self); + + gtk_widget_show_all(GTK_WIDGET(self)); + gtk_widget_hide(GTK_WIDGET(self)); +} + +static void +spawn_async_callback(VteTerminal *terminal, GPid pid, GError *error, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + NemoTerminalWidgetPrivate *priv = self->priv; + + if (pid == -1) { - case PROP_CURRENT_LOCATION: - nemo_terminal_widget_set_current_location(self, g_value_get_object(value)); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); - break; + g_warning("Failed to spawn terminal: %s", error ? error->message : "Unknown error"); + priv->needs_respawn = TRUE; + priv->child_pid = -1; + } + else + { + priv->child_pid = pid; + priv->needs_respawn = FALSE; + if (priv->pending_ssh_hostname) + { + _initiate_ssh_connection(self, priv->pending_ssh_hostname, priv->pending_ssh_username, priv->pending_ssh_port, priv->pending_ssh_sync_mode); + g_clear_pointer(&priv->pending_ssh_hostname, g_free); + g_clear_pointer(&priv->pending_ssh_username, g_free); + g_clear_pointer(&priv->pending_ssh_port, g_free); + } + else if (priv->current_location) + { + /* If a local shell just spawned, sync its directory to the current location. + * This is crucial for new tabs/windows where the terminal starts visible. */ + change_directory_in_terminal(self, priv->current_location); + } + } +} + +static void +spawn_terminal_async(NemoTerminalWidget *self) +{ + NemoTerminalWidgetPrivate *priv = self->priv; + g_autofree gchar *working_directory = NULL; + const gchar *shell_executable; + gchar *argv[2]; + gchar *envp[] = { + "TERM=xterm-256color", + "LC_ALL=C.UTF-8", + "COLORTERM=truecolor", + NULL + }; + + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + + if (priv->child_pid != -1) + return; + + if (g_cancellable_is_cancelled(priv->spawn_cancellable)) + { + g_clear_object(&priv->spawn_cancellable); + priv->spawn_cancellable = g_cancellable_new(); + } + + shell_executable = g_getenv("SHELL"); + if (!shell_executable || *shell_executable == '\0') + shell_executable = "/bin/sh"; + + argv[0] = (gchar *)shell_executable; + argv[1] = NULL; + + if (priv->pending_ssh_hostname != NULL) + { + working_directory = NULL; } + else if (priv->current_location && g_file_query_exists(priv->current_location, NULL)) + { + working_directory = g_file_get_path(priv->current_location); + } + + vte_terminal_spawn_async(priv->terminal, + VTE_PTY_DEFAULT, + working_directory, + argv, + envp, + G_SPAWN_SEARCH_PATH, + NULL, NULL, NULL, + -1, + priv->spawn_cancellable, + (VteTerminalSpawnAsyncCallback)spawn_async_callback, + self); } static void -nemo_terminal_widget_get_property(GObject *object, - guint prop_id, - GValue *value, - GParamSpec *pspec) +_clear_ssh_connection_data(NemoTerminalWidgetPrivate *priv) { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(object); - - switch (prop_id) - { - case PROP_CURRENT_LOCATION: - g_value_set_object(value, self->current_location); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); - break; - } + g_clear_pointer(&priv->ssh_hostname, g_free); + g_clear_pointer(&priv->ssh_username, g_free); + g_clear_pointer(&priv->ssh_port, g_free); + g_clear_pointer(&priv->ssh_remote_path, g_free); + g_clear_pointer(&priv->pending_ssh_hostname, g_free); + g_clear_pointer(&priv->pending_ssh_username, g_free); + g_clear_pointer(&priv->pending_ssh_port, g_free); } -/** - * nemo_terminal_widget_init: - * @self: The #NemoTerminalWidget instance to initialize. - * - * GObject instance initialization function. Sets up the widget's internal - * structure, VTE terminal, default settings, and connects signals. - */ static void -nemo_terminal_widget_init(NemoTerminalWidget *self) +_reset_to_local_state(NemoTerminalWidget *self) { - GtkStyleContext *context; - GtkCssProvider *provider; + NemoTerminalWidgetPrivate *priv = self->priv; - // Initialize widget members - self->scrolled_window = gtk_scrolled_window_new(NULL, NULL); - gtk_widget_set_vexpand(self->scrolled_window, TRUE); - gtk_widget_set_hexpand(self->scrolled_window, TRUE); + _clear_ssh_connection_data(priv); + priv->ssh_sync_mode = NEMO_TERMINAL_SYNC_NONE; - self->terminal = VTE_TERMINAL(vte_terminal_new()); + priv->state = NEMO_TERMINAL_STATE_LOCAL; + gtk_widget_hide(priv->ssh_indicator); + priv->needs_respawn = TRUE; +} - // SSH indicator label - self->ssh_indicator = gtk_label_new("SSH"); - gtk_widget_set_name(self->ssh_indicator, "ssh-indicator"); // For CSS styling - gtk_widget_set_no_show_all(self->ssh_indicator, TRUE); // Initially hidden - gtk_widget_hide(self->ssh_indicator); - gtk_widget_set_vexpand(self->ssh_indicator, FALSE); - gtk_widget_set_hexpand(self->ssh_indicator, TRUE); // Allow to expand horizontally - gtk_label_set_xalign(GTK_LABEL(self->ssh_indicator), 0.5); // Center the text +static void +on_terminal_child_exited(VteTerminal *terminal, gint status, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + NemoTerminalWidgetPrivate *priv = self->priv; - // Apply CSS to SSH indicator - provider = gtk_css_provider_new(); - // Basic styling for the SSH indicator label - const char *css = "label#ssh-indicator { background-color: #3465a4; color: white; padding: 2px 5px; margin: 0; font-weight: bold; }"; - gtk_css_provider_load_from_data(provider, css, -1, NULL); - context = gtk_widget_get_style_context(self->ssh_indicator); - gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_USER); - g_object_unref(provider); // Provider is now managed by style context - - // Initialize state flags - self->is_exiting_ssh = FALSE; - self->ignore_next_terminal_cd_signal = FALSE; - self->container_paned = NULL; // Will be set when integrated into UI - self->is_visible = FALSE; // Assume initially not visible until ensure_state - self->needs_respawn = FALSE; - self->in_toggling = FALSE; - self->focus_timeout_id = 0; - self->maintain_focus = TRUE; // Default to maintaining focus - - // Configure VTE terminal properties - vte_terminal_set_scroll_on_output(self->terminal, FALSE); - vte_terminal_set_scroll_on_keystroke(self->terminal, TRUE); - vte_terminal_set_scrollback_lines(self->terminal, 10000); // Generous scrollback - vte_terminal_set_allow_bold(self->terminal, TRUE); - // vte_terminal_set_mouse_autohide(self->terminal, TRUE); // Optional: auto-hide mouse cursor - - setup_terminal_font(self->terminal); // Set font based on settings - // Color scheme will be applied after self->color_scheme is initialized from settings - // nemo_terminal_widget_apply_color_scheme(self); // Deferred until color_scheme is loaded - - // Connect VTE terminal signals - g_signal_connect(self->terminal, "child-exited", G_CALLBACK(on_terminal_child_exited), self); - g_signal_connect(self->terminal, "button-press-event", G_CALLBACK(on_terminal_button_press), self); - g_signal_connect(self->terminal, "contents-changed", G_CALLBACK(on_terminal_contents_changed), self); - - // VTE signal for directory change can have different names in different versions - if (g_signal_lookup("current-directory-uri-changed", VTE_TYPE_TERMINAL)) { - g_signal_connect(self->terminal, "current-directory-uri-changed", G_CALLBACK(on_terminal_directory_changed), self); - } else if (g_signal_lookup("directory-uri-changed", VTE_TYPE_TERMINAL)) { // Older VTE versions - g_signal_connect(self->terminal, "directory-uri-changed", G_CALLBACK(on_terminal_directory_changed), self); - } else { - g_warning("Could not find a suitable directory change signal for VteTerminal."); + if (gtk_widget_in_destruction(GTK_WIDGET(self))) + { + priv->child_pid = -1; + return; } - // Layout: VBox contains SSH indicator (optional) and ScrolledWindow (for terminal) - GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - gtk_box_pack_start(GTK_BOX(vbox), self->ssh_indicator, FALSE, FALSE, 0); // Indicator at the top, no expand - gtk_container_add(GTK_CONTAINER(self->scrolled_window), GTK_WIDGET(self->terminal)); - gtk_box_pack_start(GTK_BOX(vbox), self->scrolled_window, TRUE, TRUE, 0); // Scrolled window takes remaining space + priv->child_pid = -1; - // Add the VBox to this NemoTerminalWidget (which is a GtkBox itself) - gtk_box_pack_start(GTK_BOX(self), vbox, TRUE, TRUE, 0); + if (priv->state == NEMO_TERMINAL_STATE_IN_SSH) + { + _reset_to_local_state(self); + } - // Load initial settings for sync modes and color scheme - self->color_scheme = NULL; // Will be loaded by get_color_scheme on demand - nemo_terminal_widget_get_color_scheme(self); // Ensure it's loaded - nemo_terminal_widget_apply_color_scheme(self); // Apply the loaded scheme - - self->ssh_sync_mode = NEMO_TERMINAL_SYNC_NONE; // Default for new SSH sessions, can be overridden - self->ssh_auto_connect_mode = g_settings_get_enum(nemo_window_state, "ssh-terminal-auto-connect-mode"); - self->local_sync_mode = g_settings_get_enum(nemo_window_state, "local-terminal-sync-mode"); - - // Event handling for key presses (also on scrolled window for focus reasons) - gtk_widget_set_can_focus(GTK_WIDGET(self->terminal), TRUE); // VTE terminal itself should be focusable - gtk_widget_set_can_focus(self->scrolled_window, FALSE); // Scrolled window usually not directly focusable - // but key events might bubble. - // Connect key press to terminal primarily, and to self (the GtkBox) as a fallback if needed. - // Or, let key events propagate from terminal. This seems fine for now. - g_signal_connect(self->terminal, "key-press-event", G_CALLBACK(on_terminal_key_press), self); - // Scrolled window might also need to forward some key events if terminal doesn't get them. - // g_signal_connect(self->scrolled_window, "key-press-event", G_CALLBACK(on_terminal_key_press), self); - - // Setup GActionGroup for standard actions (copy, paste, etc.) - self->action_group = g_simple_action_group_new(); - g_action_map_add_action_entries(G_ACTION_MAP(self->action_group), - terminal_entries, - G_N_ELEMENTS(terminal_entries), - self); // User data for actions is self - gtk_widget_insert_action_group(GTK_WIDGET(self), "terminal", G_ACTION_GROUP(self->action_group)); - - gtk_widget_show_all(GTK_WIDGET(self)); // Show internal components - gtk_widget_hide(GTK_WIDGET(self)); // But hide the whole widget initially; visibility managed by ensure_state. + if (gtk_widget_get_ancestor(GTK_WIDGET(self), GTK_TYPE_WINDOW)) + { + if (priv->is_visible) + spawn_terminal_async(self); + else + priv->needs_respawn = TRUE; + } } -/*** Public functions ***/ - -/** - * spawn_terminal_in_widget: - * @self: The #NemoTerminalWidget instance. - * - * Spawns a new shell process inside the VTE terminal widget. - * It determines the shell to use (from $SHELL or defaults), sets the - * working directory based on `self->current_location` (if local and exists), - * and configures `PROMPT_COMMAND` for OSC7 terminal-to-FM synchronization - * if enabled for local terminals. - */ -void -spawn_terminal_in_widget(NemoTerminalWidget *self) +static void +on_terminal_preference_changed(GSettings *settings, const gchar *key, gpointer user_data) { - g_autofree char **env = NULL; - g_autoptr(GError) error = NULL; - const char *shell_executable; - g_autofree gchar *working_directory = NULL; - GPid child_pid; // VTE handles reaping this PID + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + NemoTerminalWidgetPrivate *priv = self->priv; - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + if (g_strcmp0(key, "local-terminal-sync-mode") == 0) + { + priv->local_sync_mode = g_settings_get_enum(settings, "local-terminal-sync-mode"); + } + else if (g_strcmp0(key, "ssh-terminal-auto-connect-mode") == 0) + { + priv->ssh_auto_connect_mode = g_settings_get_enum(settings, "ssh-terminal-auto-connect-mode"); + } + else if (g_strcmp0(key, "terminal-font") == 0 || g_strcmp0(key, "terminal-font-size") == 0) + { + setup_terminal_font(priv->terminal); + } + else if (g_strcmp0(key, "terminal-color-scheme") == 0) + { + g_free(priv->color_scheme); + priv->color_scheme = g_settings_get_string(settings, "terminal-color-scheme"); + nemo_terminal_widget_apply_color_scheme(self); + } +} + +static void +on_terminal_directory_changed(VteTerminal *terminal, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + NemoTerminalWidgetPrivate *priv = self->priv; + g_autoptr(GFile) new_gfile_location = NULL; + gboolean should_sync_to_fm = FALSE; + const gchar *cwd_uri = vte_terminal_get_current_directory_uri(terminal); - self->needs_respawn = FALSE; // Reset flag as we are attempting to spawn + if (!cwd_uri) return; - // Determine shell executable - shell_executable = g_getenv("SHELL"); - if (shell_executable == NULL || *shell_executable == '\0') + if (priv->ignore_next_terminal_cd_signal) { - // Fallback to common default shells - const char *default_shells[] = {"/bin/bash", "/bin/sh", NULL}; - for (int i = 0; default_shells[i]; ++i) { - if (g_file_test(default_shells[i], G_FILE_TEST_IS_EXECUTABLE)) { - shell_executable = default_shells[i]; - break; - } - } - if (shell_executable == NULL || *shell_executable == '\0') { - shell_executable = "/bin/sh"; // Ultimate fallback - g_warning("SHELL environment variable not set, and common shells not found. Defaulting to /bin/sh."); - } + priv->ignore_next_terminal_cd_signal = FALSE; + return; } - // Determine working directory (only for local, non-SSH spawns) - // For SSH, the remote shell starts in its default (e.g., home) or handled by ssh_remote_path. - if (!self->in_ssh_mode && self->current_location != NULL) + if (priv->state == NEMO_TERMINAL_STATE_IN_SSH) { - if (G_IS_FILE(self->current_location)) + if (priv->ssh_sync_mode == NEMO_TERMINAL_SYNC_BOTH || priv->ssh_sync_mode == NEMO_TERMINAL_SYNC_TERM_TO_FM) { - // Only use native paths for local shell's CWD - if (g_file_is_native(self->current_location) && - g_file_query_exists(self->current_location, NULL)) - { - working_directory = g_file_get_path(self->current_location); - } - else if (!g_file_is_native(self->current_location)) { - // Current location is remote (e.g. sftp://), spawn local shell in home. - g_warning("Current location is remote (%s) but attempting to spawn local shell. Using home directory.", - g_file_get_uri_scheme(self->current_location)); - // working_directory remains NULL, VTE will use default (usually home) - } - else // Native path but doesn't exist + should_sync_to_fm = TRUE; + if (g_str_has_prefix(cwd_uri, "file://")) { - g_autofree gchar *uri_for_warning = g_file_get_uri(self->current_location); - g_warning("Current local location %s no longer exists. Spawning terminal in home directory.", - uri_for_warning ? uri_for_warning : "(unknown URI)"); - g_set_object(&self->current_location, NULL); // Reset invalid location - // working_directory remains NULL + g_autofree gchar *local_path = g_filename_from_uri(cwd_uri, NULL, NULL); + if (local_path && priv->ssh_hostname) + { + g_autoptr(GString) sftp_uri = g_string_new("sftp://"); + if (priv->ssh_username && *priv->ssh_username) + g_string_append_printf(sftp_uri, "%s@", priv->ssh_username); + g_string_append(sftp_uri, priv->ssh_hostname); + if (priv->ssh_port && *priv->ssh_port) + g_string_append_printf(sftp_uri, ":%s", priv->ssh_port); + g_string_append(sftp_uri, local_path); + new_gfile_location = g_file_new_for_uri(sftp_uri->str); + } } } - else // self->current_location is not a GFile (should not happen if logic is correct) + } + else + { + if (priv->local_sync_mode == NEMO_TERMINAL_SYNC_BOTH || priv->local_sync_mode == NEMO_TERMINAL_SYNC_TERM_TO_FM) { - g_warning("self->current_location is not a GFile in spawn_terminal_in_widget. Spawning terminal in home directory."); - g_set_object(&self->current_location, NULL); // Reset invalid location + should_sync_to_fm = TRUE; + new_gfile_location = g_file_new_for_uri(cwd_uri); } } - char *argv[] = {(char *)shell_executable, NULL}; // Arguments for the shell - - // Spawn the shell process in the VTE terminal - vte_terminal_spawn_sync(self->terminal, - VTE_PTY_DEFAULT, // PTY flags - working_directory, // Working directory (can be NULL for default) - argv, // Command and arguments - (char **)env, // Environment variables (can be NULL for current) - G_SPAWN_SEARCH_PATH, // Spawn flags - NULL, NULL, // Child setup function and data (unused) - &child_pid, // Returns child PID (unused by us directly) - NULL, // Cancellable (unused) - &error); // GError for reporting issues - - if (error != NULL) + if (should_sync_to_fm && new_gfile_location && (priv->current_location == NULL || !g_file_equal(new_gfile_location, priv->current_location))) { - g_warning("Failed to spawn terminal (shell: %s, wd: %s): %s", - shell_executable, working_directory ? working_directory : "(default)", error->message); + g_set_object(&priv->current_location, new_gfile_location); + g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_CURRENT_LOCATION]); + g_signal_emit(self, signals[CHANGE_DIRECTORY], 0, new_gfile_location); } - // env is freed by g_autofree } -/** - * nemo_terminal_widget_get_default_height: - * - * Retrieves the default/saved height for the terminal widget from GSettings. +/* + * feed_cd_command: + * @terminal: The VteTerminal to send the command to. + * @path: The directory path to change to. * - * Returns: The terminal height in pixels. Defaults to 300 if setting is invalid or too small. + * This function programmatically sends a 'cd' command to the terminal's + * child process. It uses a sequence of shell control characters (CTRL+A, + * CTRL+K, etc.) to insert the command at the beginning of the line, + * execute it, and then restore any text the user might have been typing. + * This provides a less disruptive user experience. */ -int -nemo_terminal_widget_get_default_height(void) +static void +feed_cd_command(VteTerminal *terminal, const char *path) { - int saved_height = g_settings_get_int(nemo_window_state, "terminal-height"); - // Ensure a minimum sensible height - return (saved_height > 50 && saved_height < 8000) ? saved_height : 300; -} + g_return_if_fail(VTE_IS_TERMINAL(terminal)); + g_return_if_fail(path != NULL); -/** - * nemo_terminal_widget_save_height: - * @self: The #NemoTerminalWidget instance. - * @height: The height in pixels to save. - * - * Saves the terminal's height. Updates the internal `self->height` and - * persists the value to GSettings if it's within a reasonable range. - */ -void -nemo_terminal_widget_save_height(NemoTerminalWidget *self, int height) -{ - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + g_autofree gchar *quoted_path = g_shell_quote(path); + g_autofree gchar *cd_command_str = g_strdup_printf(" cd %s\r", quoted_path); - // Save only if height is within a reasonable range to avoid extreme values - if (height > 50 && height < 8000) // Min 50px, Max 8000px (arbitrary upper limit) - { - if (self->height != height) { // Only save if changed - self->height = height; - g_settings_set_int(nemo_window_state, "terminal-height", height); - } - } + vte_terminal_feed_child(terminal, SHELL_CTRL_A, -1); + vte_terminal_feed_child(terminal, " ", -1); + vte_terminal_feed_child(terminal, SHELL_CTRL_A, -1); + vte_terminal_feed_child(terminal, SHELL_CTRL_K, -1); + vte_terminal_feed_child(terminal, cd_command_str, -1); + vte_terminal_feed_child(terminal, SHELL_CTRL_Y, -1); + vte_terminal_feed_child(terminal, SHELL_CTRL_A, -1); + vte_terminal_feed_child(terminal, SHELL_DELETE, -1); + vte_terminal_feed_child(terminal, SHELL_CTRL_E, -1); } -/** - * nemo_terminal_widget_apply_new_size: - * @self: The #NemoTerminalWidget instance. - * - * Applies the currently stored `self->height` to the #GtkPaned container - * that holds the terminal. This adjusts the paned's divider position. - * Should be called when the terminal is visible and the paned is realized. - */ -void -nemo_terminal_widget_apply_new_size(NemoTerminalWidget *self) +static gboolean +on_terminal_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data) { - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); - - if (!self->container_paned || !GTK_IS_PANED(self->container_paned) || - !gtk_widget_get_realized(GTK_WIDGET(self->container_paned))) - return; // Paned not set, not a paned, or not realized + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + NemoTerminalWidgetPrivate *priv = self->priv; - int total_height = gtk_widget_get_allocated_height(GTK_WIDGET(self->container_paned)); - if (total_height > 0 && self->height > 0) + if ((event->state & GDK_CONTROL_MASK) && (event->state & GDK_SHIFT_MASK)) { - // Calculate new paned divider position. - // Terminal is pack2 (bottom pane). Its height is `self->height`. - // Position = total_height - terminal_height. - int new_pos = total_height - self->height; - - // Clamp position to be valid: 0 <= new_pos <= total_height - min_terminal_height (e.g. 50) - if (new_pos < 0) new_pos = 0; - // Ensure terminal retains a minimum height (e.g., 50px) - if (new_pos > total_height - 50) new_pos = total_height - 50; - - if (new_pos >= 0 && new_pos <= total_height) { // Double check validity - gtk_paned_set_position(GTK_PANED(self->container_paned), new_pos); + switch (event->keyval) + { + case GDK_KEY_C: + case GDK_KEY_c: + vte_terminal_copy_clipboard_format(priv->terminal, VTE_FORMAT_TEXT); + return TRUE; + case GDK_KEY_V: + case GDK_KEY_v: + vte_terminal_paste_clipboard(priv->terminal); + return TRUE; } } + return FALSE; } -/** - * on_paned_destroy: - * @widget: The #GtkPaned widget that is being destroyed. - * @user_data: The #NemoTerminalWidget instance. - * - * Callback for the "destroy" signal of the container paned. - * Clears the `self->container_paned` reference in the terminal widget - * to prevent dangling pointers if the paned is destroyed externally. - */ static void -on_paned_destroy(GtkWidget *widget, gpointer user_data) +on_ssh_exit_activate(GtkMenuItem *menuitem, gpointer user_data) { NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); - - if (self && NEMO_IS_TERMINAL_WIDGET(self) && self->container_paned == widget) + NemoTerminalWidgetPrivate *priv = self->priv; + if (priv->state == NEMO_TERMINAL_STATE_IN_SSH) { - // Paned is being destroyed, so nullify our reference to it. - // No need to disconnect signals here, as GTK does that on destroy. - self->container_paned = NULL; + vte_terminal_feed_child(priv->terminal, " exit\n", -1); } } -/** - * nemo_terminal_widget_initialize_in_paned: - * @self: The #NemoTerminalWidget instance. - * @unused_view_content: (Unused) Original content widget. The `view_overlay` is used instead. - * @view_overlay: The #GtkWidget (typically an overlay or main view area) that will - * become the top child of the new #GtkPaned. - * - * Integrates the terminal widget into the UI by creating a new #GtkPaned. - * The @view_overlay is reparented into the top part of the paned, and - * the terminal widget (#NemoTerminalWidget) is placed in the bottom part. - * The new paned then replaces @view_overlay in its original parent. - * - * Returns: %TRUE if initialization was successful, %FALSE otherwise. - */ -gboolean -nemo_terminal_widget_initialize_in_paned(NemoTerminalWidget *self, - GtkWidget *unused_view_content, - GtkWidget *view_overlay) +static GtkWidget * +_create_radio_menu_item(GSList **group, const gchar *label, gboolean is_active, GCallback activate_callback, gpointer user_data, const gchar *data_key, gpointer data_value) { - g_return_val_if_fail(NEMO_IS_TERMINAL_WIDGET(self), FALSE); - - if (!view_overlay || !gtk_widget_get_parent(view_overlay)) - { - g_warning("Cannot add terminal: view_overlay is NULL or has no parent."); - return FALSE; - } + GtkWidget *item = gtk_radio_menu_item_new_with_label(*group, label); + if (*group == NULL) + *group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item)); - GtkWidget *parent_container = gtk_widget_get_parent(view_overlay); - if (!GTK_IS_CONTAINER(parent_container)) { - g_warning("Cannot add terminal: parent of view_overlay is not a GtkContainer."); - return FALSE; - } - - // Create a new vertical paned - GtkWidget *vpaned = gtk_paned_new(GTK_ORIENTATION_VERTICAL); - self->container_paned = vpaned; // Store reference to the paned - - // Preserve packing properties if parent was a GtkBox - gint position_in_parent = -1; - gboolean box_expand = TRUE, box_fill = TRUE; // Defaults for GtkBox - guint box_padding = 0; - - if (GTK_IS_BOX(parent_container)) { - GtkBox *box_parent = GTK_BOX(parent_container); - gtk_box_query_child_packing(box_parent, view_overlay, &box_expand, &box_fill, &box_padding, NULL); - - // Get original position of view_overlay to reinsert paned at same spot - g_autoptr(GList) children = gtk_container_get_children(GTK_CONTAINER(parent_container)); - position_in_parent = g_list_index(children, view_overlay); - } + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), is_active); + g_signal_connect(item, "activate", activate_callback, user_data); + g_object_set_data(G_OBJECT(item), data_key, data_value); - // Reparent view_overlay into the paned - g_object_ref(view_overlay); // Increment ref before removing from old parent - gtk_container_remove(GTK_CONTAINER(parent_container), view_overlay); + return item; +} - gtk_paned_pack1(GTK_PANED(vpaned), view_overlay, TRUE, FALSE); // view_overlay in top, resize=TRUE, shrink=FALSE - gtk_paned_pack2(GTK_PANED(vpaned), GTK_WIDGET(self), FALSE, TRUE); // terminal in bottom, resize=FALSE, shrink=TRUE - g_object_unref(view_overlay); // Decrement ref, paned now owns it +static void on_ssh_connect_activate(GtkMenuItem *menuitem, gpointer user_data); - // Add the new paned to the original parent container - if (GTK_IS_BOX(parent_container)) { - gtk_box_pack_start(GTK_BOX(parent_container), vpaned, box_expand, box_fill, box_padding); - if (position_in_parent != -1) { - gtk_box_reorder_child(GTK_BOX(parent_container), vpaned, position_in_parent); - } - } else { // For other container types (e.g., GtkOverlay, GtkGrid - though grid needs attach) - gtk_container_add(GTK_CONTAINER(parent_container), vpaned); - } +static GtkWidget * +_build_color_scheme_submenu(NemoTerminalWidget *self) +{ + GtkWidget *submenu = gtk_menu_new(); + GSList *radio_group = NULL; + const gchar *current_scheme = nemo_terminal_widget_get_color_scheme(self); - // Connect signals to the paned - if (self->container_paned) { - g_signal_connect(self->container_paned, "notify::position", - G_CALLBACK(on_container_size_changed), self); - // Also connect destroy to clear our reference if paned is removed by other means - g_signal_connect(self->container_paned, "destroy", - G_CALLBACK(on_paned_destroy), self); - } + for (gsize i = 0; i < G_N_ELEMENTS(COLOR_SCHEME_ENTRIES); ++i) + { + GtkWidget *item = _create_radio_menu_item(&radio_group, + _(COLOR_SCHEME_ENTRIES[i].label_pot), + g_strcmp0(current_scheme, COLOR_SCHEME_ENTRIES[i].id) == 0, + G_CALLBACK(on_color_scheme_changed), + self, + DATA_KEY_VALUE, + (gpointer)COLOR_SCHEME_ENTRIES[i].id); + gtk_menu_shell_append(GTK_MENU_SHELL(submenu), item); + } + return submenu; +} - gtk_widget_show_all(vpaned); // Show the paned and its children (terminal is initially hidden by ensure_state) +static int +get_terminal_font_size(void) +{ + int saved_size_pts = g_settings_get_int(nemo_window_state, "terminal-font-size"); + return CLAMP(saved_size_pts, MIN_FONT_SIZE, MAX_FONT_SIZE); +} - // Apply initial size after widgets are realized (idle callback) - g_idle_add(apply_initial_size_idle, self); - nemo_terminal_widget_ensure_state(self); // Set initial visibility and size +static GtkWidget * +_build_font_size_submenu(NemoTerminalWidget *self) +{ + GtkWidget *submenu = gtk_menu_new(); + GSList *radio_group = NULL; + int current_size_pts = get_terminal_font_size(); - return TRUE; + for (gsize i = 0; i < G_N_ELEMENTS(FONT_SIZE_ENTRIES); ++i) + { + g_autofree gchar *label = g_strdup_printf("%d", FONT_SIZE_ENTRIES[i].size_pts); + GtkWidget *item = _create_radio_menu_item(&radio_group, + label, + current_size_pts == FONT_SIZE_ENTRIES[i].size_pts, + G_CALLBACK(on_font_size_changed), + self, + DATA_KEY_VALUE, + GINT_TO_POINTER(FONT_SIZE_ENTRIES[i].size_pts)); + gtk_menu_shell_append(GTK_MENU_SHELL(submenu), item); + } + return submenu; } -/** - * nemo_terminal_widget_get_visible: +/* + * _build_enum_pref_submenu: * @self: The #NemoTerminalWidget instance. + * @entries: A static array of menu entry data. + * @count: The number of entries in the array. + * @current_value: The current value of the preference to check against. + * @settings_key: The GSettings key name for this preference. * - * Checks if the terminal widget is currently considered visible. + * A helper function to reduce code duplication when building radio-button + * submenus for enum-based preferences. It iterates over the provided + * entries, creates a radio menu item for each, and connects it to the + * generic `on_enum_pref_changed` callback. * - * Returns: %TRUE if the terminal is marked as visible, %FALSE otherwise. - * Note: This reflects the intended state; the widget itself - * might still be hidden if its parent is hidden. + * Returns: (transfer full): A new GtkMenu widget containing the radio items. */ -gboolean -nemo_terminal_widget_get_visible(NemoTerminalWidget *self) +static GtkWidget * +_build_enum_pref_submenu(NemoTerminalWidget *self, const MenuSyncModeEntry *entries, gsize count, gint current_value, const gchar *settings_key) { - g_return_val_if_fail(NEMO_IS_TERMINAL_WIDGET(self), FALSE); - return self->is_visible; + GtkWidget *submenu = gtk_menu_new(); + GSList *radio_group = NULL; + for (gsize i = 0; i < count; ++i) + { + GtkWidget *item = _create_radio_menu_item(&radio_group, + _(entries[i].label_pot), + current_value == entries[i].mode, + G_CALLBACK(on_enum_pref_changed), + (gpointer)settings_key, + DATA_KEY_VALUE, + GINT_TO_POINTER(entries[i].mode)); + gtk_menu_shell_append(GTK_MENU_SHELL(submenu), item); + } + return submenu; } -/** - * nemo_terminal_widget_ensure_state: - * @self: The #NemoTerminalWidget instance. - * - * Ensures the terminal's visibility and height match the saved settings. - * This is typically called on startup or when the UI context changes. - * If the terminal should be visible but isn't, it's shown. - * If it should be hidden but isn't, it's hidden. - * The saved height is applied if visible. - */ -void -nemo_terminal_widget_ensure_state(NemoTerminalWidget *self) +static GtkWidget * +_build_local_sync_submenu(NemoTerminalWidget *self) { - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + NemoTerminalWidgetPrivate *priv = self->priv; + return _build_enum_pref_submenu(self, LOCAL_SYNC_MODE_ENTRIES, G_N_ELEMENTS(LOCAL_SYNC_MODE_ENTRIES), priv->local_sync_mode, "local-terminal-sync-mode"); +} - gboolean should_be_visible = g_settings_get_boolean(nemo_window_state, "terminal-visible"); - self->height = nemo_terminal_widget_get_default_height(); // Load desired height +static GtkWidget * +_build_sftp_auto_connect_submenu(NemoTerminalWidget *self) +{ + NemoTerminalWidgetPrivate *priv = self->priv; + /* We can reuse the helper here, but need to cast the array type as the structs are compatible. */ + return _build_enum_pref_submenu(self, (const MenuSyncModeEntry*)SFTP_AUTO_CONNECT_ENTRIES, G_N_ELEMENTS(SFTP_AUTO_CONNECT_ENTRIES), priv->ssh_auto_connect_mode, "ssh-terminal-auto-connect-mode"); +} - if (should_be_visible != self->is_visible) - { - // Current visibility state doesn't match setting, toggle it (without saving back, as we are applying a setting) - // `is_manual_toggle = FALSE` because this is programmatic application of state - nemo_terminal_widget_toggle_visible_with_save(self, FALSE); - } - else if (should_be_visible) // Is visible and should be visible, ensure size is applied +static GtkWidget * +_build_manual_ssh_connect_submenu(NemoTerminalWidget *self, const gchar *hostname, const gchar *username, const gchar *port) +{ + GtkWidget *submenu = gtk_menu_new(); + for (gsize i = 0; i < G_N_ELEMENTS(MANUAL_SSH_SYNC_ENTRIES); ++i) { - gtk_widget_show(GTK_WIDGET(self)); // Ensure self (the GtkBox) is shown - if (self->container_paned && GTK_IS_WIDGET(self->container_paned)) { - if (gtk_widget_get_realized(GTK_WIDGET(self->container_paned))) { - nemo_terminal_widget_apply_new_size(self); - } else { - // If not realized, schedule size application for later - g_idle_add(apply_initial_size_idle, self); - } - } - if (self->needs_respawn) { // If shell exited while hidden - spawn_terminal_in_widget(self); - } - } else { // Is not visible and should not be visible - gtk_widget_hide(GTK_WIDGET(self)); - } + GtkWidget *item = gtk_menu_item_new_with_label(_(MANUAL_SSH_SYNC_ENTRIES[i].label_pot)); + g_object_set_data_full(G_OBJECT(item), DATA_KEY_SSH_HOSTNAME, g_strdup(hostname), g_free); + g_object_set_data_full(G_OBJECT(item), DATA_KEY_SSH_USERNAME, g_strdup(username), g_free); + g_object_set_data_full(G_OBJECT(item), DATA_KEY_SSH_PORT, g_strdup(port), g_free); + g_object_set_data(G_OBJECT(item), DATA_KEY_SSH_SYNC_MODE, GINT_TO_POINTER(MANUAL_SSH_SYNC_ENTRIES[i].mode)); + g_signal_connect(item, "activate", G_CALLBACK(on_ssh_connect_activate), self); + gtk_menu_shell_append(GTK_MENU_SHELL(submenu), item); + } + return submenu; } -/** - * nemo_terminal_widget_toggle_visible_with_save: - * @self: The #NemoTerminalWidget instance. - * @is_manual_toggle: %TRUE if the toggle was initiated by direct user action (e.g., F4 key), - * %FALSE if programmatic (e.g., applying settings). - * - * Toggles the visibility of the terminal widget. If becoming visible, - * applies its saved height and may attempt to grab focus if it's a manual toggle. - * The new visibility state is saved to GSettings. - * Emits the "toggle-visibility" signal. - */ -void -nemo_terminal_widget_toggle_visible_with_save(NemoTerminalWidget *self, - gboolean is_manual_toggle) +static void +_append_menu_item(GtkMenuShell *menu, const gchar *label, GCallback callback, gpointer user_data) { - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + GtkWidget *item = gtk_menu_item_new_with_label(label); + g_signal_connect(item, "activate", callback, user_data); + gtk_menu_shell_append(menu, item); +} - if (self->in_toggling) return; // Debounce: avoid rapid toggles - self->in_toggling = TRUE; +static void +_append_menu_item_with_submenu(GtkMenuShell *menu, const gchar *label, GtkWidget *submenu) +{ + GtkWidget *item = gtk_menu_item_new_with_label(label); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), submenu); + gtk_menu_shell_append(menu, item); +} - self->is_visible = !self->is_visible; // Toggle the state +static GtkWidget * +create_terminal_popup_menu(NemoTerminalWidget *self) +{ + NemoTerminalWidgetPrivate *priv = self->priv; + GtkWidget *menu = gtk_menu_new(); + gboolean is_sftp_location = FALSE; - if (self->is_visible) - { - gtk_widget_show(GTK_WIDGET(self)); // Show the terminal widget (the GtkBox) - if (self->container_paned && GTK_IS_WIDGET(self->container_paned)) { - // Apply size when shown - if (gtk_widget_get_realized(GTK_WIDGET(self->container_paned))) { - nemo_terminal_widget_apply_new_size(self); - } else { - g_idle_add(apply_initial_size_idle, self); // Apply after realization - } - } + _append_menu_item(GTK_MENU_SHELL(menu), _("Copy"), G_CALLBACK(vte_terminal_copy_clipboard_format), priv->terminal); + _append_menu_item(GTK_MENU_SHELL(menu), _("Paste"), G_CALLBACK(vte_terminal_paste_clipboard), priv->terminal); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + _append_menu_item(GTK_MENU_SHELL(menu), _("Select All"), G_CALLBACK(vte_terminal_select_all), priv->terminal); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - if (self->needs_respawn) { // If shell exited while hidden, respawn now - spawn_terminal_in_widget(self); - } + _append_menu_item_with_submenu(GTK_MENU_SHELL(menu), _("Color Scheme"), _build_color_scheme_submenu(self)); + _append_menu_item_with_submenu(GTK_MENU_SHELL(menu), _("Font Size"), _build_font_size_submenu(self)); + _append_menu_item_with_submenu(GTK_MENU_SHELL(menu), _("SSH Auto-Connect"), _build_sftp_auto_connect_submenu(self)); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - if (is_manual_toggle) { // If user explicitly showed it, focus it - nemo_terminal_widget_ensure_terminal_focus(self); - } - } - else // Becoming hidden + if (priv->current_location) { - gtk_widget_hide(GTK_WIDGET(self)); + g_autofree gchar *scheme = g_file_get_uri_scheme(priv->current_location); + if (scheme && g_strcmp0(scheme, "sftp") == 0) + is_sftp_location = TRUE; } - // Save the new visibility state to settings - g_settings_set_boolean(nemo_window_state, "terminal-visible", self->is_visible); - - // Emit signal about visibility change - g_signal_emit(self, signals[TOGGLE_VISIBILITY], 0, self->is_visible); + if (priv->state == NEMO_TERMINAL_STATE_IN_SSH) + { + _append_menu_item(GTK_MENU_SHELL(menu), _("Disconnect from SSH"), G_CALLBACK(on_ssh_exit_activate), self); + } + else + { + _append_menu_item_with_submenu(GTK_MENU_SHELL(menu), _("Local Folder Sync"), _build_local_sync_submenu(self)); + if (is_sftp_location) + { + g_autofree gchar *hostname = NULL, *username = NULL, *port = NULL; + if (parse_gvfs_ssh_path(priv->current_location, &hostname, &username, &port)) + { + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + g_autofree gchar *label = g_strdup_printf(_("SSH Connection to %s"), hostname); + _append_menu_item_with_submenu(GTK_MENU_SHELL(menu), label, _build_manual_ssh_connect_submenu(self, hostname, username, port)); + } + } + } - // Reset toggling flag after a short delay to prevent rapid re-toggling - g_timeout_add(100, reset_toggling_flag, self); // 100ms debounce + gtk_widget_show_all(menu); + return menu; } -/** - * nemo_terminal_widget_toggle_visible: - * @self: The #NemoTerminalWidget instance. - * - * Convenience function to toggle terminal visibility, assuming it's a manual action. - * Calls `nemo_terminal_widget_toggle_visible_with_save()` with `is_manual_toggle = TRUE`. - */ -void -nemo_terminal_widget_toggle_visible(NemoTerminalWidget *self) +static gboolean +on_terminal_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data) { - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); - nemo_terminal_widget_toggle_visible_with_save(self, TRUE); // Assume manual toggle -} + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); -/** - * nemo_terminal_widget_ensure_terminal_focus: - * @self: The #NemoTerminalWidget instance. - * - * Attempts to set keyboard focus to the VTE terminal widget. - * Uses an idle callback to ensure this happens after UI updates. - */ -void -nemo_terminal_widget_ensure_terminal_focus(NemoTerminalWidget *self) -{ - g_idle_add((GSourceFunc)gtk_widget_grab_focus, GTK_WIDGET(self->terminal)); + if (event->button == GDK_BUTTON_SECONDARY && event->type == GDK_BUTTON_PRESS) + { + GtkWidget *menu = create_terminal_popup_menu(self); + gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event); + return TRUE; + } + return FALSE; } -/** - * nemo_terminal_widget_set_current_location: - * @self: The #NemoTerminalWidget instance. - * @location: The #GFile representing the new current location. Can be %NULL. - * - * Sets the terminal's current location. This may involve: - * 1. Updating `self->current_location` and notifying property changes. - * 2. If the new location is SFTP and auto-connect is enabled (and not already in SSH), - * an SSH connection might be initiated. - * 3. If not initiating SSH, and the location is different, it calls - * `change_directory_in_terminal()` to `cd` in the terminal (respecting sync modes). - * 4. If @location is %NULL and not in SSH, it might respawn the terminal in the home directory. - */ -void -nemo_terminal_widget_set_current_location(NemoTerminalWidget *self, - GFile *location) +static gchar * +get_remote_path_from_sftp_gfile(GFile *location) { - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); - if (location != NULL) { // location can be NULL - g_return_if_fail(G_IS_FILE(location)); - } + g_autoptr(GUri) uri = NULL; + g_autofree gchar *uri_str = g_file_get_uri(location); + if (!uri_str) + return NULL; - // Check if the location has logically changed (different GFile or one is NULL) - gboolean location_logically_changed = FALSE; - if ((self->current_location == NULL && location != NULL) || - (self->current_location != NULL && location == NULL) || - (self->current_location != NULL && location != NULL && !g_file_equal(self->current_location, location))) { - location_logically_changed = TRUE; - } + uri = g_uri_parse(uri_str, G_URI_FLAGS_NONE, NULL); + if (uri) + return g_strdup(g_uri_get_path(uri)); - // Update the internal GFile object for current_location - gboolean object_pointer_changed = g_set_object(&self->current_location, location); + return NULL; +} - if (object_pointer_changed) // If the GFile object pointer itself changed - { - g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_CURRENT_LOCATION]); - } +static void +change_directory_in_terminal(NemoTerminalWidget *self, GFile *location) +{ + NemoTerminalWidgetPrivate *priv = self->priv; + g_autofree gchar *target_path = NULL; + gboolean should_sync = FALSE; - // If neither the object pointer nor the logical location changed, nothing more to do. - if (!location_logically_changed && !object_pointer_changed) { + if (!priv->is_visible || priv->child_pid == -1) return; - } - // Handle SSH auto-connection if navigating to an SFTP path - if (!self->in_ssh_mode && location != NULL) + if (priv->state == NEMO_TERMINAL_STATE_IN_SSH) { - g_autofree gchar *uri = g_file_get_uri(location); - if (uri && g_str_has_prefix(uri, "sftp://")) + if (priv->ssh_sync_mode == NEMO_TERMINAL_SYNC_BOTH || priv->ssh_sync_mode == NEMO_TERMINAL_SYNC_FM_TO_TERM) { - if (self->ssh_auto_connect_mode != NEMO_TERMINAL_SSH_AUTOCONNECT_OFF) - { - g_autofree gchar *hostname = NULL, *username = NULL, *port = NULL; - if (parse_gvfs_ssh_path(location, &hostname, &username, &port)) - { - NemoTerminalSyncMode sync_mode_for_auto_conn; - // Determine sync mode based on auto-connect setting - switch (self->ssh_auto_connect_mode) { - case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH: sync_mode_for_auto_conn = NEMO_TERMINAL_SYNC_BOTH; break; - case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM: sync_mode_for_auto_conn = NEMO_TERMINAL_SYNC_FM_TO_TERM; break; - case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM: sync_mode_for_auto_conn = NEMO_TERMINAL_SYNC_TERM_TO_FM; break; - case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_NONE: sync_mode_for_auto_conn = NEMO_TERMINAL_SYNC_NONE; break; - default: g_warning("Invalid SSH auto-connect mode: %d", self->ssh_auto_connect_mode); return; // Abort - } - _initiate_ssh_connection(self, hostname, username, port, sync_mode_for_auto_conn); - // SSH connection initiated, further 'cd' will be handled by SSH logic - return; // Don't fall through to change_directory_in_terminal for local - } - else { // Failed to parse SFTP path for auto-connect - g_warning("Failed to parse SFTP path for auto-connection: %s", uri); - // Proceed to treat as a GVFS mount path if local sync is on. - } - } - else // SSH auto-connect is OFF + should_sync = TRUE; + target_path = get_remote_path_from_sftp_gfile(location); + } + } + else + { + if (priv->local_sync_mode == NEMO_TERMINAL_SYNC_BOTH || priv->local_sync_mode == NEMO_TERMINAL_SYNC_FM_TO_TERM) + { + if (g_file_query_exists(location, NULL)) { - // If local sync FM->Term is on, and this is a GVFS sftp mount, cd to the *local mount point*. - if (self->local_sync_mode == NEMO_TERMINAL_SYNC_BOTH || - self->local_sync_mode == NEMO_TERMINAL_SYNC_FM_TO_TERM) + target_path = g_file_get_path(location); + if (target_path != NULL) { - g_autofree gchar *local_path = g_file_get_path(location); // Path to GVFS mount point - if (local_path && g_str_has_prefix(local_path, "/run/user/") && strstr(local_path, "/gvfs/sftp:host=")) - { - // This is a GVFS SFTP mount path. CD to it locally. - // Use the original 'location' GFile which represents this local mount point. - change_directory_in_terminal(self, location); - return; // Handled - } + should_sync = TRUE; } - // If not syncing locally or not a GVFS path, do nothing for SFTP if auto-connect is off. - return; - } - } - else // Not an SFTP URI, must be local or other non-SSH remote - { - if (location_logically_changed) { // Standard local directory change - change_directory_in_terminal(self, location); } } } - else if (self->in_ssh_mode && location != NULL) // Already in SSH mode, FM location changed + + if (should_sync && target_path) { - if (location_logically_changed) { // If FM navigates while in SSH, sync if enabled - change_directory_in_terminal(self, location); - } - } - else if (!location) { // Location became NULL (e.g., navigating to "Computer://") - if (!self->in_ssh_mode && location_logically_changed) { - // If local terminal and location becomes invalid/null, reset to home by respawning. - spawn_terminal_in_widget(self); + const gchar *term_uri = vte_terminal_get_current_directory_uri(priv->terminal); + g_autoptr(GFile) term_gfile = term_uri ? g_file_new_for_uri(term_uri) : NULL; + g_autofree gchar *term_path = term_gfile ? g_file_get_path(term_gfile) : NULL; + + if (term_path == NULL || g_strcmp0(term_path, target_path) != 0) + { + priv->ignore_next_terminal_cd_signal = TRUE; + feed_cd_command(priv->terminal, target_path); } - // If in SSH mode and location becomes NULL, typically do nothing, keep SSH session as is. } } -/** - * nemo_terminal_widget_new_with_location: - * @location: (Optional) The initial #GFile location for the terminal. - * If %NULL, the terminal will start in the default directory (e.g., home). - * - * Creates a new #NemoTerminalWidget. If @location is provided, it's set - * as the initial current location. The terminal spawns a shell process. - * - * Returns: A new #NemoTerminalWidget instance. The caller owns the returned object. - */ -NemoTerminalWidget * -nemo_terminal_widget_new_with_location(GFile *location) +static gboolean +parse_gvfs_ssh_path(GFile *location, gchar **hostname, gchar **username, gchar **port) { - // Create instance using GObject new - NemoTerminalWidget *self = g_object_new(NEMO_TYPE_TERMINAL_WIDGET, NULL); + g_autoptr(GUri) uri = NULL; + g_autofree gchar *uri_str = g_file_get_uri(location); + + *hostname = NULL; + *username = NULL; + *port = NULL; + + if (!uri_str) + return FALSE; - if (location) + uri = g_uri_parse(uri_str, G_URI_FLAGS_NONE, NULL); + if (uri && g_strcmp0(g_uri_get_scheme(uri), "sftp") == 0) { - g_return_val_if_fail(G_IS_FILE(location), NULL); // Should not happen if caller is sane - // Set initial location without triggering full sync logic yet, as spawn will handle initial CWD. - g_set_object(&self->current_location, location); + *hostname = g_strdup(g_uri_get_host(uri)); + if (g_uri_get_userinfo(uri)) + *username = g_strdup(g_uri_get_userinfo(uri)); + if (g_uri_get_port(uri) > 0) + *port = g_strdup_printf("%d", g_uri_get_port(uri)); + return (*hostname != NULL); } - self->height = nemo_terminal_widget_get_default_height(); // Load default height - spawn_terminal_in_widget(self); // Spawn shell; uses self->current_location if set and local - // Initial visibility and placement are handled by ensure_state and initialize_in_paned. - - return self; + return FALSE; } -/* Terminal color scheme definitions */ -typedef struct -{ - GdkRGBA foreground; - GdkRGBA background; - GdkRGBA palette[16]; // Standard 16 ANSI colors - gboolean use_system_colors; // If TRUE, VTE uses system theme colors -} NemoTerminalColorPalette; - -// "System" theme: delegates to VTE's default behavior (often respects GTK theme) -static const NemoTerminalColorPalette system_palette = { - .use_system_colors = TRUE -}; - -// A basic dark theme -static const NemoTerminalColorPalette dark_palette = { - .foreground = {.red = 0.9, .green = 0.9, .blue = 0.9, .alpha = 1.0}, // Light gray text - .background = {.red = 0.12, .green = 0.12, .blue = 0.12, .alpha = 1.0}, // Dark gray background - .palette = { // Standard 16 colors (8 normal, 8 bright) - {.red = 0.0, .green = 0.0, .blue = 0.0, .alpha = 1.0}, /* Black */ - {.red = 0.8, .green = 0.0, .blue = 0.0, .alpha = 1.0}, /* Red */ - {.red = 0.0, .green = 0.8, .blue = 0.0, .alpha = 1.0}, /* Green */ - {.red = 0.8, .green = 0.8, .blue = 0.0, .alpha = 1.0}, /* Yellow */ - {.red = 0.0, .green = 0.0, .blue = 0.8, .alpha = 1.0}, /* Blue */ - {.red = 0.8, .green = 0.0, .blue = 0.8, .alpha = 1.0}, /* Magenta */ - {.red = 0.0, .green = 0.8, .blue = 0.8, .alpha = 1.0}, /* Cyan */ - {.red = 0.8, .green = 0.8, .blue = 0.8, .alpha = 1.0}, /* White */ - {.red = 0.5, .green = 0.5, .blue = 0.5, .alpha = 1.0}, /* Bright Black (Grey) */ - {.red = 1.0, .green = 0.4, .blue = 0.4, .alpha = 1.0}, /* Bright Red */ - {.red = 0.4, .green = 1.0, .blue = 0.4, .alpha = 1.0}, /* Bright Green */ - {.red = 1.0, .green = 1.0, .blue = 0.4, .alpha = 1.0}, /* Bright Yellow */ - {.red = 0.4, .green = 0.4, .blue = 1.0, .alpha = 1.0}, /* Bright Blue */ - {.red = 1.0, .green = 0.4, .blue = 1.0, .alpha = 1.0}, /* Bright Magenta */ - {.red = 0.4, .green = 1.0, .blue = 1.0, .alpha = 1.0}, /* Bright Cyan */ - {.red = 1.0, .green = 1.0, .blue = 1.0, .alpha = 1.0} /* Bright White */ - }, - .use_system_colors = FALSE -}; - -// A basic light theme -static const NemoTerminalColorPalette light_palette = { - .foreground = {.red = 0.15, .green = 0.15, .blue = 0.15, .alpha = 1.0}, // Dark gray text - .background = {.red = 0.98, .green = 0.98, .blue = 0.98, .alpha = 1.0}, // Very light gray background - .palette = { - {.red = 0.2, .green = 0.2, .blue = 0.2, .alpha = 1.0}, /* Black */ - {.red = 0.8, .green = 0.2, .blue = 0.2, .alpha = 1.0}, /* Red */ - {.red = 0.1, .green = 0.6, .blue = 0.1, .alpha = 1.0}, /* Green */ - {.red = 0.7, .green = 0.6, .blue = 0.1, .alpha = 1.0}, /* Yellow */ - {.red = 0.2, .green = 0.4, .blue = 0.7, .alpha = 1.0}, /* Blue */ - {.red = 0.6, .green = 0.3, .blue = 0.5, .alpha = 1.0}, /* Magenta */ - {.red = 0.3, .green = 0.6, .blue = 0.7, .alpha = 1.0}, /* Cyan */ - {.red = 0.7, .green = 0.7, .blue = 0.7, .alpha = 1.0}, /* White */ - {.red = 0.4, .green = 0.4, .blue = 0.4, .alpha = 1.0}, /* Bright Black (Grey) */ - {.red = 0.9, .green = 0.3, .blue = 0.3, .alpha = 1.0}, /* Bright Red */ - {.red = 0.2, .green = 0.7, .blue = 0.2, .alpha = 1.0}, /* Bright Green */ - {.red = 0.8, .green = 0.7, .blue = 0.2, .alpha = 1.0}, /* Bright Yellow */ - {.red = 0.3, .green = 0.5, .blue = 0.8, .alpha = 1.0}, /* Bright Blue */ - {.red = 0.7, .green = 0.4, .blue = 0.6, .alpha = 1.0}, /* Bright Magenta */ - {.red = 0.4, .green = 0.7, .blue = 0.8, .alpha = 1.0}, /* Bright Cyan */ - {.red = 0.9, .green = 0.9, .blue = 0.9, .alpha = 1.0} /* Bright White */ - }, - .use_system_colors = FALSE -}; - -// Solarized Dark theme -static const NemoTerminalColorPalette solarized_dark_palette = { - .foreground = {.red = 0.8235, .green = 0.8588, .blue = 0.8706, .alpha = 1.0}, // base0 - .background = {.red = 0.0000, .green = 0.1686, .blue = 0.2118, .alpha = 1.0}, // base03 - .palette = { - {.red = 0.0275, .green = 0.2118, .blue = 0.2588, .alpha = 1.0}, // base02 - {.red = 0.8627, .green = 0.1961, .blue = 0.1843, .alpha = 1.0}, // red - {.red = 0.5216, .green = 0.6000, .blue = 0.0000, .alpha = 1.0}, // green - {.red = 0.7098, .green = 0.5412, .blue = 0.0000, .alpha = 1.0}, // yellow - {.red = 0.1490, .green = 0.5451, .blue = 0.8235, .alpha = 1.0}, // blue - {.red = 0.8275, .green = 0.2118, .blue = 0.5098, .alpha = 1.0}, // magenta - {.red = 0.1647, .green = 0.6314, .blue = 0.6000, .alpha = 1.0}, // cyan - {.red = 0.9294, .green = 0.9098, .blue = 0.8353, .alpha = 1.0}, // base2 - {.red = 0.0000, .green = 0.1686, .blue = 0.2118, .alpha = 1.0}, // base03 (Bright Black) - {.red = 0.8000, .green = 0.2588, .blue = 0.2078, .alpha = 1.0}, // orange (Bright Red) - {.red = 0.3725, .green = 0.4235, .blue = 0.4314, .alpha = 1.0}, // base01 (Bright Green) - {.red = 0.4078, .green = 0.4745, .blue = 0.4784, .alpha = 1.0}, // base00 (Bright Yellow) - {.red = 0.5137, .green = 0.5804, .blue = 0.5843, .alpha = 1.0}, // base0 (Bright Blue) - {.red = 0.4235, .green = 0.4431, .blue = 0.6118, .alpha = 1.0}, // violet (Bright Magenta) - {.red = 0.5804, .green = 0.6078, .blue = 0.5373, .alpha = 1.0}, // base1 (Bright Cyan) - {.red = 0.9922, .green = 0.9647, .blue = 0.8902, .alpha = 1.0} // base3 (Bright White) - }, - .use_system_colors = FALSE -}; - -// Solarized Light theme -static const NemoTerminalColorPalette solarized_light_palette = { - .foreground = {.red = 0.4000, .green = 0.4784, .blue = 0.5098, .alpha = 1.0}, // base00 - .background = {.red = 0.9922, .green = 0.9647, .blue = 0.8902, .alpha = 1.0}, // base3 - .palette = { - {.red = 0.0275, .green = 0.2118, .blue = 0.2588, .alpha = 1.0}, // base02 - {.red = 0.8627, .green = 0.1961, .blue = 0.1843, .alpha = 1.0}, // red - {.red = 0.5216, .green = 0.6000, .blue = 0.0000, .alpha = 1.0}, // green - {.red = 0.7098, .green = 0.5412, .blue = 0.0000, .alpha = 1.0}, // yellow - {.red = 0.1490, .green = 0.5451, .blue = 0.8235, .alpha = 1.0}, // blue - {.red = 0.8275, .green = 0.2118, .blue = 0.5098, .alpha = 1.0}, // magenta - {.red = 0.1647, .green = 0.6314, .blue = 0.6000, .alpha = 1.0}, // cyan - {.red = 0.9294, .green = 0.9098, .blue = 0.8353, .alpha = 1.0}, // base2 - {.red = 0.0000, .green = 0.1686, .blue = 0.2118, .alpha = 1.0}, // base03 (Bright Black) - {.red = 0.8000, .green = 0.2588, .blue = 0.2078, .alpha = 1.0}, // orange (Bright Red) - {.red = 0.3725, .green = 0.4235, .blue = 0.4314, .alpha = 1.0}, // base01 (Bright Green) - {.red = 0.4078, .green = 0.4745, .blue = 0.4784, .alpha = 1.0}, // base00 (Bright Yellow) - {.red = 0.5137, .green = 0.5804, .blue = 0.5843, .alpha = 1.0}, // base0 (Bright Blue) - {.red = 0.4235, .green = 0.4431, .blue = 0.6118, .alpha = 1.0}, // violet (Bright Magenta) - {.red = 0.5804, .green = 0.6078, .blue = 0.5373, .alpha = 1.0}, // base1 (Bright Cyan) - {.red = 0.8235, .green = 0.8588, .blue = 0.8706, .alpha = 1.0} // base0 (Bright White) - }, - .use_system_colors = FALSE -}; - -// Matrix theme (green on black) -static const NemoTerminalColorPalette matrix_palette = { - .foreground = {.red = 0.1, .green = 0.9, .blue = 0.1, .alpha = 1.0}, // Bright green text - .background = {.red = 0.0, .green = 0.0, .blue = 0.0, .alpha = 1.0}, // Pure black background - .palette = { - {.red = 0.0, .green = 0.0, .blue = 0.0, .alpha = 1.0}, /* Black */ - {.red = 0.0, .green = 0.5, .blue = 0.0, .alpha = 1.0}, /* Red (as dark green) */ - {.red = 0.0, .green = 0.8, .blue = 0.0, .alpha = 1.0}, /* Green */ - {.red = 0.1, .green = 0.6, .blue = 0.0, .alpha = 1.0}, /* Yellow (as yellow-green) */ - {.red = 0.0, .green = 0.4, .blue = 0.0, .alpha = 1.0}, /* Blue (as darker green) */ - {.red = 0.1, .green = 0.5, .blue = 0.1, .alpha = 1.0}, /* Magenta (as mid-green) */ - {.red = 0.0, .green = 0.7, .blue = 0.1, .alpha = 1.0}, /* Cyan (as cyan-green) */ - {.red = 0.1, .green = 0.9, .blue = 0.1, .alpha = 1.0}, /* White (as bright green) */ - {.red = 0.0, .green = 0.3, .blue = 0.0, .alpha = 1.0}, /* Bright Black (very dark green) */ - {.red = 0.0, .green = 0.6, .blue = 0.0, .alpha = 1.0}, /* Bright Red */ - {.red = 0.0, .green = 1.0, .blue = 0.0, .alpha = 1.0}, /* Bright Green (full green) */ - {.red = 0.2, .green = 0.7, .blue = 0.0, .alpha = 1.0}, /* Bright Yellow */ - {.red = 0.0, .green = 0.5, .blue = 0.0, .alpha = 1.0}, /* Bright Blue */ - {.red = 0.2, .green = 0.6, .blue = 0.2, .alpha = 1.0}, /* Bright Magenta */ - {.red = 0.0, .green = 0.8, .blue = 0.2, .alpha = 1.0}, /* Bright Cyan */ - {.red = 0.2, .green = 1.0, .blue = 0.2, .alpha = 1.0} /* Bright White (very bright green) */ - }, - .use_system_colors = FALSE -}; - -// One Half Dark theme (approximated from popular editor themes) -static const NemoTerminalColorPalette one_half_dark_palette = { - .foreground = {.red = 0.870, .green = 0.870, .blue = 0.870, .alpha = 1.0}, // abb2bf - .background = {.red = 0.157, .green = 0.168, .blue = 0.184, .alpha = 1.0}, // 282c34 - .palette = { - {.red = 0.157, .green = 0.168, .blue = 0.184, .alpha = 1.0}, /* Black (bg) 282c34 */ - {.red = 0.882, .green = 0.490, .blue = 0.470, .alpha = 1.0}, /* Red e06c75 */ - {.red = 0.560, .green = 0.749, .blue = 0.450, .alpha = 1.0}, /* Green 98c379 */ - {.red = 0.941, .green = 0.768, .blue = 0.470, .alpha = 1.0}, /* Yellow e5c07b */ - {.red = 0.400, .green = 0.627, .blue = 0.850, .alpha = 1.0}, /* Blue 61afef */ - {.red = 0.768, .green = 0.470, .blue = 0.800, .alpha = 1.0}, /* Magenta c678dd */ - {.red = 0.341, .green = 0.709, .blue = 0.729, .alpha = 1.0}, /* Cyan 56b6c2 */ - {.red = 0.870, .green = 0.870, .blue = 0.870, .alpha = 1.0}, /* White (fg) abb2bf */ - {.red = 0.400, .green = 0.450, .blue = 0.500, .alpha = 1.0}, /* Bright Black 5c6370 (comments) */ - {.red = 0.882, .green = 0.490, .blue = 0.470, .alpha = 1.0}, /* Bright Red (same as normal) */ - {.red = 0.560, .green = 0.749, .blue = 0.450, .alpha = 1.0}, /* Bright Green */ - {.red = 0.941, .green = 0.768, .blue = 0.470, .alpha = 1.0}, /* Bright Yellow */ - {.red = 0.400, .green = 0.627, .blue = 0.850, .alpha = 1.0}, /* Bright Blue */ - {.red = 0.768, .green = 0.470, .blue = 0.800, .alpha = 1.0}, /* Bright Magenta */ - {.red = 0.341, .green = 0.709, .blue = 0.729, .alpha = 1.0}, /* Bright Cyan */ - {.red = 0.970, .green = 0.970, .blue = 0.970, .alpha = 1.0} /* Bright White (lighter fg) */ - }, - .use_system_colors = FALSE -}; - -// One Half Light theme (approximated) -static const NemoTerminalColorPalette one_half_light_palette = { - .foreground = {.red = 0.220, .green = 0.240, .blue = 0.260, .alpha = 1.0}, // 383a42 (text) - .background = {.red = 0.980, .green = 0.980, .blue = 0.980, .alpha = 1.0}, //fafafa (bg) - .palette = { - {.red = 0.220, .green = 0.240, .blue = 0.260, .alpha = 1.0}, /* Black (fg) 383a42 */ - {.red = 0.858, .green = 0.200, .blue = 0.180, .alpha = 1.0}, /* Red e45649 */ - {.red = 0.310, .green = 0.600, .blue = 0.110, .alpha = 1.0}, /* Green 50a14f */ - {.red = 0.850, .green = 0.588, .blue = 0.100, .alpha = 1.0}, /* Yellow c18401 */ - {.red = 0.231, .green = 0.490, .blue = 0.749, .alpha = 1.0}, /* Blue 4078f2 */ - {.red = 0.670, .green = 0.270, .blue = 0.729, .alpha = 1.0}, /* Magenta a626a4 */ - {.red = 0.149, .green = 0.639, .blue = 0.678, .alpha = 1.0}, /* Cyan 0184bc */ - {.red = 0.800, .green = 0.800, .blue = 0.800, .alpha = 1.0}, /* White (light gray) a0a1a7 */ - {.red = 0.400, .green = 0.400, .blue = 0.400, .alpha = 1.0}, /* Bright Black (gray comments) 696c77 */ - {.red = 0.858, .green = 0.200, .blue = 0.180, .alpha = 1.0}, /* Bright Red */ - {.red = 0.310, .green = 0.600, .blue = 0.110, .alpha = 1.0}, /* Bright Green */ - {.red = 0.850, .green = 0.588, .blue = 0.100, .alpha = 1.0}, /* Bright Yellow */ - {.red = 0.231, .green = 0.490, .blue = 0.749, .alpha = 1.0}, /* Bright Blue */ - {.red = 0.670, .green = 0.270, .blue = 0.729, .alpha = 1.0}, /* Bright Magenta */ - {.red = 0.149, .green = 0.639, .blue = 0.678, .alpha = 1.0}, /* Bright Cyan */ - {.red = 0.080, .green = 0.080, .blue = 0.080, .alpha = 1.0} /* Bright White (darkest text) 14161a */ - }, - .use_system_colors = FALSE -}; - -// Monokai theme (classic approximation) -static const NemoTerminalColorPalette monokai_palette = { - .foreground = {.red = 0.929, .green = 0.925, .blue = 0.910, .alpha = 1.0}, // f8f8f2 - .background = {.red = 0, .green = 0, .blue = 0, .alpha = 1.0}, // 000000 - .palette = { - {.red = 0.153, .green = 0.157, .blue = 0.149, .alpha = 1.0}, /* Black (bg) 272822 */ - {.red = 0.980, .green = 0.149, .blue = 0.450, .alpha = 1.0}, /* Red f92672 */ - {.red = 0.650, .green = 0.890, .blue = 0.180, .alpha = 1.0}, /* Green a6e22e */ - {.red = 0.960, .green = 0.780, .blue = 0.310, .alpha = 1.0}, /* Yellow f4bf75 */ - {.red = 0.208, .green = 0.580, .blue = 0.839, .alpha = 1.0}, /* Blue 66d9ef (often cyan used as blue) */ - {.red = 0.670, .green = 0.380, .blue = 0.960, .alpha = 1.0}, /* Magenta ae81ff */ - {.red = 0.239, .green = 0.909, .blue = 0.920, .alpha = 1.0}, /* Cyan (using a brighter cyan) 3 sensación */ - {.red = 0.929, .green = 0.925, .blue = 0.910, .alpha = 1.0}, /* White (fg) f8f8f2 */ - {.red = 0.400, .green = 0.400, .blue = 0.400, .alpha = 1.0}, /* Bright Black (comments) 75715e */ - {.red = 0.980, .green = 0.149, .blue = 0.450, .alpha = 1.0}, /* Bright Red */ - {.red = 0.650, .green = 0.890, .blue = 0.180, .alpha = 1.0}, /* Bright Green */ - {.red = 0.960, .green = 0.780, .blue = 0.310, .alpha = 1.0}, /* Bright Yellow */ - {.red = 0.208, .green = 0.580, .blue = 0.839, .alpha = 1.0}, /* Bright Blue */ - {.red = 0.670, .green = 0.380, .blue = 0.960, .alpha = 1.0}, /* Bright Magenta */ - {.red = 0.400, .green = 0.950, .blue = 0.950, .alpha = 1.0}, /* Bright Cyan (very bright) */ - {.red = 1.000, .green = 1.000, .blue = 1.000, .alpha = 1.0} /* Bright White (pure white) */ - }, - .use_system_colors = FALSE -}; - -/** - * nemo_terminal_widget_get_color_scheme: +/* + * _initiate_ssh_connection: * @self: The #NemoTerminalWidget instance. - * - * Retrieves the name of the currently active color scheme. - * If not already loaded from GSettings, it loads it and defaults to "system" - * if the setting is missing or empty. - * - * Returns: A string representing the current color scheme name (e.g., "system", "dark"). - * This string is owned by the widget instance or is a literal and should not be freed by the caller. + * @hostname: The hostname to connect to. + * @username: The username for the connection (can be %NULL). + * @port: The port for the connection (can be %NULL). + * @sync_mode: The synchronization mode for this SSH session. + * + * The command executed on the remote host is carefully constructed to first + * change to the target directory and then start a new interactive shell. + * It also injects a `PROMPT_COMMAND` to enable directory tracking via OSC 7 + * escape sequences, which is necessary for Terminal -> File Manager sync. */ -const gchar * -nemo_terminal_widget_get_color_scheme(NemoTerminalWidget *self) +static void +_initiate_ssh_connection(NemoTerminalWidget *self, const gchar *hostname, const gchar *username, const gchar *port, NemoTerminalSyncMode sync_mode) { - g_return_val_if_fail(NEMO_IS_TERMINAL_WIDGET(self), "system"); // Default "system" on failure + NemoTerminalWidgetPrivate *priv = self->priv; + g_autoptr(GPtrArray) argv_array = g_ptr_array_new_with_free_func(g_free); + g_autofree gchar *remote_cmd = NULL; + GString *remote_cmd_builder; - if (self->color_scheme == NULL) // Lazy load from settings + if (priv->child_pid != -1) { - self->color_scheme = g_settings_get_string(nemo_window_state, "terminal-color-scheme"); - // If setting is NULL, empty, or invalid, default to "system" - if (self->color_scheme == NULL || *self->color_scheme == '\0') { - g_free(self->color_scheme); // Safe if NULL - self->color_scheme = g_strdup("system"); // Ensure it's a valid, owned string - } - // Further validation against COLOR_SCHEME_ENTRIES could be done here if needed + kill(priv->child_pid, SIGTERM); + priv->child_pid = -1; } - return self->color_scheme; -} -/** - * nemo_terminal_widget_set_color_scheme: - * @self: The #NemoTerminalWidget instance. - * @scheme_name: The name of the color scheme to set (e.g., "dark", "solarized-light"). - * - * Sets the terminal's color scheme. If the provided @scheme_name is different - * from the current one and is valid, it updates the internal state, saves the - * new scheme name to GSettings, and applies the scheme to the VTE terminal. - * If @scheme_name is invalid, it defaults to "system". - */ -void -nemo_terminal_widget_set_color_scheme(NemoTerminalWidget *self, const gchar *scheme_name) -{ - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); - g_return_if_fail(scheme_name != NULL); + priv->state = NEMO_TERMINAL_STATE_IN_SSH; + priv->ssh_sync_mode = sync_mode; + priv->ssh_hostname = g_strdup(hostname); + priv->ssh_username = g_strdup(username); + priv->ssh_port = g_strdup(port); - // Validate the scheme name against known schemes - gboolean is_valid_scheme = FALSE; - for (gsize i = 0; i < G_N_ELEMENTS(COLOR_SCHEME_ENTRIES); ++i) { - if (g_strcmp0(scheme_name, COLOR_SCHEME_ENTRIES[i].id) == 0) { - is_valid_scheme = TRUE; - break; - } - } + if (priv->current_location) + priv->ssh_remote_path = get_remote_path_from_sftp_gfile(priv->current_location); - if (!is_valid_scheme) { - g_warning("Invalid terminal color scheme requested: '%s'. Defaulting to 'system'.", scheme_name); - scheme_name = "system"; // Fallback to a known default + remote_cmd_builder = g_string_new(""); + if (priv->ssh_remote_path && *priv->ssh_remote_path) + { + g_autofree gchar *quoted_remote_path = g_shell_quote(priv->ssh_remote_path); + g_string_append_printf(remote_cmd_builder, " cd %s; ", quoted_remote_path); } + if (priv->ssh_sync_mode == NEMO_TERMINAL_SYNC_BOTH || priv->ssh_sync_mode == NEMO_TERMINAL_SYNC_TERM_TO_FM) + { + g_string_append(remote_cmd_builder, " export PROMPT_COMMAND='echo -en \"\\033]7;file://$PWD\\007\"'; "); + } + g_string_append(remote_cmd_builder, "$SHELL -l"); + remote_cmd = g_string_free(remote_cmd_builder, FALSE); - // Only update if the scheme has actually changed - if (g_strcmp0(nemo_terminal_widget_get_color_scheme(self), scheme_name) != 0) { - g_free(self->color_scheme); // Free old scheme name string - self->color_scheme = g_strdup(scheme_name); // Store new one - - g_settings_set_string(nemo_window_state, "terminal-color-scheme", self->color_scheme); - nemo_terminal_widget_apply_color_scheme(self); // Apply the new scheme visually + g_ptr_array_add(argv_array, g_strdup("ssh")); + g_ptr_array_add(argv_array, g_strdup("-t")); + if (port && *port) + { + g_ptr_array_add(argv_array, g_strdup("-p")); + g_ptr_array_add(argv_array, g_strdup(port)); } + if (username && *username) + g_ptr_array_add(argv_array, g_strdup_printf("%s@%s", username, hostname)); + else + g_ptr_array_add(argv_array, g_strdup(hostname)); + + g_ptr_array_add(argv_array, g_strdup(remote_cmd)); + g_ptr_array_add(argv_array, NULL); + + gtk_widget_show(priv->ssh_indicator); + + priv->ignore_next_terminal_cd_signal = TRUE; + + vte_terminal_spawn_async(priv->terminal, + VTE_PTY_DEFAULT, + NULL, + (gchar **)argv_array->pdata, + NULL, + G_SPAWN_SEARCH_PATH, + NULL, NULL, NULL, + -1, + priv->spawn_cancellable, + (VteTerminalSpawnAsyncCallback)spawn_async_callback, + self); + nemo_terminal_widget_ensure_terminal_focus(self); } -/** - * nemo_terminal_widget_apply_color_scheme: - * @self: The #NemoTerminalWidget instance. - * - * Applies the currently selected color scheme (stored in `self->color_scheme`) - * to the VTE terminal widget. This involves setting foreground, background, - * and palette colors, or resetting to system colors if "system" scheme is chosen. - */ -void -nemo_terminal_widget_apply_color_scheme(NemoTerminalWidget *self) +static void +save_terminal_font_size(int font_size_pts) { - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + g_settings_set_int(nemo_window_state, "terminal-font-size", font_size_pts); +} - const NemoTerminalColorPalette *palette_to_apply = NULL; - const gchar *current_scheme_name = nemo_terminal_widget_get_color_scheme(self); - - // Map scheme name to its corresponding palette definition - if (g_strcmp0(current_scheme_name, "dark") == 0) - palette_to_apply = &dark_palette; - else if (g_strcmp0(current_scheme_name, "light") == 0) - palette_to_apply = &light_palette; - else if (g_strcmp0(current_scheme_name, "solarized-dark") == 0) - palette_to_apply = &solarized_dark_palette; - else if (g_strcmp0(current_scheme_name, "solarized-light") == 0) - palette_to_apply = &solarized_light_palette; - else if (g_strcmp0(current_scheme_name, "matrix") == 0) - palette_to_apply = &matrix_palette; - else if (g_strcmp0(current_scheme_name, "one-half-dark") == 0) - palette_to_apply = &one_half_dark_palette; - else if (g_strcmp0(current_scheme_name, "one-half-light") == 0) - palette_to_apply = &one_half_light_palette; - else if (g_strcmp0(current_scheme_name, "monokai") == 0) - palette_to_apply = &monokai_palette; - else // Default to "system" scheme (includes explicit "system" or unrecognized) - palette_to_apply = &system_palette; - - // Apply the chosen palette to the VTE terminal - if (palette_to_apply->use_system_colors) +static void +on_color_scheme_changed(GtkCheckMenuItem *menuitem, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + if (gtk_check_menu_item_get_active(menuitem)) { - // Reset to VTE/system default colors - // Passing NULL or a zeroed GdkRGBA typically resets to defaults. - GdkRGBA default_color = {0}; // Zeroed structure - vte_terminal_set_color_background(self->terminal, &default_color); // Reset background - vte_terminal_set_color_foreground(self->terminal, &default_color); // Reset foreground - vte_terminal_set_colors(self->terminal, NULL, NULL, NULL, 0); // Reset palette + const gchar *scheme_name = g_object_get_data(G_OBJECT(menuitem), DATA_KEY_VALUE); + nemo_terminal_widget_set_color_scheme(self, scheme_name); } - else +} + +static void +on_font_size_changed(GtkCheckMenuItem *menuitem, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + NemoTerminalWidgetPrivate *priv = self->priv; + if (gtk_check_menu_item_get_active(menuitem)) { - // Apply the custom foreground, background, and 16-color palette - vte_terminal_set_colors(self->terminal, - &palette_to_apply->foreground, - &palette_to_apply->background, - palette_to_apply->palette, // Array of GdkRGBA - G_N_ELEMENTS(palette_to_apply->palette)); // Count of palette colors + int font_size_pts = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem), DATA_KEY_VALUE)); + g_autoptr(PangoFontDescription) font_desc = pango_font_description_copy(vte_terminal_get_font(priv->terminal)); + pango_font_description_set_size(font_desc, font_size_pts * PANGO_SCALE); + vte_terminal_set_font(priv->terminal, font_desc); + save_terminal_font_size(font_size_pts); } } -/** - * nemo_terminal_widget_finalize: - * @object: The #NemoTerminalWidget GObject instance being finalized. - * - * GObject finalize function. Frees allocated resources associated with the - * widget instance, such as GFile objects, action groups, strings, and - * disconnects signals from external objects if necessary. - */ static void -nemo_terminal_widget_finalize(GObject *object) +on_enum_pref_changed(GtkCheckMenuItem *menuitem, gpointer user_data) { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(object); - - // Disconnect signals connected to self->container_paned if it still exists - // This prevents callbacks on a partially destroyed 'self' if paned outlives 'self'. - // Note: GTK usually handles disconnection from destroyed objects, but explicit is safer for non-child objects. - if (self->container_paned && GTK_IS_WIDGET(self->container_paned)) + const gchar *pref_name = user_data; + if (gtk_check_menu_item_get_active(menuitem)) { - g_signal_handlers_disconnect_by_func(self->container_paned, G_CALLBACK(on_container_size_changed), self); - g_signal_handlers_disconnect_by_func(self->container_paned, G_CALLBACK(on_paned_destroy), self); - // Do not unref container_paned here, it's owned by its parent GTK container. - // on_paned_destroy should set self->container_paned to NULL if it's destroyed first. + gint new_value = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem), DATA_KEY_VALUE)); + g_settings_set_enum(nemo_window_state, pref_name, new_value); } - self->container_paned = NULL; // Clear reference +} - // Clean up GObject resources - g_clear_object(&self->current_location); - g_clear_object(&self->action_group); +static void +on_ssh_connect_activate(GtkMenuItem *menuitem, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + const gchar *hostname = g_object_get_data(G_OBJECT(menuitem), DATA_KEY_SSH_HOSTNAME); + const gchar *username = g_object_get_data(G_OBJECT(menuitem), DATA_KEY_SSH_USERNAME); + const gchar *port = g_object_get_data(G_OBJECT(menuitem), DATA_KEY_SSH_PORT); + NemoTerminalSyncMode sync_mode = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem), DATA_KEY_SSH_SYNC_MODE)); - // Free allocated strings - g_free(self->color_scheme); - self->color_scheme = NULL; + if (hostname) + _initiate_ssh_connection(self, hostname, username, port, sync_mode); +} - // Clear any remaining SSH state (important for freeing SSH-related strings) - clear_ssh_state(self); +static void +setup_terminal_font(VteTerminal *terminal) +{ + g_autoptr(PangoFontDescription) font_desc = NULL; + g_autofree gchar *font_name = g_settings_get_string(nemo_window_state, "terminal-font"); + int font_size = get_terminal_font_size(); - // Cancel any pending timeouts - if (self->focus_timeout_id > 0) { - g_source_remove(self->focus_timeout_id); - self->focus_timeout_id = 0; + if (font_name && *font_name) { + font_desc = pango_font_description_from_string(font_name); + } else { + font_desc = pango_font_description_from_string("Monospace"); } - // (reset_toggling_flag timeout should also be handled if it were stored with an ID) - // Chain up to the parent class's finalize method - G_OBJECT_CLASS(nemo_terminal_widget_parent_class)->finalize(object); + pango_font_description_set_size(font_desc, font_size * PANGO_SCALE); + vte_terminal_set_font(terminal, font_desc); } -/** - * nemo_terminal_get_font_size: - * - * Retrieves the saved terminal font size (in points) from GSettings. - * - * Returns: The font size in points. Defaults to 12 if the setting is - * invalid or outside a reasonable range (6-72pt). - */ -static int -nemo_terminal_get_font_size(void) +static const gchar * +nemo_terminal_widget_get_color_scheme(NemoTerminalWidget *self) { - int saved_size_pts = g_settings_get_int(nemo_window_state, "terminal-font-size"); - // Validate saved size, provide a default if out of range - return (saved_size_pts >= 6 && saved_size_pts <= 72) ? saved_size_pts : 12; // Default 12pt + NemoTerminalWidgetPrivate *priv = self->priv; + if (priv->color_scheme == NULL) { + priv->color_scheme = g_settings_get_string(nemo_window_state, "terminal-color-scheme"); + } + return priv->color_scheme; } -/** - * nemo_terminal_widget_save_font_size: - * @self: The #NemoTerminalWidget instance. - * @font_size_pts: The font size in points to save. - * - * Saves the terminal's font size (in points) to GSettings, if it's within - * a reasonable range (6-72pt). - */ static void -nemo_terminal_widget_save_font_size(NemoTerminalWidget *self, int font_size_pts) +nemo_terminal_widget_set_color_scheme(NemoTerminalWidget *self, const gchar *scheme) { - g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); - - // Persist only if font size is within a sensible range - if (font_size_pts >= 6 && font_size_pts <= 72) - { - // Only write to GSettings if it's different from current setting to avoid unnecessary writes. - if (g_settings_get_int(nemo_window_state, "terminal-font-size") != font_size_pts) { - g_settings_set_int(nemo_window_state, "terminal-font-size", font_size_pts); - } - } + NemoTerminalWidgetPrivate *priv = self->priv; + g_settings_set_string(nemo_window_state, "terminal-color-scheme", scheme); + g_free(priv->color_scheme); + priv->color_scheme = g_strdup(scheme); + nemo_terminal_widget_apply_color_scheme(self); } -/** - * build_ssh_command_string: - * @hostname: The hostname for the SSH connection (mandatory). - * @username: (Optional) The username for SSH. - * @port: (Optional) The port number for SSH as a string. - * - * Constructs the basic SSH command line string (e.g., "ssh user@host -p 2222\n"). - * Username and hostname are shell-quoted. Port is validated to be numeric - * and within the valid port range. The command always ends with a newline - * character, suitable for direct feeding to `vte_terminal_feed_child` to execute. - * - * Returns: A newly allocated string containing the SSH command. - * The caller must free this string. Returns %NULL on failure (e.g. no hostname). - */ -static gchar * -build_ssh_command_string(const gchar *hostname, const gchar *username, const gchar *port) +static void +nemo_terminal_widget_apply_color_scheme(NemoTerminalWidget *self) { - g_return_val_if_fail(hostname != NULL && *hostname != '\0', NULL); - - // GString struct itself is managed by the g_string_free call at the end when stealing the buffer. - // Do NOT use g_autofree on cmd_builder here, as g_string_free(..., FALSE) frees the struct. - GString *cmd_builder = g_string_new(" ssh "); - - // Append username if provided - if (username != NULL && *username != '\0') - { - // g_shell_quote returns a new string that must be freed. g_autofree handles this. - g_autofree gchar *quoted_username = g_shell_quote(username); - g_string_append_printf(cmd_builder, "%s@", quoted_username); - } - - // Append hostname (mandatory) - g_autofree gchar *quoted_hostname = g_shell_quote(hostname); - g_string_append(cmd_builder, quoted_hostname); - - // Append port if provided and valid - if (port != NULL && *port != '\0') - { - gboolean is_numeric_port = TRUE; - for (const gchar *p_char = port; *p_char; ++p_char) { - if (!g_ascii_isdigit(*p_char)) { - is_numeric_port = FALSE; - break; - } - } + NemoTerminalWidgetPrivate *priv = self->priv; + const gchar *scheme_id = nemo_terminal_widget_get_color_scheme(self); + const MenuSchemeEntry *scheme = NULL; + GtkStyleContext *context; + GdkRGBA fg, bg; - if (is_numeric_port) { - long port_num_long = g_ascii_strtoll(port, NULL, 10); // Base 10 - if (port_num_long > 0 && port_num_long <= 65535) { // Valid TCP/UDP port range - // Port is numeric and in range, append it. No need to quote numeric port. - g_string_append_printf(cmd_builder, " -p %s", port); - } else { - g_warning("Invalid port number specified: %s. Port option will be omitted.", port); - } - } else { - g_warning("Non-numeric port specified: %s. Port option will be omitted.", port); + for (gsize i = 0; i < G_N_ELEMENTS(COLOR_SCHEME_ENTRIES); ++i) { + if (g_strcmp0(COLOR_SCHEME_ENTRIES[i].id, scheme_id) == 0) { + scheme = &COLOR_SCHEME_ENTRIES[i]; + break; } } - g_string_append_c(cmd_builder, '\n'); // Add newline to execute command when fed + if (scheme == NULL) { /* Fallback to system */ + scheme = &COLOR_SCHEME_ENTRIES[0]; + } - // Frees the GString struct cmd_builder itself, and returns ownership of the internal char* buffer. - return g_string_free(cmd_builder, FALSE); + if (scheme->palette.use_system_colors) { + context = gtk_widget_get_style_context(GTK_WIDGET(priv->terminal)); + gtk_style_context_get_color(context, gtk_widget_get_state_flags(GTK_WIDGET(priv->terminal)), &fg); + gtk_style_context_get_background_color(context, gtk_widget_get_state_flags(GTK_WIDGET(priv->terminal)), &bg); + vte_terminal_set_colors(priv->terminal, &fg, &bg, NULL, 0); + } else { + vte_terminal_set_colors(priv->terminal, + &scheme->palette.foreground, + &scheme->palette.background, + (GdkRGBA *)scheme->palette.palette, + G_N_ELEMENTS(scheme->palette.palette)); + } } - -/** - * parse_gvfs_ssh_path: - * @location: The #GFile representing a location, potentially SFTP. - * @hostname: (Output) Pointer to store the extracted hostname. - * @username: (Output) Pointer to store the extracted username. - * @port: (Output) Pointer to store the extracted port string. - * - * Parses a #GFile's URI or path to extract SSH connection details (hostname, - * username, port) if it represents an SFTP location. - * Handles "sftp://" URIs directly. - * Also attempts to parse GVFS-style local mount paths for SFTP shares - * (e.g., "/run/user/UID/gvfs/sftp:host=example.com,user=me/path"). - * Output parameters are allocated by this function and must be freed by the caller - * if the function returns %TRUE. If %FALSE, their state is undefined but typically NULL. - * - * Returns: %TRUE if SSH details (at least hostname) were successfully parsed, %FALSE otherwise. - */ -static gboolean -parse_gvfs_ssh_path(GFile *location, gchar **hostname, gchar **username, gchar **port) +NemoTerminalWidget * +nemo_terminal_widget_new(void) { - g_return_val_if_fail(G_IS_FILE(location), FALSE); - g_return_val_if_fail(hostname != NULL && username != NULL && port != NULL, FALSE); + return g_object_new(NEMO_TYPE_TERMINAL_WIDGET, NULL); +} - // Initialize output parameters to NULL - *hostname = NULL; - *username = NULL; - *port = NULL; +NemoTerminalWidget * +nemo_terminal_widget_new_with_location(GFile *location) +{ + return g_object_new(NEMO_TYPE_TERMINAL_WIDGET, "current-location", location, NULL); +} - g_autofree gchar *uri_str = g_file_get_uri(location); - if (uri_str == NULL) return FALSE; // Cannot proceed without a URI +void +nemo_terminal_widget_set_current_location(NemoTerminalWidget *self, GFile *location) +{ + NemoTerminalWidgetPrivate *priv; + g_autofree gchar *hostname = NULL, *username = NULL, *port = NULL; + g_autofree gchar *scheme = NULL; - gboolean success = FALSE; + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + priv = self->priv; - // Try parsing as a standard "sftp://" URI first - if (g_str_has_prefix(uri_str, "sftp://")) + if (location != NULL && (priv->current_location == NULL || !g_file_equal(location, priv->current_location))) { - g_autoptr(GUri) parsed_sftp_uri = g_uri_parse(uri_str, G_URI_FLAGS_NONE, NULL); - if (parsed_sftp_uri) { - const char *parsed_host_const = g_uri_get_host(parsed_sftp_uri); - if (parsed_host_const && *parsed_host_const != '\0') { - *hostname = g_strdup(parsed_host_const); - success = TRUE; // At least hostname is found - - // Get user info (can be "user" or "user:password") - // For SFTP, typically just "user". g_uri_get_user() is better if available (GLib >= 2.66) - const char *user_info_const = g_uri_get_userinfo(parsed_sftp_uri); - if (user_info_const && *user_info_const != '\0') { - // Assuming no password in userinfo for SFTP URIs from GVFS. - // If password could be present, strchr for ':' would be needed. - *username = g_strdup(user_info_const); - } + g_set_object(&priv->current_location, location); + g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_CURRENT_LOCATION]); + } - int port_num_int = g_uri_get_port(parsed_sftp_uri); - // Only store port if it's non-standard (not 22) and valid. - if (port_num_int > 0 && port_num_int <= 65535 && port_num_int != 22) { - *port = g_strdup_printf("%d", port_num_int); - } - } - } + if (location) { + scheme = g_file_get_uri_scheme(location); } - else // Fallback: Try parsing as a local GVFS mount path for SFTP - { - g_autofree gchar *local_fs_path = g_file_get_path(location); - if (local_fs_path) - { - // Example path: /run/user/1000/gvfs/sftp:host=example.com,user=testuser/remote/folder - // Look for the characteristic GVFS sftp mount string part. - const char *gvfs_sftp_marker_prefix = "/gvfs/sftp:host="; // A common pattern - char *sftp_details_start = strstr(local_fs_path, gvfs_sftp_marker_prefix); - - if (sftp_details_start) { - // Move past "/gvfs/" to the start of "sftp:host=..." or "host=..." - sftp_details_start += strlen("/gvfs/"); - - // The details (host, user, port) are comma-separated before the actual remote path part. - // Find end of connection details part (start of actual path, or end of string) - char *path_component_start = strchr(sftp_details_start, '/'); - g_autofree gchar *details_substring = NULL; - if (path_component_start) { - details_substring = g_strndup(sftp_details_start, path_component_start - sftp_details_start); - } else { - details_substring = g_strdup(sftp_details_start); - } - g_auto(GStrv) parts = g_strsplit(details_substring, ",", -1); - for (gchar **part_iter = parts; part_iter && *part_iter; ++part_iter) - { - if (g_str_has_prefix(*part_iter, "sftp:host=")) { - g_free(*hostname); // Free previous if any (e.g. from "host=") - *hostname = g_strdup(*part_iter + strlen("sftp:host=")); - } else if (g_str_has_prefix(*part_iter, "host=") && *hostname == NULL) { // Only if sftp:host not found - *hostname = g_strdup(*part_iter + strlen("host=")); - } else if (g_str_has_prefix(*part_iter, "user=")) { - g_free(*username); - *username = g_strdup(*part_iter + strlen("user=")); - } else if (g_str_has_prefix(*part_iter, "port=")) { - g_free(*port); - *port = g_strdup(*part_iter + strlen("port=")); - } - } - // Success if hostname was found - if (*hostname != NULL && **hostname != '\0') { - success = TRUE; - } + if (priv->state == NEMO_TERMINAL_STATE_IN_SSH) { + if (scheme && g_strcmp0(scheme, "sftp") == 0) { + if (parse_gvfs_ssh_path(location, &hostname, &username, &port) && + g_strcmp0(hostname, priv->ssh_hostname) == 0) { + change_directory_in_terminal(self, location); + } else { + _reset_to_local_state(self); + spawn_terminal_async(self); } + } else { + _reset_to_local_state(self); + spawn_terminal_async(self); } - } + } else { /* Local state */ + if (scheme && g_strcmp0(scheme, "sftp") == 0 && + priv->ssh_auto_connect_mode != NEMO_TERMINAL_SSH_AUTOCONNECT_OFF && + parse_gvfs_ssh_path(location, &hostname, &username, &port)) { + + NemoTerminalSyncMode sync_mode; + switch (priv->ssh_auto_connect_mode) { + case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH: sync_mode = NEMO_TERMINAL_SYNC_BOTH; break; + case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM: sync_mode = NEMO_TERMINAL_SYNC_FM_TO_TERM; break; + case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM: sync_mode = NEMO_TERMINAL_SYNC_TERM_TO_FM; break; + default: sync_mode = NEMO_TERMINAL_SYNC_NONE; break; + } - // If parsing failed but memory was allocated for outputs, free it. - if (!success) { - g_clear_pointer(hostname, g_free); - g_clear_pointer(username, g_free); - g_clear_pointer(port, g_free); + if (priv->child_pid != -1) { + _initiate_ssh_connection(self, hostname, username, port, sync_mode); + } else { + priv->pending_ssh_hostname = g_strdup(hostname); + priv->pending_ssh_username = g_strdup(username); + priv->pending_ssh_port = g_strdup(port); + priv->pending_ssh_sync_mode = sync_mode; + } + } else { + change_directory_in_terminal(self, location); + } } - return success; } -/** - * on_terminal_contents_changed: - * @terminal: The #VteTerminal whose contents changed. - * @user_data: The #NemoTerminalWidget instance. - * - * Callback for VTE's "contents-changed" signal. - * This is used heuristically to detect when an SSH connection has likely - * become "live" (i.e., a shell prompt or login message appears). - * When `self->ssh_connecting` is TRUE, it scans recent terminal output - * for common prompt indicators. If found, it finalizes the SSH setup: - * - Sets up PROMPT_COMMAND for Term->FM sync if enabled. - * - `cd`s to the `ssh_remote_path` if set and FM->Term sync is enabled. - * - Grabs focus for the terminal. - */ -static void -on_terminal_contents_changed(VteTerminal *terminal, - gpointer user_data) +void +nemo_terminal_widget_set_container_paned(NemoTerminalWidget *self, GtkWidget *paned) { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + NemoTerminalWidgetPrivate *priv; g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + priv = self->priv; + g_weak_ref_set(&priv->paned_weak_ref, paned); +} - // If we are in the process of establishing an SSH connection: - if (self->ssh_connecting) - { - // Heuristic: Check if a prompt has appeared, indicating connection established. - // Avoid checking if there's a selection, as that might be user activity. - if (vte_terminal_get_has_selection(terminal)) return; - - glong cursor_row, cursor_col; - vte_terminal_get_cursor_position(terminal, &cursor_col, &cursor_row); - - if (cursor_row < 0 || cursor_col < 0) return; // Cursor position not valid +void +nemo_terminal_widget_toggle_visible(NemoTerminalWidget *self) +{ + NemoTerminalWidgetPrivate *priv; + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + priv = self->priv; + + priv->is_visible = !priv->is_visible; + g_settings_set_boolean(nemo_window_state, "terminal-visible", priv->is_visible); + + if (!priv->is_visible) { + g_autoptr(GtkWidget) paned = g_weak_ref_get(&priv->paned_weak_ref); + if (paned && GTK_IS_PANED(paned)) { + int position = gtk_paned_get_position(GTK_PANED(paned)); + int total_height = gtk_widget_get_allocated_height(paned); + if (total_height > 0) { + g_settings_set_int(nemo_window_state, "terminal-pane-size", total_height - position); + } + } + } - // Check a few lines of recent output for prompt-like strings. - // This is a heuristic and might not be 100% reliable for all SSH servers/shells. - glong start_scan_row = MAX(0, cursor_row - 5); // Scan last 5 lines approx. - glong terminal_cols = vte_terminal_get_column_count(terminal); + nemo_terminal_widget_ensure_state(self); + g_signal_emit(self, signals[TOGGLE_VISIBILITY], 0, priv->is_visible); +} - // Get text from a range. VTE might return less if at start/end of buffer. - g_autofree gchar *recent_text = vte_terminal_get_text_range(terminal, - start_scan_row, 0, // Start row, col - cursor_row, terminal_cols, // End row, col - NULL, NULL, NULL); // Predicates unused +void +nemo_terminal_widget_ensure_state(NemoTerminalWidget *self) +{ + NemoTerminalWidgetPrivate *priv; + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + priv = self->priv; - if (recent_text) - { - // Common shell prompt indicators or SSH welcome messages - const char *prompt_indicators[] = { - "$ ", "# ", "% ", "> ", // Common shell prompts - "@", // Often part of user@host - "~]$", "~]#", // Common Bash/Zsh full prompts - "Last login:", "Welcome to", // SSH login messages - NULL // Terminator - }; - - gboolean prompt_likely_found = FALSE; - for (int i = 0; prompt_indicators[i]; ++i) { - if (strstr(recent_text, prompt_indicators[i])) { - prompt_likely_found = TRUE; - break; - } - } + priv->is_visible = g_settings_get_boolean(nemo_window_state, "terminal-visible"); - if (prompt_likely_found) - { - // SSH connection seems to be live - self->ssh_connecting = FALSE; // No longer in "connecting" state + if (priv->is_visible) { + if (priv->needs_respawn) { + spawn_terminal_async(self); + } + gtk_widget_show(GTK_WIDGET(self)); + nemo_terminal_widget_ensure_terminal_focus(self); + } else { + gtk_widget_hide(GTK_WIDGET(self)); + } +} - // If sync Term->FM is enabled for this SSH session, set up PROMPT_COMMAND on remote. - // This is a best-effort attempt; remote shell must support PROMPT_COMMAND (e.g., bash, zsh). - if (self->pending_ssh_sync_mode == NEMO_TERMINAL_SYNC_BOTH || - self->pending_ssh_sync_mode == NEMO_TERMINAL_SYNC_TERM_TO_FM) - { - // Simple PROMPT_COMMAND for OSC7. - const char *osc7_export_cmd = " export PROMPT_COMMAND='echo -en \"\\033]7;file://$PWD\\007\"'\n"; - vte_terminal_feed_child(self->terminal, osc7_export_cmd, -1); - } +static gboolean +_ensure_focus_timeout(gpointer user_data) +{ + NemoTerminalWidget *self = user_data; + NemoTerminalWidgetPrivate *priv = self->priv; + priv->focus_timeout_id = 0; + if (priv->is_visible) { + gtk_widget_grab_focus(GTK_WIDGET(priv->terminal)); + } + return G_SOURCE_REMOVE; +} - // If a remote path was stored and FM->Term sync is enabled, cd to it. - if (self->ssh_remote_path && *self->ssh_remote_path && - (self->pending_ssh_sync_mode == NEMO_TERMINAL_SYNC_BOTH || - self->pending_ssh_sync_mode == NEMO_TERMINAL_SYNC_FM_TO_TERM)) - { - self->ignore_next_terminal_cd_signal = TRUE; // We are initiating this cd - feed_cd_command(self->terminal, self->ssh_remote_path); - } +void +nemo_terminal_widget_ensure_terminal_focus(NemoTerminalWidget *self) +{ + NemoTerminalWidgetPrivate *priv; + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + priv = self->priv; - // Connection established and initial commands sent, grab focus. - gtk_widget_grab_focus(GTK_WIDGET(self->terminal)); - } - } + if (priv->focus_timeout_id > 0) { + g_source_remove(priv->focus_timeout_id); } + priv->focus_timeout_id = g_timeout_add(100, _ensure_focus_timeout, self); } -/** - * feed_cd_command: - * @terminal: The #VteTerminal to feed the command to. - * @path: The directory path to change to. - * - * Feeds a "cd /path/to/directory\r" command to the terminal. - * It attempts to preserve any text already typed by the user on the current - * command line by using shell control sequences (Ctrl+A, Ctrl+K, Ctrl+Y). - * This is a common technique to avoid disrupting user input, especially - * with shells that have auto-suggestion features (like fish, zsh with plugins). - * The path is shell-quoted. - */ -static void -feed_cd_command(VteTerminal *terminal, const char *path) +gboolean +nemo_terminal_widget_get_visible(NemoTerminalWidget *self) { - g_return_if_fail(VTE_IS_TERMINAL(terminal)); - g_return_if_fail(path != NULL); + g_return_val_if_fail(NEMO_IS_TERMINAL_WIDGET(self), FALSE); + return self->priv->is_visible; +} - g_autofree gchar *quoted_path = g_shell_quote(path); - // Use \r (carriage return) to execute, some shells might prefer \n. \r is common. - g_autofree gchar *cd_command_str = g_strdup_printf(" cd %s\r", quoted_path); +void +nemo_terminal_widget_apply_new_size(NemoTerminalWidget *self) +{ + NemoTerminalWidgetPrivate *priv; + g_autoptr(GtkWidget) paned = NULL; + gint term_size; - if (!cd_command_str) { - g_warning("feed_cd_command: Failed to create cd command string for path: %s", path); + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); + priv = self->priv; + + paned = g_weak_ref_get(&priv->paned_weak_ref); + if (!paned || !GTK_IS_PANED(paned) || !gtk_widget_get_realized(paned)) { return; } - // This sequence aims to preserve user's current input line: - // 1. \x01 (Ctrl+A): Move cursor to start of line. - // 2. " ": Insert a space. (Ensures Ctrl+K has something to cut if line was empty, and simplifies restoration). - // 3. \x01 (Ctrl+A): Move cursor to start of line again (before the space). - // 4. \x0B (Ctrl+K): Kill (cut) text from cursor to end of line. This saves it to the shell's kill-ring. - // 5. (feed cd command): Execute the `cd` command. - // 6. \x19 (Ctrl+Y): Yank (paste) the killed text back. - // 7. \x01 (Ctrl+A): Move cursor to start of line. - // 8. \033[3~ (Delete): Delete the leading space that was inserted. (Standard VT100/xterm delete char sequence) - // 9. \x05 (Ctrl+E): Move cursor to end of line. (Restores cursor position if user was typing at end) - - vte_terminal_feed_child(terminal, "\x01 ", -1); // Ctrl+A, space - vte_terminal_feed_child(terminal, "\x01", -1); // Ctrl+A - vte_terminal_feed_child(terminal, "\x0B", -1); // Ctrl+K (cut line) - vte_terminal_feed_child(terminal, cd_command_str, -1); // Feed "cd /new/path\r" - vte_terminal_feed_child(terminal, "\x19", -1); // Ctrl+Y (paste old line) - vte_terminal_feed_child(terminal, "\x01", -1); // Ctrl+A - vte_terminal_feed_child(terminal, "\033[3~", -1); // Delete char (the space) - vte_terminal_feed_child(terminal, "\x05", -1); // Ctrl+E (end of line) + if (priv->is_visible) { + gint total_height = gtk_widget_get_allocated_height(paned); + term_size = g_settings_get_int(nemo_window_state, "terminal-pane-size"); + if (term_size <= 0) { + term_size = 200; /* Default fallback */ + } + gtk_paned_set_position(GTK_PANED(paned), total_height - term_size); + } } diff --git a/src/nemo-terminal-widget.h b/src/nemo-terminal-widget.h index 442f78cfb..b5517d845 100644 --- a/src/nemo-terminal-widget.h +++ b/src/nemo-terminal-widget.h @@ -27,6 +27,15 @@ G_BEGIN_DECLS +/** + * NemoTerminalSyncMode: + * @NEMO_TERMINAL_SYNC_NONE: No synchronization between file manager and terminal. + * @NEMO_TERMINAL_SYNC_FM_TO_TERM: File manager navigation changes the terminal's directory. + * @NEMO_TERMINAL_SYNC_TERM_TO_FM: Terminal `cd` commands change the file manager's location. + * @NEMO_TERMINAL_SYNC_BOTH: Synchronization is bidirectional. + * + * Defines the synchronization behavior for the terminal's current directory. + */ typedef enum { NEMO_TERMINAL_SYNC_NONE, @@ -35,6 +44,16 @@ typedef enum NEMO_TERMINAL_SYNC_BOTH } NemoTerminalSyncMode; +/** + * NemoTerminalSshAutoConnectMode: + * @NEMO_TERMINAL_SSH_AUTOCONNECT_OFF: Do not automatically connect to SSH when navigating to an SFTP location. + * @NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH: Automatically connect and sync both ways. + * @NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM: Automatically connect and sync from file manager to terminal. + * @NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM: Automatically connect and sync from terminal to file manager. + * @NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_NONE: Automatically connect but do not sync directories. + * + * Defines the auto-connection behavior when the file manager navigates to an SFTP location. + */ typedef enum { NEMO_TERMINAL_SSH_AUTOCONNECT_OFF, @@ -53,45 +72,12 @@ typedef enum typedef struct _NemoTerminalWidget NemoTerminalWidget; typedef struct _NemoTerminalWidgetClass NemoTerminalWidgetClass; -typedef struct _NemoWindowPane NemoWindowPane; +typedef struct _NemoTerminalWidgetPrivate NemoTerminalWidgetPrivate; struct _NemoTerminalWidget { GtkBox parent_instance; - - GtkWidget *scrolled_window; - VteTerminal *terminal; - GtkWidget *ssh_indicator; - GtkWidget *container_paned; - NemoWindowPane *pane; - - GSimpleActionGroup *action_group; - - gboolean is_visible; - gboolean maintain_focus; - gboolean in_toggling; - gboolean needs_respawn; - gboolean is_exiting_ssh; - gboolean ssh_connecting; - gboolean ignore_next_terminal_cd_signal; - - int height; - guint focus_timeout_id; - - GFile *current_location; - - gchar *color_scheme; - - gboolean in_ssh_mode; - NemoTerminalSyncMode ssh_sync_mode; - NemoTerminalSyncMode pending_ssh_sync_mode; - NemoTerminalSshAutoConnectMode ssh_auto_connect_mode; - gchar *ssh_hostname; - gchar *ssh_username; - gchar *ssh_port; - gchar *ssh_remote_path; - - NemoTerminalSyncMode local_sync_mode; + NemoTerminalWidgetPrivate *priv; }; struct _NemoTerminalWidgetClass @@ -99,33 +85,24 @@ struct _NemoTerminalWidgetClass GtkBoxClass parent_class; }; -GType nemo_terminal_widget_get_type(void); +GType nemo_terminal_widget_get_type(void) G_GNUC_CONST; NemoTerminalWidget *nemo_terminal_widget_new(void); NemoTerminalWidget *nemo_terminal_widget_new_with_location(GFile *location); -void spawn_terminal_in_widget(NemoTerminalWidget *self); -void nemo_terminal_widget_set_current_location(NemoTerminalWidget *self, GFile *location); -void nemo_terminal_widget_ensure_terminal_focus(NemoTerminalWidget *self); - -gboolean nemo_terminal_widget_initialize_in_paned(NemoTerminalWidget *self, - GtkWidget *unused_view_content, - GtkWidget *view_overlay); +void nemo_terminal_widget_set_current_location(NemoTerminalWidget *self, + GFile *location); +void nemo_terminal_widget_set_container_paned(NemoTerminalWidget *self, + GtkWidget *paned); void nemo_terminal_widget_toggle_visible(NemoTerminalWidget *self); -void nemo_terminal_widget_toggle_visible_with_save(NemoTerminalWidget *self, - gboolean is_manual_toggle); -gboolean nemo_terminal_widget_get_visible(NemoTerminalWidget *self); void nemo_terminal_widget_ensure_state(NemoTerminalWidget *self); +void nemo_terminal_widget_ensure_terminal_focus(NemoTerminalWidget *self); -void nemo_terminal_widget_apply_new_size(NemoTerminalWidget *self); -int nemo_terminal_widget_get_default_height(void); -void nemo_terminal_widget_save_height(NemoTerminalWidget *self, int height); +gboolean nemo_terminal_widget_get_visible(NemoTerminalWidget *self); -const gchar *nemo_terminal_widget_get_color_scheme(NemoTerminalWidget *self); -void nemo_terminal_widget_set_color_scheme(NemoTerminalWidget *self, const gchar *scheme); -void nemo_terminal_widget_apply_color_scheme(NemoTerminalWidget *self); +void nemo_terminal_widget_apply_new_size(NemoTerminalWidget *self); G_END_DECLS -#endif \ No newline at end of file +#endif /* __NEMO_TERMINAL_WIDGET_H__ */ diff --git a/src/nemo-window-menus.c b/src/nemo-window-menus.c index 936ab5b8b..9836fb5fc 100644 --- a/src/nemo-window-menus.c +++ b/src/nemo-window-menus.c @@ -1338,7 +1338,7 @@ action_toggle_terminal_callback (GtkAction *action, gpointer callback_data) window = NEMO_WINDOW (callback_data); slot = nemo_window_get_active_slot (window); - nemo_window_slot_toggle_terminal (slot, TRUE); + nemo_window_slot_toggle_terminal (slot); } static void diff --git a/src/nemo-window-slot.c b/src/nemo-window-slot.c index 3ff8bc731..5904d7bbd 100644 --- a/src/nemo-window-slot.c +++ b/src/nemo-window-slot.c @@ -46,8 +46,6 @@ #include -void nemo_window_slot_ensure_terminal_state(NemoWindowSlot *slot); - G_DEFINE_TYPE (NemoWindowSlot, nemo_window_slot, GTK_TYPE_BOX); enum { @@ -346,11 +344,10 @@ nemo_window_slot_init (NemoWindowSlot *slot) G_CALLBACK (floating_bar_action_cb), slot); slot->cache_bar = NULL; + slot->terminal_widget = NULL; + slot->terminal_visible = FALSE; slot->title = g_strdup (_("Loading...")); - - slot->terminal_visible = g_settings_get_boolean (nemo_window_state, "terminal-visible"); - } static void @@ -663,6 +660,9 @@ nemo_window_slot_set_content_view_widget (NemoWindowSlot *slot, /* If terminal-visible is enabled in config, ensure terminal is initialized and visible */ gboolean terminal_should_be_visible = g_settings_get_boolean(nemo_window_state, "terminal-visible"); if (terminal_should_be_visible) { + /* Defer terminal initialization to an idle callback. This ensures that the main + * window and its widgets have been allocated sizes before we attempt to create + * and size the terminal pane, preventing race conditions and sizing issues on startup. */ g_idle_add((GSourceFunc)nemo_window_slot_ensure_terminal_state, slot); } } @@ -974,86 +974,126 @@ nemo_window_slot_new (NemoWindowPane *pane) static void on_terminal_visibility_changed(NemoTerminalWidget *terminal, - gboolean visible, - NemoWindowSlot *slot) + gboolean visible, + NemoWindowSlot *slot) { - // Update slot visibility state slot->terminal_visible = visible; } static void on_terminal_directory_changed(NemoTerminalWidget *terminal, - GFile *location, - NemoWindowSlot *slot) + GFile *location, + NemoWindowSlot *slot) { - // Skip updating file manager location if the terminal is exiting SSH - if (terminal->is_exiting_ssh) { - return; - } - - // When terminal's directory changes, update the file browser location if (location != NULL) { nemo_window_slot_open_location(slot, location, 0); } } -/* nemo_window_slot_init_terminal: - * @slot: a #NemoWindowSlot +static void +on_paned_size_allocated (GtkWidget *paned, GtkAllocation *allocation, gpointer user_data) +{ + NemoTerminalWidget *terminal = NEMO_TERMINAL_WIDGET (user_data); + nemo_terminal_widget_apply_new_size (terminal); + /* Disconnect after the first call to avoid re-applying the size on every allocation. */ + g_signal_handlers_disconnect_by_func (paned, on_paned_size_allocated, terminal); +} + +static gboolean +on_paned_button_release (GtkWidget *paned, GdkEventButton *event, gpointer user_data) +{ + int position = gtk_paned_get_position (GTK_PANED (paned)); + int total_height = gtk_widget_get_allocated_height (paned); + + if (total_height > 0) + { + int height = total_height - position; + g_settings_set_int (nemo_window_state, "terminal-pane-size", height); + } + + return FALSE; +} + +/* + * _initialize_terminal_in_paned: + * @slot: The #NemoWindowSlot to add the terminal to. * - * Initializes the terminal pane for the window slot. + * This function performs a delicate re-parenting of widgets to insert the + * terminal. It takes the existing view_overlay, removes it from its parent, + * creates a new GtkPaned, and places the view_overlay in the top pane and + * the new terminal widget in the bottom pane. This new GtkPaned is then + * inserted back into the original parent of the view_overlay. */ +static void +_initialize_terminal_in_paned(NemoWindowSlot *slot) +{ + GtkWidget *paned_container; + GtkWidget *parent_of_overlay; + gint position; + GList *children; + + parent_of_overlay = gtk_widget_get_parent(slot->view_overlay); + if (!GTK_IS_BOX(parent_of_overlay)) { + g_warning("Cannot initialize terminal in paned: parent of view_overlay is not a GtkBox."); + return; + } + + children = gtk_container_get_children(GTK_CONTAINER(parent_of_overlay)); + position = g_list_index(children, slot->view_overlay); + g_list_free(children); + + paned_container = gtk_paned_new(GTK_ORIENTATION_VERTICAL); + + g_object_ref(slot->view_overlay); + gtk_container_remove(GTK_CONTAINER(parent_of_overlay), slot->view_overlay); + + gtk_paned_pack1(GTK_PANED(paned_container), slot->view_overlay, TRUE, TRUE); + g_object_unref(slot->view_overlay); + + gtk_paned_pack2(GTK_PANED(paned_container), GTK_WIDGET(slot->terminal_widget), FALSE, TRUE); + + gtk_box_pack_start(GTK_BOX(parent_of_overlay), paned_container, TRUE, TRUE, 0); + if (position != -1) { + gtk_box_reorder_child(GTK_BOX(parent_of_overlay), paned_container, position); + } + + g_signal_connect (paned_container, "size-allocate", G_CALLBACK (on_paned_size_allocated), slot->terminal_widget); + g_signal_connect (paned_container, "button-release-event", G_CALLBACK (on_paned_button_release), NULL); + + gtk_widget_show_all(paned_container); + + nemo_terminal_widget_set_container_paned(slot->terminal_widget, paned_container); +} + void nemo_window_slot_init_terminal (NemoWindowSlot *slot) { if (slot->terminal_widget != NULL) { return; } - - // Create the terminal widget with the current location - slot->terminal_widget = nemo_terminal_widget_new_with_location (slot->location); - - // Connect signals - g_signal_connect (slot->terminal_widget, "toggle-visibility", - G_CALLBACK (on_terminal_visibility_changed), slot); - g_signal_connect (slot->terminal_widget, "change-directory", - G_CALLBACK (on_terminal_directory_changed), slot); - - nemo_terminal_widget_initialize_in_paned( - slot->terminal_widget, - GTK_WIDGET(slot->content_view), - slot->view_overlay); + + slot->terminal_widget = nemo_terminal_widget_new_with_location(slot->location); + + g_signal_connect(slot->terminal_widget, "toggle-visibility", + G_CALLBACK(on_terminal_visibility_changed), slot); + g_signal_connect(slot->terminal_widget, "change-directory", + G_CALLBACK(on_terminal_directory_changed), slot); + + _initialize_terminal_in_paned(slot); } -/* nemo_window_slot_toggle_terminal: - * @slot: a #NemoWindowSlot - * @is_manual_toggle: whether this is a user-initiated toggle (TRUE) or an automatic one (FALSE) - * - * Toggles the visibility of the terminal pane for the window slot. - */ void -nemo_window_slot_toggle_terminal (NemoWindowSlot *slot, gboolean is_manual_toggle) +nemo_window_slot_toggle_terminal (NemoWindowSlot *slot) { if (slot->terminal_widget == NULL) { nemo_window_slot_init_terminal(slot); } - - // Delegate toggle to the terminal widget - if (slot->terminal_widget != NULL) { - nemo_terminal_widget_toggle_visible_with_save(slot->terminal_widget, is_manual_toggle); - slot->terminal_visible = nemo_terminal_widget_get_visible(slot->terminal_widget); - // If terminal is now visible, ensure it's at the same location as file manager - if (slot->terminal_visible && slot->location != NULL) { - nemo_terminal_widget_set_current_location(slot->terminal_widget, slot->location); - } + if (slot->terminal_widget != NULL) { + nemo_terminal_widget_toggle_visible(slot->terminal_widget); } } -/* nemo_window_slot_update_terminal_location: - * @slot: a #NemoWindowSlot - * - * Updates the terminal's working directory to match the current location - */ void nemo_window_slot_update_terminal_location (NemoWindowSlot *slot) { @@ -1062,22 +1102,22 @@ nemo_window_slot_update_terminal_location (NemoWindowSlot *slot) } } -/* nemo_window_slot_ensure_terminal_state: - * @slot: a #NemoWindowSlot - * - * Ensures the terminal is properly positioned if it's already visible - * This function is called after the content view is initialized - */ -void -nemo_window_slot_ensure_terminal_state (NemoWindowSlot *slot) +gboolean +nemo_window_slot_ensure_terminal_state (gpointer user_data) { - gboolean terminal_visible = g_settings_get_boolean (nemo_window_state, "terminal-visible"); + NemoWindowSlot *slot = user_data; + gboolean terminal_should_be_visible = g_settings_get_boolean(nemo_window_state, "terminal-visible"); - if (terminal_visible && slot->terminal_widget == NULL) { - nemo_window_slot_init_terminal(slot); - } - else if (slot->terminal_widget != NULL) { - // Let the terminal widget handle state consistency + if (terminal_should_be_visible) { + if (slot->terminal_widget == NULL) { + nemo_window_slot_init_terminal(slot); + } nemo_terminal_widget_ensure_state(slot->terminal_widget); + nemo_window_slot_update_terminal_location(slot); + } else { + if (slot->terminal_widget != NULL) { + nemo_terminal_widget_ensure_state(slot->terminal_widget); + } } -} + return G_SOURCE_REMOVE; +} \ No newline at end of file diff --git a/src/nemo-window-slot.h b/src/nemo-window-slot.h index 9c9a32dcd..e875bd125 100644 --- a/src/nemo-window-slot.h +++ b/src/nemo-window-slot.h @@ -73,11 +73,8 @@ struct NemoWindowSlot { GtkWidget *no_search_results_box; /* Terminal pane */ - GtkWidget *terminal_pane; NemoTerminalWidget *terminal_widget; - GtkWidget *terminal_vpaned; gboolean terminal_visible; - int terminal_height; guint set_status_timeout_id; guint loading_timeout_id; @@ -200,7 +197,8 @@ void nemo_window_slot_check_bad_cache_bar (NemoWindowSlot *slot); void nemo_window_slot_set_show_thumbnails (NemoWindowSlot *slot, gboolean show_thumbnails); -void nemo_window_slot_toggle_terminal (NemoWindowSlot *slot, gboolean is_manual_toggle); +void nemo_window_slot_toggle_terminal (NemoWindowSlot *slot); void nemo_window_slot_update_terminal_location (NemoWindowSlot *slot); +gboolean nemo_window_slot_ensure_terminal_state (gpointer user_data); -#endif /* NEMO_WINDOW_SLOT_H */ +#endif /* NEMO_WINDOW_SLOT_H */ \ No newline at end of file From 76cc9a61934a002c179259897477aecb7606ff85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Gon=C3=A7alves?= Date: Wed, 16 Jul 2025 12:17:43 -0300 Subject: [PATCH 6/7] Better code to implement embedded terminal --- libnemo-private/org.nemo.gschema.xml.txt | 934 ----------------------- 1 file changed, 934 deletions(-) delete mode 100644 libnemo-private/org.nemo.gschema.xml.txt diff --git a/libnemo-private/org.nemo.gschema.xml.txt b/libnemo-private/org.nemo.gschema.xml.txt deleted file mode 100644 index d909407c7..000000000 --- a/libnemo-private/org.nemo.gschema.xml.txt +++ /dev/null @@ -1,934 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'after-current-tab' - Where to position newly open tabs in browser windows. - If set to "after-current-tab", then new tabs are inserted after the current tab. If set to "end", then new tabs are appended to the end of the tab list. - - - true - Enables the classic Nemo behavior, where all windows are browsers - If set to true, then all Nemo windows will be browser windows. This is how Nemo used to behave before version 2.6, and some people prefer this behavior. - - - false - Enables renaming of icons by two times clicking with pause between clicks - If set to true, then icons in all Nemo windows will be able to get renamed quickly. Users should click two times on icons with a pause time more than double-click time of their system. - - - true - During drag-and-drop operations, automatically expand rows when hovering them briefly - - - false - Show the location entry by default - If set to true, then Nemo browser windows will show a textual input entry for the location toolbar. - - - true - Show Previous button in nemo toolbar - If set to true, then Nemo browser windows will show the button. - - - true - Show Next button in nemo toolbar - If set to true, then Nemo browser windows will show the button. - - - true - Show Up button in nemo toolbar - If set to true, then Nemo browser windows will show the button. - - - false - Show refresh button in nemo toolbar - If set to true, then Nemo browser windows will show the button. - - - true - Show toggle button location entry/pathbar - If set to true, then Nemo browser windows will show the button. - - - false - Show Home button in nemo toolbar - If set to true, then Nemo browser windows will show the button. - - - false - Show Computer button in nemo toolbar - If set to true, then Nemo browser windows will show the button. - - - true - Show Search button in nemo toolbar - If set to true, then Nemo browser windows will show the button. - - - false - Show new folder button in nemo toolbar - If set to true, then Nemo browser windows will show the button. - - - false - Show open in terminal in the nemo toolbar - If set to true, then Nemo browser windows will show the button. - - - true - Show Icon View button in nemo toolbar - If set to true, then Nemo browser windows will show the button. - - - true - Show List View button in nemo toolbar - If set to true, then Nemo browser windows will show the button. - - - true - Show Compact View button in nemo toolbar - If set to true, then Nemo browser windows will show the button. - - - false - Show Thumbnails button in nemo toolbar - If set to true, then Nemo browser windows will show the button. - - - true - Show warning when opening as root - If set to true, then Nemo show warning message when run Nemo as root user. - - - false - Whether to ask for confirmation when moving files to Trash - If set to true, then Nemo will ask for confirmation when you attempt to move files to the Trash. - - - true - Whether to ask for confirmation when deleting files, or emptying Trash - If set to true, then Nemo will ask for confirmation when you attempt to delete files, or empty the Trash. - - - true - Whether to enable immediate deletion - If set to true, then Nemo will have a feature allowing you to delete a file immediately and in-place, instead of moving it to the trash. This feature can be dangerous, so use caution. - - - false - Whether to swap the hotkeys for Trash and Delete - If set to true, the Delete key will permanently delete a file, and the Shift-Delete key will only trash a file. - - - - 'local-only' - When to show number of items in a folder - Speed tradeoff for when to show the number of items in a folder. If set to "always" then always show item counts, even if the folder is on a remote server. If set to "local-only" then only show counts for local file systems. If set to "never" then never bother to compute item counts. - - - 'double' - Type of click used to launch/open files - Possible values are "single" to launch files on a single click, or "double" to launch them on a double click. - - - 'ask' - What to do with executable text files when activated - What to do with executable text files when they are activated (single or double clicked). Possible values are "launch" to launch them as programs, "ask" to ask what to do via a dialog, and "display" to display them as text files. - - - ['xviewer','feh','sxiv'] - Image viewer executables to pass sort order to - When opening a single image with an application in this list, allow that viewer to display other images in the current nemo view, in the presented order (including searches). Other image viewers can be added, but are likely to ignore the order. The viewer Exec line must accept a list of files (either %U or %F). - - - true - Use extra mouse button events in Nemo' browser window - For users with mice that have "Forward" and "Back" buttons, this key will determine if any action is taken inside of Nemo when either is pressed. - - - 9 - Mouse button to activate the "Forward" command in browser window - For users with mice that have buttons for "Forward" and "Back", this key will set which button activates the "Forward" command in a browser window. Possible values range between 6 and 14. - - - 8 - Mouse button to activate the "Back" command in browser window - For users with mice that have buttons for "Forward" and "Back", this key will set which button activates the "Back" command in a browser window. Possible values range between 6 and 14. - - - - 'local-only' - When to show thumbnails of image files - Speed tradeoff for when to show an image file as a thumbnail. If set to "always" then always thumbnail, even if the folder is on a remote server. If set to "local-only" then only show thumbnails for local file systems. If set to "never" then never bother to thumbnail images, just use a generic icon. - - - false - Inherit thumbnail visibility from parent - If set, folders will inherit their thumbnail visibility from their parents - - - 1048576 - Maximum image size for thumbnailing - Images over this size (in bytes) won't be thumbnailed. The purpose of this setting is to avoid thumbnailing large images that may take a long time to load or use lots of memory. - - - false - Show advanced permissions in the file property dialog - If set to true, then Nemo lets you edit and display file permissions in a more unix-like way, accessing some more esoteric options. - - - true - Show folders first in windows - If set to true, then Nemo shows folders prior to showing files in the icon and list views. - - - true - Show favorites first in windows - If set to true, then Nemo shows favorites prior to other files in the icon and list views. - - - - - - 'name' - Default sort order - The default sort-order for items in the icon view. Possible values are "name", "size", "type" and "mtime". - - - false - Reverse sort order in new windows - If true, files in new windows will be sorted in reverse order. ie, if sorted by name, then instead of sorting the files from "a" to "z", they will be sorted from "z" to "a"; if sorted by size, instead of being incrementally they will be sorted decrementally. - - - false - Nemo uses the users home folder as the desktop - If set to true, then Nemo will use the user's home folder as the desktop. If it is false, then it will use ~/Desktop as the desktop. - - - - - - - - 'icon-view' - Default folder viewer - When a folder is visited this viewer is used unless you have selected another view for that particular folder. Possible values are "list-view", "icon-view" and "compact-view". - - - false - Inherit the view type (icon, compact, list) from parent to children - When a folder is visited the viewer is inherited from that folder's parent unless you have selected another view for that particular folder. - - - 'locale' - Date Format - The format of file dates. Possible values are "locale", "iso", and "informal". - - - 'auto-mono' - The font to use for the date/time columns. - The format of file dates. Possible values are "auto-mono" (best effort to match the application font), "system-mono" (use the current system mono font), and "no-mono" (use a normal font). - - - false - Whether to show hidden files - If set to true, then hidden files are shown by default in the file manager. Hidden files are either dotfiles, listed in the folder's .hidden file or backup files ending with a tilde (~). - - - false - Whether to show the full path of the current view in the title bar and tab bars - If set to true, will show the normal title of a window or tab, followed by the full path to that location in parentheses. - - - [] - Bulk rename utility - If set, Nemo will append URIs of selected files and treat the result as a command line for bulk renaming. Bulk rename applications can register themselves in this key by setting the key to a space-separated string of their executable name and any command line options. If the executable name is not set to a full path, it will be searched for in the search path. - - - 'base-10' - Prefixes used for file sizes - Determines whether Nemo uses base-10, base-10 long, base-2 or base-2 long file size prefixes - - - false - Whether to close a view of a removeable device instead of navigating Home - If set to true, a view open for a removeable device will be closed instead of sent Home if the device is ejected - - - false - Whether to default to showing dual-pane view when a new window is opened - If set to true, new Nemo windows will default to showing two panes - - - false - Whether to ignore folder metadata for view zoom levels and layouts - If set to true, views will not change according to their metadata, but stay consistent for the life of that window - - - true - Whether to list bookmarks in the Move To/Copy To menus - If set to true, bookmarks will be listed in the MoveTo/CopyTo menus - - - true - Whether to list places in the Move To/Copy To menus - If set to true, places will be listed in the MoveTo/CopyTo menus - - - false - deprecated - no longer used - - - false - Show tooltips for desktop items - If true, tooltips will be displayed for desktop items. - - - false - Show tooltips when hovering on items in an icon or compact view - If true, tooltips will be displayed for icon and compact view items - - - false - Show tooltips when hovering on items in a list view - If true, tooltips will be displayed for list view items - - - false - Show detailed file type in tooltip - If true, tooltips will show a detailed file type. - - - false - Show file modified date in tooltip - If true, tooltips will show their modified date. - - - false - Show file accessed date in tooltip - If true, tooltips will show their accessed date. - - - false - Show file creation (birth) date in tooltip - If true, tooltips will show their creation date. - - - false - Show full path in tooltip - If true, tooltips will show the file's full path. - - - false - Don't show the explainer message when turning off the main menu - If true, you will no longer recieve a popup explaining how to reactivate the main menu once you've hidden it - - - 2 - Last server connect method used - - - false - If true, all file operations will start immediately - - - false - If true, double click left on blank area will go to parent folder - - - false - Display the 'Make executable and run' button in the mime-action dialog (open an unknown filetype) - - - 150 - Maximum number of files to preload deferred attributes for when opening a directory - Certain file attributes (like thumbnail and extension info) are deferred until a folder finishes loading. This number specifies how many files to skip this behavior on so that smaller folders won't have an obvious delay when loading these attributes. - - - false - Suppress any safeguards when running nemo/nemo-desktop as the root user. For some systems there is only a root user. - - - true - If true, enable detection of the type of content of a mounted media and display a suggested application to open the media. - - - -1 - Number of threads to dedicate to thumbnailing. -1 to let the program decide. The maximum allowed threads is half the number of logical processors, regardless of what is set here. If you change this setting you must restart Nemo for it to take effect. - - - - - - [ 'none', 'size', 'date_modified' ] - List of possible captions on icons - A list of captions below an icon in the icon view and - the desktop. The actual number of captions shown depends on - the zoom level. Some possible values are: - "size", "type", "date_modified", "date_changed", "date_accessed", "owner", - "group", "permissions", "octal_permissions" and "mime_type". - - - false - deprecated - not used - - - false - Put labels beside icons - If true, labels will be placed beside icons rather than underneath them. - - - 'standard' - Default icon zoom level - Default zoom level used by the icon view. - - - 64 - Default Thumbnail Icon Size - The default size of an icon for a thumbnail in the icon view. - - - [ '3' ] - Text Ellipsis Limit - A string specifying how parts of overlong file names - should be replaced by ellipses, depending on the zoom - level. - Each of the list entries is of the form "Zoom Level:Integer". - For each specified zoom level, if the given integer is - larger than 0, the file name will not exceed the given number of lines. - If the integer is 0 or smaller, no limit is imposed on the specified zoom level. - A default entry of the form "Integer" without any specified zoom level - is also allowed. It defines the maximum number of lines for all other zoom levels. - Examples: - 0 - always display overlong file names; - 3 - shorten file names if they exceed three lines; - smallest:5,smaller:4,0 - shorten file names if they exceed five lines - for zoom level "smallest". Shorten file names if they exceed four lines - for zoom level "smaller". Do not shorten file names for other zoom levels. - - Available zoom levels: - smallest (33%), smaller (50%), small (66%), standard (100%), large (150%), - larger (200%), largest (400%) - - - - - - 'standard' - Default compact view zoom level - Default zoom level used by the compact view. - - - false - All columns have same width - If this preference is set, all columns in the compact view have the same width. Otherwise, the width of each column is determined seperately. - - - - - - 'smaller' - Default list zoom level - Default zoom level used by the list view. - - - [ 'name', 'size', 'type', 'date_modified' ] - Default list of columns visible in the list view - Default list of columns visible in the list view. - - - [ 'name', 'size', 'type', 'date_modified' ] - Default column order in the list view - Default column order in the list view. - - - false - If true, allow folders with content to be expanded in the current view. - - - - - - - - - - true - Only show folders in the tree side pane - If set to true, Nemo will only show folders in the tree side pane. Otherwise it will show both folders and files. - - - - - - 'Noto Sans 10' - Desktop font - The font description used for the icons on the desktop. - - - "true::false" - Desktop layout - Format bool:bool, show desktop folder on primary monitor:show desktop on remaining monitors - - - true - Whether to show icons from inactive monitors on another monitor - - - true - Deprecated: Allow Nemo to manage the desktop - Deprecated: If this is set to true, Nemo will autostart and manage the desktop - - - true - Which desktop view type to use - If true, the new desktop grid view will be used by nemo-desktop, otherwise the legacy view will be used. - - - 1.0 - Vertical desktop grid adjustment - Overrides the standard vertical spacing for the desktop grid, in situation where default spacing is not ideal due to label customizations. This is a value from 0.5 to 1.5, with 1.0 being no adjustment, and 0.5 being half the default spacing. - - - 1.0 - Horizontal desktop grid adjustment - Overrides the standard horizontal spacing for the desktop grid, in situation where default spacing is not ideal due to label customizations. This is a value from 0.5 to 1.5, with 1.0 being no adjustment, and 0.5 being half the default spacing. - - - false - Home icon visible on desktop - If this is set to true, an icon linking to the home folder will be put on the desktop. - - - false - Computer icon visible on desktop - If this is set to true, an icon linking to the computer location will be put on the desktop. - - - false - Trash icon visible on desktop - If this is set to true, an icon linking to the trash will be put on the desktop. - - - false - Show mounted volumes on the desktop - If this is set to true, icons linking to mounted volumes will be put on the desktop. - - - false - Network Servers icon visible on the desktop - If this is set to true, an icon linking to the Network Servers view will be put on the desktop. - - - 2 - Text Ellipsis Limit - An integer specifying how parts of overlong file names should be replaced by ellipses on the desktop. If the number is larger than 0, the file name will not exceed the given number of lines. If the number is 0 or smaller, no limit is imposed on the number of displayed lines. - - - ['conky', 'csd-background'] - List of desktop-handling to ignore when determining whether or not to manager the desktop. - Nemo checks for _NET_WM_WINDOW_TYPE_DESKTOP-type windows, and skips managing the desktop if any others are detected. Add potential names to this list to ignore when performing this check. This means that you want nemo to create its own desktop window(s) even though something else already seems to. The check is based on a program's WM_CLASS. - - - true - Fade the background on change - If set to true, then Nemo will use a fade effect to change the desktop background. - - - - - - '' - The geometry string for a navigation window. - A string containing the saved geometry and coordinates string for navigation windows. - - - false - Whether the navigation window should be maximized. - Whether the navigation window should be maximized by default. - - - 'both' - Local terminal folder synchronization mode - Determines how the embedded terminal synchronizes its current directory with the file manager when navigating local folders. 'none': No synchronization. 'fm-to-term': File manager navigation changes terminal directory. 'term-to-fm': Terminal navigation changes file manager location. 'both': Synchronization works in both directions. - - - 'off' - SSH terminal auto-connection and synchronization mode preference - Determines behavior when an SFTP location is active. 'off': Do not connect automatically. 'sync-both': Connect and sync both ways. 'sync-fm-to-term': Connect and sync File Manager to Terminal. 'sync-term-to-fm': Connect and sync Terminal to File Manager. 'sync-none': Connect without folder synchronization. - - - 300 - Terminal panel height - Height of the terminal panel in pixels. - - - false - Terminal pane visibility - Whether the terminal pane should be visible. - - - 'system' - Terminal color scheme - The color scheme to use for the embedded terminal. Available values: 'system', 'dark', 'light', 'solarized-dark', 'solarized-light', 'matrix', 'one-half-dark', 'one-half-light', 'monokai', 'custom'. - - - 12 - Terminal font size - The font size to use for the embedded terminal in point units. - - - 170 - Width of the side pane - The default width of the side pane in new windows. - - - -1 - Index of the bookmark list to jump to the dedicated sidebar bookmark section - This is an internal setting for the sidebar that tracks the index in the bookmark list that separates bookmarks in the Computer section from bookmarks in the Bookmark section. - - - true - Show toolbar in new windows - If set to true, newly opened windows will have toolbars visible. - - - true - Show location bar in new windows - If set to true, newly opened windows will have the location bar visible. - - - true - Show status bar in new windows - If set to true, newly opened windows will have the status bar visible. - - - true - Show side pane in new windows - If set to true, newly opened windows will have the side pane visible. - - - true - Show menu bar in new windows - If set to true, newly opened windows will have the menu bar visible. - - - true - Expand My Computer section in places sidebar - View state storage for My Computer in places sidebar - - - true - Expand Bookmark section in places sidebar - View state storage for Bookmarks in places sidebar - - - true - Expand Devices section in places sidebar - View state storage for My Computer in places sidebar - - - true - Expand Network section in places sidebar - View state storage for My Computer in places sidebar - - - - - - - - - - - 'places' - Side pane view - The side pane view to show in newly opened windows. - - - - - - [] - List of extensions -not- to load. - List of extension names you do -not- want to load. This maintains the behavior of an installed extension being enabled by default. - - - [] - List of NemoActions -not- to load. - List of action files you do -not- want loaded. This maintains the behavior of an installed action being enabled by default. - - - [] - List of scripts -not- to load. - List of script files you do -not- want loaded. This maintains the behavior of an installed script being enabled by default. - - - - - - - true - Show the selection context menu's Open item. - - - true - Show the selection context menu's Open in New Tab item. - - - true - Show the selection context menu's Open in New Window item. - - - true - Show the selection context menu's Scripts submenu. - - - true - Show the selection context menu's Cut item. - - - true - Show the selection context menu's Copy item. - - - true - Show the selection context menu's Paste item. - - - false - Show the selection context menu's Duplicate item. - - - true - Show the selection context menu's Pin/Unpin item. - - - true - Show the selection context menu's Favorite/Unfavorite item. - - - false - Show the selection context menu's Create Link item. - - - true - Show the selection context menu's Rename item. - - - false - Show the selection context menu's Copy To submenu. - - - false - Show the selection context menu's Move To submenu. - - - true - Show the selection context menu's Open in Terminal item. - - - true - Show the selection context menu's Open As Root item. - - - true - Show the selection context menu's Move to Trash item. - - - true - Show the selection context menu's Properties item. - - - - - true - Show the background context menu's Create New Folder item. - - - true - Show the background context menu's Scripts submenu. - - - true - Show the background context menu's Open in Terminal item. - - - true - Show the background context menu's Open as Root item. - - - true - Show the background context menu's Show Hidden Files item. - - - true - Show the background context menu's Paste item. - - - true - Show the background context menu's Properties item. - - - - - true - Show the background context menu's Arrange Items submenu (icon view only). - - - true - Show the background context menu's Organize by Name item (icon view only). - - - - - true - Show the background context menu's Customize item (new-style desktop only). - - - - - - false - Stores the most recent state of the file search regex toggle - - - false - Stores the most recent state of the content search regex toggle - - - 'pcre' - valid formats: pcre, javascript - - - false - Treat patterns as raw bytes, not utf-8 - - - false - Stores the most recent state of the file search case toggle - - - false - Stores the most recent state of the content search case toggle - - - ['/dev', '/proc', '/sys', 'dosdevices', '.git'] - Paths or folder names to never recurse into when searching - List of locations that the search engine will never enter when looking for matches. These can be absolute or simply folder names (like .git). You can still enter those folders and search inside of them, however. - - - true - Recurse into subfolders when performing a search - - - [] - Saved list of columns visible in the search view. - - - '' - Column to sort on when viewing search results - - - false - Reverse the direction of the sort when viewing search results - - - From ff6b44f87b88d20cd5150e1a28d158f9cc3da59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Gon=C3=A7alves?= Date: Thu, 24 Jul 2025 02:51:19 -0300 Subject: [PATCH 7/7] Fixes to work fine on bash, zsh and fish --- src/nemo-terminal-widget.c | 341 +++++++++++++++++++++++-------------- 1 file changed, 214 insertions(+), 127 deletions(-) diff --git a/src/nemo-terminal-widget.c b/src/nemo-terminal-widget.c index 418321ccf..316b1b551 100644 --- a/src/nemo-terminal-widget.c +++ b/src/nemo-terminal-widget.c @@ -286,6 +286,7 @@ static void _clear_ssh_connection_data(NemoTerminalWidgetPrivate *priv); static void _reset_to_local_state(NemoTerminalWidget *self); static const gchar * nemo_terminal_widget_get_color_scheme(NemoTerminalWidget *self); static void nemo_terminal_widget_set_color_scheme(NemoTerminalWidget *self, const gchar *scheme); +static void _sync_terminal_to_fm (NemoTerminalWidget *self, const gchar *cwd_uri); static GParamSpec *properties[N_PROPS]; static guint signals[LAST_SIGNAL]; @@ -424,7 +425,7 @@ nemo_terminal_widget_init(NemoTerminalWidget *self) gtk_label_set_xalign(GTK_LABEL(priv->ssh_indicator), 0.5); provider = gtk_css_provider_new(); - gtk_css_provider_load_from_data(provider, "label#ssh-indicator { background-color: #3465a4; color: white; padding: 2px 5px; margin: 0; font-weight: bold; }", -1, NULL); + gtk_css_provider_load_from_data(provider, "label#ssh-indicator { background-color: @theme_selected_bg_color; color: @theme_selected_fg_color; padding: 2px 5px; margin: 0; font-weight: bold; }", -1, NULL); context = gtk_widget_get_style_context(priv->ssh_indicator); gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_USER); @@ -458,6 +459,11 @@ spawn_async_callback(VteTerminal *terminal, GPid pid, GError *error, gpointer us NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); NemoTerminalWidgetPrivate *priv = self->priv; + if (gtk_widget_in_destruction(GTK_WIDGET(self))) + { + return; + } + if (pid == -1) { g_warning("Failed to spawn terminal: %s", error ? error->message : "Unknown error"); @@ -475,12 +481,6 @@ spawn_async_callback(VteTerminal *terminal, GPid pid, GError *error, gpointer us g_clear_pointer(&priv->pending_ssh_username, g_free); g_clear_pointer(&priv->pending_ssh_port, g_free); } - else if (priv->current_location) - { - /* If a local shell just spawned, sync its directory to the current location. - * This is crucial for new tabs/windows where the terminal starts visible. */ - change_directory_in_terminal(self, priv->current_location); - } } } @@ -490,13 +490,8 @@ spawn_terminal_async(NemoTerminalWidget *self) NemoTerminalWidgetPrivate *priv = self->priv; g_autofree gchar *working_directory = NULL; const gchar *shell_executable; - gchar *argv[2]; - gchar *envp[] = { - "TERM=xterm-256color", - "LC_ALL=C.UTF-8", - "COLORTERM=truecolor", - NULL - }; + gchar **argv = NULL; + gchar **envp = NULL; g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); @@ -513,16 +508,57 @@ spawn_terminal_async(NemoTerminalWidget *self) if (!shell_executable || *shell_executable == '\0') shell_executable = "/bin/sh"; - argv[0] = (gchar *)shell_executable; - argv[1] = NULL; - if (priv->pending_ssh_hostname != NULL) { working_directory = NULL; } - else if (priv->current_location && g_file_query_exists(priv->current_location, NULL)) + else if (priv->current_location) + { + g_autoptr(GFile) dir_location = NULL; + g_autoptr(GFileInfo) info = g_file_query_info(priv->current_location, G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL); + if (info && g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY) { + dir_location = g_object_ref(priv->current_location); + } else { + dir_location = g_file_get_parent(priv->current_location); + } + if (dir_location) { + working_directory = g_file_get_path(dir_location); + } + } + + if (g_str_has_suffix(shell_executable, "zsh")) + { + g_autofree gchar *config_dir = g_build_filename(g_get_user_config_dir(), "nemo", NULL); + g_autofree gchar *zshrc_path = g_build_filename(config_dir, ".zshrc", NULL); + g_autofree gchar *zshrc_content = NULL; + g_autofree gchar *zdotdir_env = NULL; + + g_mkdir_with_parents(config_dir, 0700); + + zshrc_content = g_strdup_printf("_nemo_vte_update_cwd() { echo -en \"\\033]7;file://$PWD\\007\"; };\n" + "typeset -a precmd_functions;\n" + "if [[ -z \"$precmd_functions[(r)_nemo_vte_update_cwd]\" ]]; then\n" + " precmd_functions+=(_nemo_vte_update_cwd);\n" + "fi;\n" + "[ -f \"$HOME/.zshrc\" ] && . \"$HOME/.zshrc\";\n"); + + g_file_set_contents(zshrc_path, zshrc_content, -1, NULL); + + zdotdir_env = g_strdup_printf("ZDOTDIR=%s", config_dir); + gchar *zsh_envp[] = { "TERM=xterm-256color", "COLORTERM=truecolor", zdotdir_env, NULL }; + envp = g_strdupv(zsh_envp); + argv = g_strsplit(shell_executable, " ", -1); + } + else /* Assume bash or other compatible shells */ { - working_directory = g_file_get_path(priv->current_location); + argv = g_strsplit(shell_executable, " ", -1); + gchar *bash_envp[] = { + "TERM=xterm-256color", + "COLORTERM=truecolor", + "PROMPT_COMMAND=echo -en \"\\033]7;file://$PWD\\007\"", + NULL + }; + envp = g_strdupv(bash_envp); } vte_terminal_spawn_async(priv->terminal, @@ -536,6 +572,9 @@ spawn_terminal_async(NemoTerminalWidget *self) priv->spawn_cancellable, (VteTerminalSpawnAsyncCallback)spawn_async_callback, self); + + g_strfreev(argv); + g_strfreev(envp); } static void @@ -580,14 +619,18 @@ on_terminal_child_exited(VteTerminal *terminal, gint status, gpointer user_data) if (priv->state == NEMO_TERMINAL_STATE_IN_SSH) { _reset_to_local_state(self); + if (priv->is_visible) + { + spawn_terminal_async(self); + } } - - if (gtk_widget_get_ancestor(GTK_WIDGET(self), GTK_TYPE_WINDOW)) + else if (priv->state == NEMO_TERMINAL_STATE_LOCAL) { + priv->needs_respawn = TRUE; if (priv->is_visible) - spawn_terminal_async(self); - else - priv->needs_respawn = TRUE; + { + nemo_terminal_widget_toggle_visible(self); + } } } @@ -618,15 +661,16 @@ on_terminal_preference_changed(GSettings *settings, const gchar *key, gpointer u } static void -on_terminal_directory_changed(VteTerminal *terminal, gpointer user_data) +_sync_terminal_to_fm (NemoTerminalWidget *self, const gchar *cwd_uri) { - NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); NemoTerminalWidgetPrivate *priv = self->priv; g_autoptr(GFile) new_gfile_location = NULL; gboolean should_sync_to_fm = FALSE; - const gchar *cwd_uri = vte_terminal_get_current_directory_uri(terminal); - if (!cwd_uri) return; + if (!cwd_uri) + { + return; + } if (priv->ignore_next_terminal_cd_signal) { @@ -673,17 +717,13 @@ on_terminal_directory_changed(VteTerminal *terminal, gpointer user_data) } } -/* - * feed_cd_command: - * @terminal: The VteTerminal to send the command to. - * @path: The directory path to change to. - * - * This function programmatically sends a 'cd' command to the terminal's - * child process. It uses a sequence of shell control characters (CTRL+A, - * CTRL+K, etc.) to insert the command at the beginning of the line, - * execute it, and then restore any text the user might have been typing. - * This provides a less disruptive user experience. - */ +static void +on_terminal_directory_changed(VteTerminal *terminal, gpointer user_data) +{ + const gchar *cwd_uri_str = vte_terminal_get_current_directory_uri(terminal); + _sync_terminal_to_fm(NEMO_TERMINAL_WIDGET(user_data), cwd_uri_str); +} + static void feed_cd_command(VteTerminal *terminal, const char *path) { @@ -804,21 +844,6 @@ _build_font_size_submenu(NemoTerminalWidget *self) return submenu; } -/* - * _build_enum_pref_submenu: - * @self: The #NemoTerminalWidget instance. - * @entries: A static array of menu entry data. - * @count: The number of entries in the array. - * @current_value: The current value of the preference to check against. - * @settings_key: The GSettings key name for this preference. - * - * A helper function to reduce code duplication when building radio-button - * submenus for enum-based preferences. It iterates over the provided - * entries, creates a radio menu item for each, and connects it to the - * generic `on_enum_pref_changed` callback. - * - * Returns: (transfer full): A new GtkMenu widget containing the radio items. - */ static GtkWidget * _build_enum_pref_submenu(NemoTerminalWidget *self, const MenuSyncModeEntry *entries, gsize count, gint current_value, const gchar *settings_key) { @@ -849,7 +874,6 @@ static GtkWidget * _build_sftp_auto_connect_submenu(NemoTerminalWidget *self) { NemoTerminalWidgetPrivate *priv = self->priv; - /* We can reuse the helper here, but need to cast the array type as the structs are compatible. */ return _build_enum_pref_submenu(self, (const MenuSyncModeEntry*)SFTP_AUTO_CONNECT_ENTRIES, G_N_ELEMENTS(SFTP_AUTO_CONNECT_ENTRIES), priv->ssh_auto_connect_mode, "ssh-terminal-auto-connect-mode"); } @@ -871,10 +895,32 @@ _build_manual_ssh_connect_submenu(NemoTerminalWidget *self, const gchar *hostnam } static void -_append_menu_item(GtkMenuShell *menu, const gchar *label, GCallback callback, gpointer user_data) +on_copy_activate(GtkMenuItem *menuitem, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + vte_terminal_copy_clipboard_format(self->priv->terminal, VTE_FORMAT_TEXT); +} + +static void +on_paste_activate(GtkMenuItem *menuitem, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + vte_terminal_paste_clipboard(self->priv->terminal); +} + +static void +on_select_all_activate(GtkMenuItem *menuitem, gpointer user_data) +{ + NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data); + vte_terminal_select_all(self->priv->terminal); +} + +static void +_append_menu_item(GtkMenuShell *menu, const gchar *label, GCallback callback, gpointer user_data, gboolean sensitive) { GtkWidget *item = gtk_menu_item_new_with_label(label); g_signal_connect(item, "activate", callback, user_data); + gtk_widget_set_sensitive(item, sensitive); gtk_menu_shell_append(menu, item); } @@ -893,10 +939,10 @@ create_terminal_popup_menu(NemoTerminalWidget *self) GtkWidget *menu = gtk_menu_new(); gboolean is_sftp_location = FALSE; - _append_menu_item(GTK_MENU_SHELL(menu), _("Copy"), G_CALLBACK(vte_terminal_copy_clipboard_format), priv->terminal); - _append_menu_item(GTK_MENU_SHELL(menu), _("Paste"), G_CALLBACK(vte_terminal_paste_clipboard), priv->terminal); + _append_menu_item(GTK_MENU_SHELL(menu), _("Copy"), G_CALLBACK(on_copy_activate), self, vte_terminal_get_has_selection(priv->terminal)); + _append_menu_item(GTK_MENU_SHELL(menu), _("Paste"), G_CALLBACK(on_paste_activate), self, TRUE); gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - _append_menu_item(GTK_MENU_SHELL(menu), _("Select All"), G_CALLBACK(vte_terminal_select_all), priv->terminal); + _append_menu_item(GTK_MENU_SHELL(menu), _("Select All"), G_CALLBACK(on_select_all_activate), self, TRUE); gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); _append_menu_item_with_submenu(GTK_MENU_SHELL(menu), _("Color Scheme"), _build_color_scheme_submenu(self)); @@ -913,7 +959,7 @@ create_terminal_popup_menu(NemoTerminalWidget *self) if (priv->state == NEMO_TERMINAL_STATE_IN_SSH) { - _append_menu_item(GTK_MENU_SHELL(menu), _("Disconnect from SSH"), G_CALLBACK(on_ssh_exit_activate), self); + _append_menu_item(GTK_MENU_SHELL(menu), _("Disconnect from SSH"), G_CALLBACK(on_ssh_exit_activate), self, TRUE); } else { @@ -964,30 +1010,42 @@ get_remote_path_from_sftp_gfile(GFile *location) } static void -change_directory_in_terminal(NemoTerminalWidget *self, GFile *location) +change_directory_in_terminal (NemoTerminalWidget *self, GFile *location) { NemoTerminalWidgetPrivate *priv = self->priv; g_autofree gchar *target_path = NULL; gboolean should_sync = FALSE; + g_autoptr(GFile) dir_location = NULL; + + if (!priv->is_visible || priv->child_pid == -1 || !location) + return; - if (!priv->is_visible || priv->child_pid == -1) + g_autoptr(GFileInfo) info = g_file_query_info(location, G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL); + if (info && g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY) { + dir_location = g_object_ref(location); + } else { + dir_location = g_file_get_parent(location); + } + + if (!dir_location) { return; + } if (priv->state == NEMO_TERMINAL_STATE_IN_SSH) { if (priv->ssh_sync_mode == NEMO_TERMINAL_SYNC_BOTH || priv->ssh_sync_mode == NEMO_TERMINAL_SYNC_FM_TO_TERM) { should_sync = TRUE; - target_path = get_remote_path_from_sftp_gfile(location); + target_path = get_remote_path_from_sftp_gfile(dir_location); } } else { if (priv->local_sync_mode == NEMO_TERMINAL_SYNC_BOTH || priv->local_sync_mode == NEMO_TERMINAL_SYNC_FM_TO_TERM) { - if (g_file_query_exists(location, NULL)) + if (g_file_query_exists(dir_location, NULL)) { - target_path = g_file_get_path(location); + target_path = g_file_get_path(dir_location); if (target_path != NULL) { should_sync = TRUE; @@ -998,8 +1056,8 @@ change_directory_in_terminal(NemoTerminalWidget *self, GFile *location) if (should_sync && target_path) { - const gchar *term_uri = vte_terminal_get_current_directory_uri(priv->terminal); - g_autoptr(GFile) term_gfile = term_uri ? g_file_new_for_uri(term_uri) : NULL; + const gchar *term_uri_str = vte_terminal_get_current_directory_uri(priv->terminal); + g_autoptr(GFile) term_gfile = term_uri_str ? g_file_new_for_uri(term_uri_str) : NULL; g_autofree gchar *term_path = term_gfile ? g_file_get_path(term_gfile) : NULL; if (term_path == NULL || g_strcmp0(term_path, target_path) != 0) @@ -1037,19 +1095,6 @@ parse_gvfs_ssh_path(GFile *location, gchar **hostname, gchar **username, gchar * return FALSE; } -/* - * _initiate_ssh_connection: - * @self: The #NemoTerminalWidget instance. - * @hostname: The hostname to connect to. - * @username: The username for the connection (can be %NULL). - * @port: The port for the connection (can be %NULL). - * @sync_mode: The synchronization mode for this SSH session. - * - * The command executed on the remote host is carefully constructed to first - * change to the target directory and then start a new interactive shell. - * It also injects a `PROMPT_COMMAND` to enable directory tracking via OSC 7 - * escape sequences, which is necessary for Terminal -> File Manager sync. - */ static void _initiate_ssh_connection(NemoTerminalWidget *self, const gchar *hostname, const gchar *username, const gchar *port, NemoTerminalSyncMode sync_mode) { @@ -1262,57 +1307,66 @@ void nemo_terminal_widget_set_current_location(NemoTerminalWidget *self, GFile *location) { NemoTerminalWidgetPrivate *priv; - g_autofree gchar *hostname = NULL, *username = NULL, *port = NULL; - g_autofree gchar *scheme = NULL; g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); priv = self->priv; - if (location != NULL && (priv->current_location == NULL || !g_file_equal(location, priv->current_location))) + if ((priv->current_location == location) || (priv->current_location && location && g_file_equal(priv->current_location, location))) { - g_set_object(&priv->current_location, location); - g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_CURRENT_LOCATION]); + return; } - if (location) { - scheme = g_file_get_uri_scheme(location); + g_set_object(&priv->current_location, location); + g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_CURRENT_LOCATION]); + + if (!location) + { + return; } - if (priv->state == NEMO_TERMINAL_STATE_IN_SSH) { - if (scheme && g_strcmp0(scheme, "sftp") == 0) { + g_autofree gchar *scheme = g_file_get_uri_scheme(location); + gboolean is_sftp = (scheme && g_strcmp0(scheme, "sftp") == 0); + + if (priv->state == NEMO_TERMINAL_STATE_IN_SSH) + { + if (is_sftp) + { + g_autofree gchar *hostname = NULL, *username = NULL, *port = NULL; if (parse_gvfs_ssh_path(location, &hostname, &username, &port) && - g_strcmp0(hostname, priv->ssh_hostname) == 0) { + priv->ssh_hostname && g_strcmp0(hostname, priv->ssh_hostname) == 0) + { change_directory_in_terminal(self, location); - } else { - _reset_to_local_state(self); - spawn_terminal_async(self); } - } else { - _reset_to_local_state(self); - spawn_terminal_async(self); } - } else { /* Local state */ - if (scheme && g_strcmp0(scheme, "sftp") == 0 && - priv->ssh_auto_connect_mode != NEMO_TERMINAL_SSH_AUTOCONNECT_OFF && - parse_gvfs_ssh_path(location, &hostname, &username, &port)) { - - NemoTerminalSyncMode sync_mode; - switch (priv->ssh_auto_connect_mode) { - case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH: sync_mode = NEMO_TERMINAL_SYNC_BOTH; break; - case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM: sync_mode = NEMO_TERMINAL_SYNC_FM_TO_TERM; break; - case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM: sync_mode = NEMO_TERMINAL_SYNC_TERM_TO_FM; break; - default: sync_mode = NEMO_TERMINAL_SYNC_NONE; break; - } + /* If not SFTP or different host, do nothing. Keep the current SSH session. */ + } + else /* Local state */ + { + if (is_sftp && priv->ssh_auto_connect_mode != NEMO_TERMINAL_SSH_AUTOCONNECT_OFF) + { + g_autofree gchar *hostname = NULL, *username = NULL, *port = NULL; + if (parse_gvfs_ssh_path(location, &hostname, &username, &port)) + { + NemoTerminalSyncMode sync_mode; + switch (priv->ssh_auto_connect_mode) { + case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH: sync_mode = NEMO_TERMINAL_SYNC_BOTH; break; + case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM: sync_mode = NEMO_TERMINAL_SYNC_FM_TO_TERM; break; + case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM: sync_mode = NEMO_TERMINAL_SYNC_TERM_TO_FM; break; + default: sync_mode = NEMO_TERMINAL_SYNC_NONE; break; + } - if (priv->child_pid != -1) { - _initiate_ssh_connection(self, hostname, username, port, sync_mode); - } else { - priv->pending_ssh_hostname = g_strdup(hostname); - priv->pending_ssh_username = g_strdup(username); - priv->pending_ssh_port = g_strdup(port); - priv->pending_ssh_sync_mode = sync_mode; + if (priv->child_pid != -1) { + _initiate_ssh_connection(self, hostname, username, port, sync_mode); + } else { + priv->pending_ssh_hostname = g_strdup(hostname); + priv->pending_ssh_username = g_strdup(username); + priv->pending_ssh_port = g_strdup(port); + priv->pending_ssh_sync_mode = sync_mode; + } } - } else { + } + else + { change_directory_in_terminal(self, location); } } @@ -1334,10 +1388,28 @@ nemo_terminal_widget_toggle_visible(NemoTerminalWidget *self) g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); priv = self->priv; + if (priv->in_toggling) + return; + priv->in_toggling = TRUE; + priv->is_visible = !priv->is_visible; - g_settings_set_boolean(nemo_window_state, "terminal-visible", priv->is_visible); - if (!priv->is_visible) { + if (priv->is_visible) + { + gtk_widget_show(GTK_WIDGET(self)); + if (priv->needs_respawn) + { + spawn_terminal_async(self); + } + else if (priv->current_location) + { + change_directory_in_terminal(self, priv->current_location); + } + nemo_terminal_widget_apply_new_size(self); + nemo_terminal_widget_ensure_terminal_focus(self); + } + else + { g_autoptr(GtkWidget) paned = g_weak_ref_get(&priv->paned_weak_ref); if (paned && GTK_IS_PANED(paned)) { int position = gtk_paned_get_position(GTK_PANED(paned)); @@ -1346,27 +1418,36 @@ nemo_terminal_widget_toggle_visible(NemoTerminalWidget *self) g_settings_set_int(nemo_window_state, "terminal-pane-size", total_height - position); } } + gtk_widget_hide(GTK_WIDGET(self)); } - nemo_terminal_widget_ensure_state(self); + g_settings_set_boolean(nemo_window_state, "terminal-visible", priv->is_visible); g_signal_emit(self, signals[TOGGLE_VISIBILITY], 0, priv->is_visible); + priv->in_toggling = FALSE; } void nemo_terminal_widget_ensure_state(NemoTerminalWidget *self) { NemoTerminalWidgetPrivate *priv; + gboolean should_be_visible; + g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self)); priv = self->priv; - priv->is_visible = g_settings_get_boolean(nemo_window_state, "terminal-visible"); + should_be_visible = g_settings_get_boolean(nemo_window_state, "terminal-visible"); - if (priv->is_visible) { + if (priv->is_visible != should_be_visible) + { + priv->is_visible = should_be_visible; + g_signal_emit(self, signals[TOGGLE_VISIBILITY], 0, priv->is_visible); + } + + if (should_be_visible) { + gtk_widget_show(GTK_WIDGET(self)); if (priv->needs_respawn) { spawn_terminal_async(self); } - gtk_widget_show(GTK_WIDGET(self)); - nemo_terminal_widget_ensure_terminal_focus(self); } else { gtk_widget_hide(GTK_WIDGET(self)); } @@ -1377,9 +1458,11 @@ _ensure_focus_timeout(gpointer user_data) { NemoTerminalWidget *self = user_data; NemoTerminalWidgetPrivate *priv = self->priv; - priv->focus_timeout_id = 0; - if (priv->is_visible) { + + if (priv && gtk_widget_get_window(GTK_WIDGET(priv->terminal))) + { gtk_widget_grab_focus(GTK_WIDGET(priv->terminal)); + priv->focus_timeout_id = 0; } return G_SOURCE_REMOVE; } @@ -1422,9 +1505,13 @@ nemo_terminal_widget_apply_new_size(NemoTerminalWidget *self) if (priv->is_visible) { gint total_height = gtk_widget_get_allocated_height(paned); term_size = g_settings_get_int(nemo_window_state, "terminal-pane-size"); - if (term_size <= 0) { + if (term_size <= MIN_TERMINAL_HEIGHT) { term_size = 200; /* Default fallback */ } - gtk_paned_set_position(GTK_PANED(paned), total_height - term_size); + + gint max_allowed_height = MAX(total_height - MIN_MAIN_VIEW_HEIGHT, MIN_TERMINAL_HEIGHT); + gint terminal_height = CLAMP(term_size, MIN_TERMINAL_HEIGHT, max_allowed_height); + + gtk_paned_set_position(GTK_PANED(paned), total_height - terminal_height); } }