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 1500a8a79..d909407c7 100644
--- a/libnemo-private/org.nemo.gschema.xml
+++ b/libnemo-private/org.nemo.gschema.xml
@@ -59,7 +59,7 @@
-
+
@@ -67,6 +67,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -629,6 +644,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..fb38b37b1
--- /dev/null
+++ b/src/nemo-terminal-widget.c
@@ -0,0 +1,3023 @@
+/* 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_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;
+}
+
+/**
+ * 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_at_pointer(GTK_MENU(menu), (GdkEvent*)event);
+ 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;
+
+ 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",
+ 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,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_BOOLEAN);
+}
+
+static void
+nemo_terminal_widget_set_property(GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(object);
+
+ 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;
+ }
+}
+
+static void
+nemo_terminal_widget_get_property(GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ 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;
+ }
+}
+
+/**
+ * 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, // 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, .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:
+ * @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)
+}
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 */