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..04f0f109a 100644
--- a/libnemo-private/org.nemo.gschema.xml
+++ b/libnemo-private/org.nemo.gschema.xml
@@ -59,7 +59,7 @@
-
+
@@ -67,6 +67,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -697,6 +712,41 @@
Side pane view
The side pane view to show in newly opened windows.
+
+ false
+ Terminal pane visibility
+ Whether the terminal pane should be visible.
+
+
+ 200
+ Terminal panel height
+ Height of the terminal panel in pixels.
+
+
+ ''
+ Terminal font
+ The font to use for the terminal. If empty, the system monospace font is used.
+
+
+ 12
+ Terminal font size
+ The font size to use for the embedded terminal in point units.
+
+
+ 'system'
+ Terminal color scheme
+ The color scheme to use for the embedded terminal. Available values: 'system', 'dark', 'light', 'solarized-dark', 'solarized-light', 'matrix', 'one-half-dark', 'one-half-light', 'monokai'.
+
+
+ 'both'
+ Local terminal folder synchronization mode
+ Determines how the embedded terminal synchronizes its current directory with the file manager when navigating local folders. 'none': No synchronization. 'fm-to-term': File manager navigation changes terminal directory. 'term-to-fm': Terminal navigation changes file manager location. 'both': Synchronization works in both directions.
+
+
+ 'off'
+ SSH terminal auto-connection and synchronization mode preference
+ Determines behavior when an SFTP location is active. 'off': Do not connect automatically. 'sync-both': Connect and sync both ways. 'sync-fm-to-term': Connect and sync File Manager to Terminal. 'sync-term-to-fm': Connect and sync Terminal to File Manager. 'sync-none': Connect without folder synchronization.
+
diff --git a/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..418321ccf
--- /dev/null
+++ b/src/nemo-terminal-widget.c
@@ -0,0 +1,1430 @@
+/* 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 "nemo-window-slot.h"
+#include "nemo-global-preferences.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* UI constants */
+#define MIN_MAIN_VIEW_HEIGHT 200
+#define MIN_TERMINAL_HEIGHT 50
+#define MIN_FONT_SIZE 6
+#define MAX_FONT_SIZE 72
+
+/* GObject properties */
+enum
+{
+ PROP_0,
+ PROP_CURRENT_LOCATION,
+ N_PROPS
+};
+
+/* GObject signals */
+enum
+{
+ CHANGE_DIRECTORY,
+ TOGGLE_VISIBILITY,
+ LAST_SIGNAL
+};
+
+/*
+ * NemoTerminalState:
+ * @NEMO_TERMINAL_STATE_LOCAL: The terminal is running a local shell.
+ * @NEMO_TERMINAL_STATE_IN_SSH: The terminal is in an active SSH session.
+ *
+ * Represents the operational state of the terminal widget. This is crucial
+ * for determining how directory synchronization and commands are handled.
+ */
+typedef enum
+{
+ NEMO_TERMINAL_STATE_LOCAL,
+ NEMO_TERMINAL_STATE_IN_SSH,
+} NemoTerminalState;
+
+struct _NemoTerminalWidgetPrivate
+{
+ /* Child widgets */
+ GtkWidget *scrolled_window; /* The GtkScrolledWindow that makes the terminal scrollable. */
+ VteTerminal *terminal; /* The core VTE terminal widget. */
+ GtkWidget *ssh_indicator; /* A label shown at the top to indicate an active SSH session. */
+
+ /* State management */
+ NemoTerminalState state; /* The current operational state (local shell or remote SSH session). */
+ gboolean is_visible; /* Tracks if the terminal widget is currently shown to the user. */
+ gboolean in_toggling; /* A re-entrancy guard for the visibility toggle function to prevent rapid, repeated calls. */
+ gboolean needs_respawn; /* Flag indicating if the terminal's child process needs to be respawned (e.g., after being hidden and shown again). */
+ gboolean ignore_next_terminal_cd_signal; /* A flag to prevent feedback loops when the file manager programmatically changes the terminal's directory. */
+ guint focus_timeout_id; /* The ID for a timeout used to ensure the terminal gets focus after certain operations. */
+ GPid child_pid; /* The process ID of the shell or SSH client running in the terminal. -1 if no process is running. */
+ GCancellable *spawn_cancellable; /* A GCancellable object to allow cancelling an asynchronous terminal spawn operation. */
+ GWeakRef paned_weak_ref; /* A weak reference to the parent GtkPaned to avoid circular references and allow size adjustments. */
+
+ /* Preferences */
+ gchar *color_scheme; /* The name of the current color scheme (e.g., "dark", "solarized-light"). */
+ NemoTerminalSyncMode local_sync_mode; /* The directory synchronization mode for local shell sessions. */
+ NemoTerminalSshAutoConnectMode ssh_auto_connect_mode; /* The auto-connection behavior when navigating to SFTP locations. */
+
+ /* Location and SSH details */
+ GFile *current_location; /* The GFile representing the current directory displayed in the file manager view. */
+
+ gchar *ssh_hostname; /* The hostname for the current SSH connection. */
+ gchar *ssh_username; /* The username for the current SSH connection. */
+ gchar *ssh_port; /* The port for the current SSH connection. */
+ gchar *ssh_remote_path; /* The remote path to change to after an SSH connection is established. */
+ NemoTerminalSyncMode ssh_sync_mode; /* The directory synchronization mode for the current SSH session. */
+
+ /* Pending SSH connection details, used when a location is set before the terminal is spawned */
+ gchar *pending_ssh_hostname; /* The hostname for a pending SSH connection, to be used after the local shell spawns. */
+ gchar *pending_ssh_username; /* The username for a pending SSH connection. */
+ gchar *pending_ssh_port; /* The port for a pending SSH connection. */
+ NemoTerminalSyncMode pending_ssh_sync_mode; /* The sync mode for a pending SSH connection. */
+};
+
+/* Data keys for g_object_set_data() */
+static const gchar *const DATA_KEY_SSH_HOSTNAME = "ntw-ssh-hostname";
+static const gchar *const DATA_KEY_SSH_USERNAME = "ntw-ssh-username";
+static const gchar *const DATA_KEY_SSH_PORT = "ntw-ssh-port";
+static const gchar *const DATA_KEY_SSH_SYNC_MODE = "ntw-ssh-sync-mode";
+
+/* Shell control sequences for preserving user input during programmatic 'cd' */
+static const gchar *const SHELL_CTRL_A = "\x01"; /* Move cursor to beginning of line */
+static const gchar *const SHELL_CTRL_K = "\x0B"; /* Kill (cut) from cursor to end of line */
+static const gchar *const SHELL_CTRL_Y = "\x19"; /* Yank (paste) killed text */
+static const gchar *const SHELL_CTRL_E = "\x05"; /* Move cursor to end of line */
+static const gchar *const SHELL_DELETE = "\033[3~"; /* Delete character under cursor */
+
+typedef struct
+{
+ GdkRGBA foreground;
+ GdkRGBA background;
+ GdkRGBA palette[16];
+ gboolean use_system_colors;
+} NemoTerminalColorPalette;
+
+#define RGB(r, g, b) ((GdkRGBA) { .red = (r), .green = (g), .blue = (b), .alpha = 1.0 })
+
+typedef struct
+{
+ const gchar *id;
+ const gchar *label_pot;
+ const NemoTerminalColorPalette palette;
+} MenuSchemeEntry;
+
+/* clang-format off */
+static const MenuSchemeEntry COLOR_SCHEME_ENTRIES[] = {
+ { "system", N_("System"), .palette = { .use_system_colors = TRUE } },
+ { "dark", N_("Dark"), .palette = {
+ .foreground = RGB(0.9, 0.9, 0.9), .background = RGB(0.12, 0.12, 0.12),
+ .palette = {
+ RGB(0.0, 0.0, 0.0), RGB(0.8, 0.0, 0.0), RGB(0.0, 0.8, 0.0), RGB(0.8, 0.8, 0.0),
+ RGB(0.0, 0.0, 0.8), RGB(0.8, 0.0, 0.8), RGB(0.0, 0.8, 0.8), RGB(0.8, 0.8, 0.8),
+ RGB(0.5, 0.5, 0.5), RGB(1.0, 0.4, 0.4), RGB(0.4, 1.0, 0.4), RGB(1.0, 1.0, 0.4),
+ RGB(0.4, 0.4, 1.0), RGB(1.0, 0.4, 1.0), RGB(0.4, 1.0, 1.0), RGB(1.0, 1.0, 1.0)
+ }
+ }
+ },
+ { "light", N_("Light"), .palette = {
+ .foreground = RGB(0.15, 0.15, 0.15), .background = RGB(0.98, 0.98, 0.98),
+ .palette = {
+ RGB(0.2, 0.2, 0.2), RGB(0.8, 0.2, 0.2), RGB(0.1, 0.6, 0.1), RGB(0.7, 0.6, 0.1),
+ RGB(0.2, 0.4, 0.7), RGB(0.6, 0.3, 0.5), RGB(0.3, 0.6, 0.7), RGB(0.7, 0.7, 0.7),
+ RGB(0.4, 0.4, 0.4), RGB(0.9, 0.3, 0.3), RGB(0.2, 0.7, 0.2), RGB(0.8, 0.7, 0.2),
+ RGB(0.3, 0.5, 0.8), RGB(0.7, 0.4, 0.6), RGB(0.4, 0.7, 0.8), RGB(0.9, 0.9, 0.9)
+ }
+ }
+ },
+ { "solarized-dark", N_("Solarized Dark"), .palette = {
+ .foreground = RGB(0.8235, 0.8588, 0.8706), .background = RGB(0.0000, 0.1686, 0.2118),
+ .palette = {
+ RGB(0.0275, 0.2118, 0.2588), RGB(0.8627, 0.1961, 0.1843), RGB(0.5216, 0.6000, 0.0000), RGB(0.7098, 0.5412, 0.0000),
+ RGB(0.1490, 0.5451, 0.8235), RGB(0.8275, 0.2118, 0.5098), RGB(0.1647, 0.6314, 0.6000), RGB(0.9294, 0.9098, 0.8353),
+ RGB(0.0000, 0.1686, 0.2118), RGB(0.8000, 0.2588, 0.2078), RGB(0.3725, 0.4235, 0.4314), RGB(0.4078, 0.4745, 0.4784),
+ RGB(0.5137, 0.5804, 0.5843), RGB(0.4235, 0.4431, 0.6118), RGB(0.5804, 0.6078, 0.5373), RGB(0.9922, 0.9647, 0.8902)
+ }
+ }
+ },
+ { "solarized-light", N_("Solarized Light"), .palette = {
+ .foreground = RGB(0.4000, 0.4784, 0.5098), .background = RGB(0.9922, 0.9647, 0.8902),
+ .palette = {
+ RGB(0.0275, 0.2118, 0.2588), RGB(0.8627, 0.1961, 0.1843), RGB(0.5216, 0.6000, 0.0000), RGB(0.7098, 0.5412, 0.0000),
+ RGB(0.1490, 0.5451, 0.8235), RGB(0.8275, 0.2118, 0.5098), RGB(0.1647, 0.6314, 0.6000), RGB(0.9294, 0.9098, 0.8353),
+ RGB(0.0000, 0.1686, 0.2118), RGB(0.8000, 0.2588, 0.2078), RGB(0.3725, 0.4235, 0.4314), RGB(0.4078, 0.4745, 0.4784),
+ RGB(0.5137, 0.5804, 0.5843), RGB(0.4235, 0.4431, 0.6118), RGB(0.5804, 0.6078, 0.5373), RGB(0.8235, 0.8588, 0.8706)
+ }
+ }
+ },
+ { "matrix", N_("Matrix"), .palette = {
+ .foreground = RGB(0.1, 0.9, 0.1), .background = RGB(0.0, 0.0, 0.0),
+ .palette = {
+ RGB(0.0, 0.0, 0.0), RGB(0.0, 0.5, 0.0), RGB(0.0, 0.8, 0.0), RGB(0.1, 0.6, 0.0),
+ RGB(0.0, 0.4, 0.0), RGB(0.1, 0.5, 0.1), RGB(0.0, 0.7, 0.1), RGB(0.1, 0.9, 0.1),
+ RGB(0.0, 0.3, 0.0), RGB(0.0, 0.6, 0.0), RGB(0.0, 1.0, 0.0), RGB(0.2, 0.7, 0.0),
+ RGB(0.0, 0.5, 0.0), RGB(0.2, 0.6, 0.2), RGB(0.0, 0.8, 0.2), RGB(0.2, 1.0, 0.2)
+ }
+ }
+ },
+ { "one-half-dark", N_("One Half Dark"), .palette = {
+ .foreground = RGB(0.870, 0.870, 0.870), .background = RGB(0.157, 0.168, 0.184),
+ .palette = {
+ RGB(0.157, 0.168, 0.184), RGB(0.882, 0.490, 0.470), RGB(0.560, 0.749, 0.450), RGB(0.941, 0.768, 0.470),
+ RGB(0.400, 0.627, 0.850), RGB(0.768, 0.470, 0.800), RGB(0.341, 0.709, 0.729), RGB(0.870, 0.870, 0.870),
+ RGB(0.400, 0.450, 0.500), RGB(0.882, 0.490, 0.470), RGB(0.560, 0.749, 0.450), RGB(0.941, 0.768, 0.470),
+ RGB(0.400, 0.627, 0.850), RGB(0.768, 0.470, 0.800), RGB(0.341, 0.709, 0.729), RGB(0.970, 0.970, 0.970)
+ }
+ }
+ },
+ { "one-half-light", N_("One Half Light"), .palette = {
+ .foreground = RGB(0.220, 0.240, 0.260), .background = RGB(0.980, 0.980, 0.980),
+ .palette = {
+ RGB(0.220, 0.240, 0.260), RGB(0.858, 0.200, 0.180), RGB(0.310, 0.600, 0.110), RGB(0.850, 0.588, 0.100),
+ RGB(0.231, 0.490, 0.749), RGB(0.670, 0.270, 0.729), RGB(0.149, 0.639, 0.678), RGB(0.800, 0.800, 0.800),
+ RGB(0.400, 0.400, 0.400), RGB(0.858, 0.200, 0.180), RGB(0.310, 0.600, 0.110), RGB(0.850, 0.588, 0.100),
+ RGB(0.231, 0.490, 0.749), RGB(0.670, 0.270, 0.729), RGB(0.149, 0.639, 0.678), RGB(0.080, 0.080, 0.080)
+ }
+ }
+ },
+ { "monokai", N_("Monokai"), .palette = {
+ .foreground = RGB(0.929, 0.925, 0.910), .background = RGB(0, 0, 0),
+ .palette = {
+ RGB(0.153, 0.157, 0.149), RGB(0.980, 0.149, 0.450), RGB(0.650, 0.890, 0.180), RGB(0.960, 0.780, 0.310),
+ RGB(0.208, 0.580, 0.839), RGB(0.670, 0.380, 0.960), RGB(0.400, 0.950, 0.950), RGB(0.929, 0.925, 0.910),
+ RGB(0.400, 0.400, 0.400), RGB(0.980, 0.149, 0.450), RGB(0.650, 0.890, 0.180), RGB(0.960, 0.780, 0.310),
+ RGB(0.208, 0.580, 0.839), RGB(0.670, 0.380, 0.960), RGB(0.400, 0.950, 0.950), RGB(1.000, 1.000, 1.000)
+ }
+ }
+ },
+};
+/* clang-format on */
+
+typedef struct
+{
+ int size_pts;
+} 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;
+ const gchar *label_pot;
+} 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;
+ const gchar *label_pot;
+} MenuSshAutoConnectEntry;
+
+static const MenuSshAutoConnectEntry SFTP_AUTO_CONNECT_ENTRIES[] = {
+ { NEMO_TERMINAL_SSH_AUTOCONNECT_OFF, N_("Do not connect automatically") },
+ { NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH, N_("Automatically connect and sync both ways") },
+ { NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM, N_("Automatically connect and sync: File Manager → Terminal") },
+ { NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM, N_("Automatically connect and sync: Terminal → File Manager") },
+ { NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_NONE, N_("Automatically connect without syncing") }
+};
+
+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") }
+};
+
+static const gchar *const DATA_KEY_VALUE = "ntw-value";
+
+/* Forward declarations */
+static void spawn_terminal_async(NemoTerminalWidget *self);
+static void on_terminal_child_exited(VteTerminal *terminal, gint status, gpointer user_data);
+static gboolean on_terminal_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
+static gboolean on_terminal_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data);
+static void on_terminal_directory_changed(VteTerminal *terminal, gpointer user_data);
+static void on_color_scheme_changed(GtkCheckMenuItem *menuitem, gpointer user_data);
+static void on_font_size_changed(GtkCheckMenuItem *menuitem, gpointer user_data);
+static void on_enum_pref_changed(GtkCheckMenuItem *menuitem, gpointer user_data);
+static void on_terminal_preference_changed(GSettings *settings, const gchar *key, gpointer user_data);
+static void setup_terminal_font(VteTerminal *terminal);
+static void nemo_terminal_widget_apply_color_scheme(NemoTerminalWidget *self);
+static void _initiate_ssh_connection(NemoTerminalWidget *self, const gchar *hostname, const gchar *username, const gchar *port, NemoTerminalSyncMode sync_mode);
+static gboolean parse_gvfs_ssh_path(GFile *location, gchar **hostname, gchar **username, gchar **port);
+static void change_directory_in_terminal(NemoTerminalWidget *self, GFile *location);
+static void _clear_ssh_connection_data(NemoTerminalWidgetPrivate *priv);
+static void _reset_to_local_state(NemoTerminalWidget *self);
+static const gchar * nemo_terminal_widget_get_color_scheme(NemoTerminalWidget *self);
+static void nemo_terminal_widget_set_color_scheme(NemoTerminalWidget *self, const gchar *scheme);
+
+static GParamSpec *properties[N_PROPS];
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_TYPE_WITH_CODE(NemoTerminalWidget, nemo_terminal_widget, GTK_TYPE_BOX,
+ G_ADD_PRIVATE(NemoTerminalWidget))
+
+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);
+ NemoTerminalWidgetPrivate *priv = self->priv;
+
+ switch (prop_id)
+ {
+ case PROP_CURRENT_LOCATION:
+ g_value_set_object(value, priv->current_location);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+nemo_terminal_widget_finalize(GObject *object)
+{
+ NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(object);
+ NemoTerminalWidgetPrivate *priv = self->priv;
+ g_autoptr(GtkWidget) paned = g_weak_ref_get(&priv->paned_weak_ref);
+
+ g_signal_handlers_disconnect_by_data(nemo_window_state, self);
+
+ if (paned)
+ {
+ g_signal_handlers_disconnect_by_data(paned, self);
+ }
+ g_weak_ref_clear(&priv->paned_weak_ref);
+
+ if (priv->focus_timeout_id > 0)
+ g_source_remove(priv->focus_timeout_id);
+
+ g_cancellable_cancel(priv->spawn_cancellable);
+ g_clear_object(&priv->spawn_cancellable);
+ g_clear_object(&priv->current_location);
+ g_clear_pointer(&priv->color_scheme, g_free);
+
+ _clear_ssh_connection_data(priv);
+
+ G_OBJECT_CLASS(nemo_terminal_widget_parent_class)->finalize(object);
+}
+
+static void
+nemo_terminal_widget_class_init(NemoTerminalWidgetClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+ 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;
+
+ properties[PROP_CURRENT_LOCATION] =
+ g_param_spec_object("current-location",
+ "Current Location",
+ "The GFile representing the current directory.",
+ G_TYPE_FILE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties(object_class, N_PROPS, properties);
+
+ signals[CHANGE_DIRECTORY] =
+ g_signal_new("change-directory",
+ G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL,
+ 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_init(NemoTerminalWidget *self)
+{
+ NemoTerminalWidgetPrivate *priv = nemo_terminal_widget_get_instance_private(self);
+ GtkStyleContext *context = NULL;
+ g_autoptr(GtkCssProvider) provider = NULL;
+ GtkWidget *vbox;
+
+ self->priv = priv;
+
+ priv->state = NEMO_TERMINAL_STATE_LOCAL;
+ priv->needs_respawn = TRUE;
+ priv->child_pid = -1;
+ priv->spawn_cancellable = g_cancellable_new();
+ g_weak_ref_init(&priv->paned_weak_ref, NULL);
+
+ priv->scrolled_window = gtk_scrolled_window_new(NULL, NULL);
+ gtk_widget_set_vexpand(priv->scrolled_window, TRUE);
+ gtk_widget_set_hexpand(priv->scrolled_window, TRUE);
+
+ priv->terminal = VTE_TERMINAL(vte_terminal_new());
+ vte_terminal_set_scroll_on_output(priv->terminal, FALSE);
+ vte_terminal_set_scroll_on_keystroke(priv->terminal, TRUE);
+ vte_terminal_set_scrollback_lines(priv->terminal, 10000);
+ gtk_widget_set_can_focus(GTK_WIDGET(priv->terminal), TRUE);
+
+ priv->ssh_indicator = gtk_label_new("SSH");
+ gtk_widget_set_name(priv->ssh_indicator, "ssh-indicator");
+ gtk_widget_set_no_show_all(priv->ssh_indicator, TRUE);
+ gtk_widget_hide(priv->ssh_indicator);
+ gtk_widget_set_vexpand(priv->ssh_indicator, FALSE);
+ gtk_widget_set_hexpand(priv->ssh_indicator, TRUE);
+ gtk_label_set_xalign(GTK_LABEL(priv->ssh_indicator), 0.5);
+
+ provider = gtk_css_provider_new();
+ gtk_css_provider_load_from_data(provider, "label#ssh-indicator { background-color: #3465a4; color: white; padding: 2px 5px; margin: 0; font-weight: bold; }", -1, NULL);
+ context = gtk_widget_get_style_context(priv->ssh_indicator);
+ gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_USER);
+
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), priv->ssh_indicator, FALSE, FALSE, 0);
+ gtk_container_add(GTK_CONTAINER(priv->scrolled_window), GTK_WIDGET(priv->terminal));
+ gtk_box_pack_start(GTK_BOX(vbox), priv->scrolled_window, TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(self), vbox, TRUE, TRUE, 0);
+
+ priv->local_sync_mode = g_settings_get_enum(nemo_window_state, "local-terminal-sync-mode");
+ priv->ssh_auto_connect_mode = g_settings_get_enum(nemo_window_state, "ssh-terminal-auto-connect-mode");
+
+ setup_terminal_font(priv->terminal);
+ nemo_terminal_widget_get_color_scheme(self);
+ nemo_terminal_widget_apply_color_scheme(self);
+
+ g_signal_connect(nemo_window_state, "changed", G_CALLBACK(on_terminal_preference_changed), self);
+
+ g_signal_connect(priv->terminal, "child-exited", G_CALLBACK(on_terminal_child_exited), self);
+ g_signal_connect(priv->terminal, "button-press-event", G_CALLBACK(on_terminal_button_press), self);
+ g_signal_connect(priv->terminal, "key-press-event", G_CALLBACK(on_terminal_key_press), self);
+ g_signal_connect(priv->terminal, "current-directory-uri-changed", G_CALLBACK(on_terminal_directory_changed), self);
+
+ gtk_widget_show_all(GTK_WIDGET(self));
+ gtk_widget_hide(GTK_WIDGET(self));
+}
+
+static void
+spawn_async_callback(VteTerminal *terminal, GPid pid, GError *error, gpointer user_data)
+{
+ NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data);
+ NemoTerminalWidgetPrivate *priv = self->priv;
+
+ if (pid == -1)
+ {
+ g_warning("Failed to spawn terminal: %s", error ? error->message : "Unknown error");
+ priv->needs_respawn = TRUE;
+ priv->child_pid = -1;
+ }
+ else
+ {
+ priv->child_pid = pid;
+ priv->needs_respawn = FALSE;
+ if (priv->pending_ssh_hostname)
+ {
+ _initiate_ssh_connection(self, priv->pending_ssh_hostname, priv->pending_ssh_username, priv->pending_ssh_port, priv->pending_ssh_sync_mode);
+ g_clear_pointer(&priv->pending_ssh_hostname, g_free);
+ g_clear_pointer(&priv->pending_ssh_username, g_free);
+ g_clear_pointer(&priv->pending_ssh_port, g_free);
+ }
+ else if (priv->current_location)
+ {
+ /* If a local shell just spawned, sync its directory to the current location.
+ * This is crucial for new tabs/windows where the terminal starts visible. */
+ change_directory_in_terminal(self, priv->current_location);
+ }
+ }
+}
+
+static void
+spawn_terminal_async(NemoTerminalWidget *self)
+{
+ NemoTerminalWidgetPrivate *priv = self->priv;
+ g_autofree gchar *working_directory = NULL;
+ const gchar *shell_executable;
+ gchar *argv[2];
+ gchar *envp[] = {
+ "TERM=xterm-256color",
+ "LC_ALL=C.UTF-8",
+ "COLORTERM=truecolor",
+ NULL
+ };
+
+ g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self));
+
+ if (priv->child_pid != -1)
+ return;
+
+ if (g_cancellable_is_cancelled(priv->spawn_cancellable))
+ {
+ g_clear_object(&priv->spawn_cancellable);
+ priv->spawn_cancellable = g_cancellable_new();
+ }
+
+ shell_executable = g_getenv("SHELL");
+ if (!shell_executable || *shell_executable == '\0')
+ shell_executable = "/bin/sh";
+
+ argv[0] = (gchar *)shell_executable;
+ argv[1] = NULL;
+
+ if (priv->pending_ssh_hostname != NULL)
+ {
+ working_directory = NULL;
+ }
+ else if (priv->current_location && g_file_query_exists(priv->current_location, NULL))
+ {
+ working_directory = g_file_get_path(priv->current_location);
+ }
+
+ vte_terminal_spawn_async(priv->terminal,
+ VTE_PTY_DEFAULT,
+ working_directory,
+ argv,
+ envp,
+ G_SPAWN_SEARCH_PATH,
+ NULL, NULL, NULL,
+ -1,
+ priv->spawn_cancellable,
+ (VteTerminalSpawnAsyncCallback)spawn_async_callback,
+ self);
+}
+
+static void
+_clear_ssh_connection_data(NemoTerminalWidgetPrivate *priv)
+{
+ g_clear_pointer(&priv->ssh_hostname, g_free);
+ g_clear_pointer(&priv->ssh_username, g_free);
+ g_clear_pointer(&priv->ssh_port, g_free);
+ g_clear_pointer(&priv->ssh_remote_path, g_free);
+ g_clear_pointer(&priv->pending_ssh_hostname, g_free);
+ g_clear_pointer(&priv->pending_ssh_username, g_free);
+ g_clear_pointer(&priv->pending_ssh_port, g_free);
+}
+
+static void
+_reset_to_local_state(NemoTerminalWidget *self)
+{
+ NemoTerminalWidgetPrivate *priv = self->priv;
+
+ _clear_ssh_connection_data(priv);
+ priv->ssh_sync_mode = NEMO_TERMINAL_SYNC_NONE;
+
+ priv->state = NEMO_TERMINAL_STATE_LOCAL;
+ gtk_widget_hide(priv->ssh_indicator);
+ priv->needs_respawn = TRUE;
+}
+
+static void
+on_terminal_child_exited(VteTerminal *terminal, gint status, gpointer user_data)
+{
+ NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data);
+ NemoTerminalWidgetPrivate *priv = self->priv;
+
+ if (gtk_widget_in_destruction(GTK_WIDGET(self)))
+ {
+ priv->child_pid = -1;
+ return;
+ }
+
+ priv->child_pid = -1;
+
+ if (priv->state == NEMO_TERMINAL_STATE_IN_SSH)
+ {
+ _reset_to_local_state(self);
+ }
+
+ if (gtk_widget_get_ancestor(GTK_WIDGET(self), GTK_TYPE_WINDOW))
+ {
+ if (priv->is_visible)
+ spawn_terminal_async(self);
+ else
+ priv->needs_respawn = TRUE;
+ }
+}
+
+static void
+on_terminal_preference_changed(GSettings *settings, const gchar *key, gpointer user_data)
+{
+ NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data);
+ NemoTerminalWidgetPrivate *priv = self->priv;
+
+ if (g_strcmp0(key, "local-terminal-sync-mode") == 0)
+ {
+ priv->local_sync_mode = g_settings_get_enum(settings, "local-terminal-sync-mode");
+ }
+ else if (g_strcmp0(key, "ssh-terminal-auto-connect-mode") == 0)
+ {
+ priv->ssh_auto_connect_mode = g_settings_get_enum(settings, "ssh-terminal-auto-connect-mode");
+ }
+ else if (g_strcmp0(key, "terminal-font") == 0 || g_strcmp0(key, "terminal-font-size") == 0)
+ {
+ setup_terminal_font(priv->terminal);
+ }
+ else if (g_strcmp0(key, "terminal-color-scheme") == 0)
+ {
+ g_free(priv->color_scheme);
+ priv->color_scheme = g_settings_get_string(settings, "terminal-color-scheme");
+ nemo_terminal_widget_apply_color_scheme(self);
+ }
+}
+
+static void
+on_terminal_directory_changed(VteTerminal *terminal, gpointer user_data)
+{
+ NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data);
+ NemoTerminalWidgetPrivate *priv = self->priv;
+ g_autoptr(GFile) new_gfile_location = NULL;
+ gboolean should_sync_to_fm = FALSE;
+ const gchar *cwd_uri = vte_terminal_get_current_directory_uri(terminal);
+
+ if (!cwd_uri) return;
+
+ if (priv->ignore_next_terminal_cd_signal)
+ {
+ priv->ignore_next_terminal_cd_signal = FALSE;
+ return;
+ }
+
+ if (priv->state == NEMO_TERMINAL_STATE_IN_SSH)
+ {
+ if (priv->ssh_sync_mode == NEMO_TERMINAL_SYNC_BOTH || priv->ssh_sync_mode == NEMO_TERMINAL_SYNC_TERM_TO_FM)
+ {
+ should_sync_to_fm = TRUE;
+ if (g_str_has_prefix(cwd_uri, "file://"))
+ {
+ g_autofree gchar *local_path = g_filename_from_uri(cwd_uri, NULL, NULL);
+ if (local_path && priv->ssh_hostname)
+ {
+ g_autoptr(GString) sftp_uri = g_string_new("sftp://");
+ if (priv->ssh_username && *priv->ssh_username)
+ g_string_append_printf(sftp_uri, "%s@", priv->ssh_username);
+ g_string_append(sftp_uri, priv->ssh_hostname);
+ if (priv->ssh_port && *priv->ssh_port)
+ g_string_append_printf(sftp_uri, ":%s", priv->ssh_port);
+ g_string_append(sftp_uri, local_path);
+ new_gfile_location = g_file_new_for_uri(sftp_uri->str);
+ }
+ }
+ }
+ }
+ else
+ {
+ if (priv->local_sync_mode == NEMO_TERMINAL_SYNC_BOTH || priv->local_sync_mode == NEMO_TERMINAL_SYNC_TERM_TO_FM)
+ {
+ should_sync_to_fm = TRUE;
+ new_gfile_location = g_file_new_for_uri(cwd_uri);
+ }
+ }
+
+ if (should_sync_to_fm && new_gfile_location && (priv->current_location == NULL || !g_file_equal(new_gfile_location, priv->current_location)))
+ {
+ g_set_object(&priv->current_location, new_gfile_location);
+ g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_CURRENT_LOCATION]);
+ g_signal_emit(self, signals[CHANGE_DIRECTORY], 0, new_gfile_location);
+ }
+}
+
+/*
+ * feed_cd_command:
+ * @terminal: The VteTerminal to send the command to.
+ * @path: The directory path to change to.
+ *
+ * This function programmatically sends a 'cd' command to the terminal's
+ * child process. It uses a sequence of shell control characters (CTRL+A,
+ * CTRL+K, etc.) to insert the command at the beginning of the line,
+ * execute it, and then restore any text the user might have been typing.
+ * This provides a less disruptive user experience.
+ */
+static void
+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);
+ g_autofree gchar *cd_command_str = g_strdup_printf(" cd %s\r", quoted_path);
+
+ vte_terminal_feed_child(terminal, SHELL_CTRL_A, -1);
+ vte_terminal_feed_child(terminal, " ", -1);
+ vte_terminal_feed_child(terminal, SHELL_CTRL_A, -1);
+ vte_terminal_feed_child(terminal, SHELL_CTRL_K, -1);
+ vte_terminal_feed_child(terminal, cd_command_str, -1);
+ vte_terminal_feed_child(terminal, SHELL_CTRL_Y, -1);
+ vte_terminal_feed_child(terminal, SHELL_CTRL_A, -1);
+ vte_terminal_feed_child(terminal, SHELL_DELETE, -1);
+ vte_terminal_feed_child(terminal, SHELL_CTRL_E, -1);
+}
+
+static gboolean
+on_terminal_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
+{
+ NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data);
+ NemoTerminalWidgetPrivate *priv = self->priv;
+
+ if ((event->state & GDK_CONTROL_MASK) && (event->state & GDK_SHIFT_MASK))
+ {
+ switch (event->keyval)
+ {
+ case GDK_KEY_C:
+ case GDK_KEY_c:
+ vte_terminal_copy_clipboard_format(priv->terminal, VTE_FORMAT_TEXT);
+ return TRUE;
+ case GDK_KEY_V:
+ case GDK_KEY_v:
+ vte_terminal_paste_clipboard(priv->terminal);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void
+on_ssh_exit_activate(GtkMenuItem *menuitem, gpointer user_data)
+{
+ NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data);
+ NemoTerminalWidgetPrivate *priv = self->priv;
+ if (priv->state == NEMO_TERMINAL_STATE_IN_SSH)
+ {
+ vte_terminal_feed_child(priv->terminal, " exit\n", -1);
+ }
+}
+
+static GtkWidget *
+_create_radio_menu_item(GSList **group, const gchar *label, gboolean is_active, GCallback activate_callback, gpointer user_data, const gchar *data_key, gpointer data_value)
+{
+ GtkWidget *item = gtk_radio_menu_item_new_with_label(*group, label);
+ if (*group == NULL)
+ *group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item));
+
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), is_active);
+ g_signal_connect(item, "activate", activate_callback, user_data);
+ g_object_set_data(G_OBJECT(item), data_key, data_value);
+
+ return item;
+}
+
+static void on_ssh_connect_activate(GtkMenuItem *menuitem, gpointer user_data);
+
+static GtkWidget *
+_build_color_scheme_submenu(NemoTerminalWidget *self)
+{
+ GtkWidget *submenu = gtk_menu_new();
+ GSList *radio_group = NULL;
+ const gchar *current_scheme = nemo_terminal_widget_get_color_scheme(self);
+
+ for (gsize i = 0; i < G_N_ELEMENTS(COLOR_SCHEME_ENTRIES); ++i)
+ {
+ GtkWidget *item = _create_radio_menu_item(&radio_group,
+ _(COLOR_SCHEME_ENTRIES[i].label_pot),
+ g_strcmp0(current_scheme, COLOR_SCHEME_ENTRIES[i].id) == 0,
+ G_CALLBACK(on_color_scheme_changed),
+ self,
+ DATA_KEY_VALUE,
+ (gpointer)COLOR_SCHEME_ENTRIES[i].id);
+ gtk_menu_shell_append(GTK_MENU_SHELL(submenu), item);
+ }
+ return submenu;
+}
+
+static int
+get_terminal_font_size(void)
+{
+ int saved_size_pts = g_settings_get_int(nemo_window_state, "terminal-font-size");
+ return CLAMP(saved_size_pts, MIN_FONT_SIZE, MAX_FONT_SIZE);
+}
+
+static GtkWidget *
+_build_font_size_submenu(NemoTerminalWidget *self)
+{
+ GtkWidget *submenu = gtk_menu_new();
+ GSList *radio_group = NULL;
+ int current_size_pts = get_terminal_font_size();
+
+ for (gsize i = 0; i < G_N_ELEMENTS(FONT_SIZE_ENTRIES); ++i)
+ {
+ g_autofree gchar *label = g_strdup_printf("%d", FONT_SIZE_ENTRIES[i].size_pts);
+ GtkWidget *item = _create_radio_menu_item(&radio_group,
+ label,
+ current_size_pts == FONT_SIZE_ENTRIES[i].size_pts,
+ G_CALLBACK(on_font_size_changed),
+ self,
+ DATA_KEY_VALUE,
+ GINT_TO_POINTER(FONT_SIZE_ENTRIES[i].size_pts));
+ gtk_menu_shell_append(GTK_MENU_SHELL(submenu), item);
+ }
+ return submenu;
+}
+
+/*
+ * _build_enum_pref_submenu:
+ * @self: The #NemoTerminalWidget instance.
+ * @entries: A static array of menu entry data.
+ * @count: The number of entries in the array.
+ * @current_value: The current value of the preference to check against.
+ * @settings_key: The GSettings key name for this preference.
+ *
+ * A helper function to reduce code duplication when building radio-button
+ * submenus for enum-based preferences. It iterates over the provided
+ * entries, creates a radio menu item for each, and connects it to the
+ * generic `on_enum_pref_changed` callback.
+ *
+ * Returns: (transfer full): A new GtkMenu widget containing the radio items.
+ */
+static GtkWidget *
+_build_enum_pref_submenu(NemoTerminalWidget *self, const MenuSyncModeEntry *entries, gsize count, gint current_value, const gchar *settings_key)
+{
+ GtkWidget *submenu = gtk_menu_new();
+ GSList *radio_group = NULL;
+ for (gsize i = 0; i < count; ++i)
+ {
+ GtkWidget *item = _create_radio_menu_item(&radio_group,
+ _(entries[i].label_pot),
+ current_value == entries[i].mode,
+ G_CALLBACK(on_enum_pref_changed),
+ (gpointer)settings_key,
+ DATA_KEY_VALUE,
+ GINT_TO_POINTER(entries[i].mode));
+ gtk_menu_shell_append(GTK_MENU_SHELL(submenu), item);
+ }
+ return submenu;
+}
+
+static GtkWidget *
+_build_local_sync_submenu(NemoTerminalWidget *self)
+{
+ NemoTerminalWidgetPrivate *priv = self->priv;
+ return _build_enum_pref_submenu(self, LOCAL_SYNC_MODE_ENTRIES, G_N_ELEMENTS(LOCAL_SYNC_MODE_ENTRIES), priv->local_sync_mode, "local-terminal-sync-mode");
+}
+
+static GtkWidget *
+_build_sftp_auto_connect_submenu(NemoTerminalWidget *self)
+{
+ NemoTerminalWidgetPrivate *priv = self->priv;
+ /* We can reuse the helper here, but need to cast the array type as the structs are compatible. */
+ return _build_enum_pref_submenu(self, (const MenuSyncModeEntry*)SFTP_AUTO_CONNECT_ENTRIES, G_N_ELEMENTS(SFTP_AUTO_CONNECT_ENTRIES), priv->ssh_auto_connect_mode, "ssh-terminal-auto-connect-mode");
+}
+
+static GtkWidget *
+_build_manual_ssh_connect_submenu(NemoTerminalWidget *self, const gchar *hostname, const gchar *username, const gchar *port)
+{
+ GtkWidget *submenu = gtk_menu_new();
+ for (gsize i = 0; i < G_N_ELEMENTS(MANUAL_SSH_SYNC_ENTRIES); ++i)
+ {
+ GtkWidget *item = gtk_menu_item_new_with_label(_(MANUAL_SSH_SYNC_ENTRIES[i].label_pot));
+ g_object_set_data_full(G_OBJECT(item), DATA_KEY_SSH_HOSTNAME, g_strdup(hostname), g_free);
+ g_object_set_data_full(G_OBJECT(item), DATA_KEY_SSH_USERNAME, g_strdup(username), g_free);
+ g_object_set_data_full(G_OBJECT(item), DATA_KEY_SSH_PORT, g_strdup(port), g_free);
+ g_object_set_data(G_OBJECT(item), DATA_KEY_SSH_SYNC_MODE, GINT_TO_POINTER(MANUAL_SSH_SYNC_ENTRIES[i].mode));
+ g_signal_connect(item, "activate", G_CALLBACK(on_ssh_connect_activate), self);
+ gtk_menu_shell_append(GTK_MENU_SHELL(submenu), item);
+ }
+ return submenu;
+}
+
+static void
+_append_menu_item(GtkMenuShell *menu, 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, item);
+}
+
+static void
+_append_menu_item_with_submenu(GtkMenuShell *menu, const gchar *label, GtkWidget *submenu)
+{
+ GtkWidget *item = gtk_menu_item_new_with_label(label);
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), submenu);
+ gtk_menu_shell_append(menu, item);
+}
+
+static GtkWidget *
+create_terminal_popup_menu(NemoTerminalWidget *self)
+{
+ NemoTerminalWidgetPrivate *priv = self->priv;
+ GtkWidget *menu = gtk_menu_new();
+ gboolean is_sftp_location = FALSE;
+
+ _append_menu_item(GTK_MENU_SHELL(menu), _("Copy"), G_CALLBACK(vte_terminal_copy_clipboard_format), priv->terminal);
+ _append_menu_item(GTK_MENU_SHELL(menu), _("Paste"), G_CALLBACK(vte_terminal_paste_clipboard), priv->terminal);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
+ _append_menu_item(GTK_MENU_SHELL(menu), _("Select All"), G_CALLBACK(vte_terminal_select_all), priv->terminal);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
+
+ _append_menu_item_with_submenu(GTK_MENU_SHELL(menu), _("Color Scheme"), _build_color_scheme_submenu(self));
+ _append_menu_item_with_submenu(GTK_MENU_SHELL(menu), _("Font Size"), _build_font_size_submenu(self));
+ _append_menu_item_with_submenu(GTK_MENU_SHELL(menu), _("SSH Auto-Connect"), _build_sftp_auto_connect_submenu(self));
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
+
+ if (priv->current_location)
+ {
+ g_autofree gchar *scheme = g_file_get_uri_scheme(priv->current_location);
+ if (scheme && g_strcmp0(scheme, "sftp") == 0)
+ is_sftp_location = TRUE;
+ }
+
+ if (priv->state == NEMO_TERMINAL_STATE_IN_SSH)
+ {
+ _append_menu_item(GTK_MENU_SHELL(menu), _("Disconnect from SSH"), G_CALLBACK(on_ssh_exit_activate), self);
+ }
+ else
+ {
+ _append_menu_item_with_submenu(GTK_MENU_SHELL(menu), _("Local Folder Sync"), _build_local_sync_submenu(self));
+ if (is_sftp_location)
+ {
+ g_autofree gchar *hostname = NULL, *username = NULL, *port = NULL;
+ if (parse_gvfs_ssh_path(priv->current_location, &hostname, &username, &port))
+ {
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
+ g_autofree gchar *label = g_strdup_printf(_("SSH Connection to %s"), hostname);
+ _append_menu_item_with_submenu(GTK_MENU_SHELL(menu), label, _build_manual_ssh_connect_submenu(self, hostname, username, port));
+ }
+ }
+ }
+
+ gtk_widget_show_all(menu);
+ return menu;
+}
+
+static gboolean
+on_terminal_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
+{
+ NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data);
+
+ if (event->button == GDK_BUTTON_SECONDARY && event->type == GDK_BUTTON_PRESS)
+ {
+ GtkWidget *menu = create_terminal_popup_menu(self);
+ gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static gchar *
+get_remote_path_from_sftp_gfile(GFile *location)
+{
+ g_autoptr(GUri) uri = NULL;
+ g_autofree gchar *uri_str = g_file_get_uri(location);
+ if (!uri_str)
+ return NULL;
+
+ uri = g_uri_parse(uri_str, G_URI_FLAGS_NONE, NULL);
+ if (uri)
+ return g_strdup(g_uri_get_path(uri));
+
+ return NULL;
+}
+
+static void
+change_directory_in_terminal(NemoTerminalWidget *self, GFile *location)
+{
+ NemoTerminalWidgetPrivate *priv = self->priv;
+ g_autofree gchar *target_path = NULL;
+ gboolean should_sync = FALSE;
+
+ if (!priv->is_visible || priv->child_pid == -1)
+ return;
+
+ if (priv->state == NEMO_TERMINAL_STATE_IN_SSH)
+ {
+ if (priv->ssh_sync_mode == NEMO_TERMINAL_SYNC_BOTH || priv->ssh_sync_mode == NEMO_TERMINAL_SYNC_FM_TO_TERM)
+ {
+ should_sync = TRUE;
+ target_path = get_remote_path_from_sftp_gfile(location);
+ }
+ }
+ else
+ {
+ if (priv->local_sync_mode == NEMO_TERMINAL_SYNC_BOTH || priv->local_sync_mode == NEMO_TERMINAL_SYNC_FM_TO_TERM)
+ {
+ if (g_file_query_exists(location, NULL))
+ {
+ target_path = g_file_get_path(location);
+ if (target_path != NULL)
+ {
+ should_sync = TRUE;
+ }
+ }
+ }
+ }
+
+ if (should_sync && target_path)
+ {
+ const gchar *term_uri = vte_terminal_get_current_directory_uri(priv->terminal);
+ g_autoptr(GFile) term_gfile = term_uri ? g_file_new_for_uri(term_uri) : NULL;
+ g_autofree gchar *term_path = term_gfile ? g_file_get_path(term_gfile) : NULL;
+
+ if (term_path == NULL || g_strcmp0(term_path, target_path) != 0)
+ {
+ priv->ignore_next_terminal_cd_signal = TRUE;
+ feed_cd_command(priv->terminal, target_path);
+ }
+ }
+}
+
+static gboolean
+parse_gvfs_ssh_path(GFile *location, gchar **hostname, gchar **username, gchar **port)
+{
+ g_autoptr(GUri) uri = NULL;
+ g_autofree gchar *uri_str = g_file_get_uri(location);
+
+ *hostname = NULL;
+ *username = NULL;
+ *port = NULL;
+
+ if (!uri_str)
+ return FALSE;
+
+ uri = g_uri_parse(uri_str, G_URI_FLAGS_NONE, NULL);
+ if (uri && g_strcmp0(g_uri_get_scheme(uri), "sftp") == 0)
+ {
+ *hostname = g_strdup(g_uri_get_host(uri));
+ if (g_uri_get_userinfo(uri))
+ *username = g_strdup(g_uri_get_userinfo(uri));
+ if (g_uri_get_port(uri) > 0)
+ *port = g_strdup_printf("%d", g_uri_get_port(uri));
+ return (*hostname != NULL);
+ }
+
+ return FALSE;
+}
+
+/*
+ * _initiate_ssh_connection:
+ * @self: The #NemoTerminalWidget instance.
+ * @hostname: The hostname to connect to.
+ * @username: The username for the connection (can be %NULL).
+ * @port: The port for the connection (can be %NULL).
+ * @sync_mode: The synchronization mode for this SSH session.
+ *
+ * The command executed on the remote host is carefully constructed to first
+ * change to the target directory and then start a new interactive shell.
+ * It also injects a `PROMPT_COMMAND` to enable directory tracking via OSC 7
+ * escape sequences, which is necessary for Terminal -> File Manager sync.
+ */
+static void
+_initiate_ssh_connection(NemoTerminalWidget *self, const gchar *hostname, const gchar *username, const gchar *port, NemoTerminalSyncMode sync_mode)
+{
+ NemoTerminalWidgetPrivate *priv = self->priv;
+ g_autoptr(GPtrArray) argv_array = g_ptr_array_new_with_free_func(g_free);
+ g_autofree gchar *remote_cmd = NULL;
+ GString *remote_cmd_builder;
+
+ if (priv->child_pid != -1)
+ {
+ kill(priv->child_pid, SIGTERM);
+ priv->child_pid = -1;
+ }
+
+ priv->state = NEMO_TERMINAL_STATE_IN_SSH;
+ priv->ssh_sync_mode = sync_mode;
+ priv->ssh_hostname = g_strdup(hostname);
+ priv->ssh_username = g_strdup(username);
+ priv->ssh_port = g_strdup(port);
+
+ if (priv->current_location)
+ priv->ssh_remote_path = get_remote_path_from_sftp_gfile(priv->current_location);
+
+ remote_cmd_builder = g_string_new("");
+ if (priv->ssh_remote_path && *priv->ssh_remote_path)
+ {
+ g_autofree gchar *quoted_remote_path = g_shell_quote(priv->ssh_remote_path);
+ g_string_append_printf(remote_cmd_builder, " cd %s; ", quoted_remote_path);
+ }
+ if (priv->ssh_sync_mode == NEMO_TERMINAL_SYNC_BOTH || priv->ssh_sync_mode == NEMO_TERMINAL_SYNC_TERM_TO_FM)
+ {
+ g_string_append(remote_cmd_builder, " export PROMPT_COMMAND='echo -en \"\\033]7;file://$PWD\\007\"'; ");
+ }
+ g_string_append(remote_cmd_builder, "$SHELL -l");
+ remote_cmd = g_string_free(remote_cmd_builder, FALSE);
+
+ g_ptr_array_add(argv_array, g_strdup("ssh"));
+ g_ptr_array_add(argv_array, g_strdup("-t"));
+ if (port && *port)
+ {
+ g_ptr_array_add(argv_array, g_strdup("-p"));
+ g_ptr_array_add(argv_array, g_strdup(port));
+ }
+ if (username && *username)
+ g_ptr_array_add(argv_array, g_strdup_printf("%s@%s", username, hostname));
+ else
+ g_ptr_array_add(argv_array, g_strdup(hostname));
+
+ g_ptr_array_add(argv_array, g_strdup(remote_cmd));
+ g_ptr_array_add(argv_array, NULL);
+
+ gtk_widget_show(priv->ssh_indicator);
+
+ priv->ignore_next_terminal_cd_signal = TRUE;
+
+ vte_terminal_spawn_async(priv->terminal,
+ VTE_PTY_DEFAULT,
+ NULL,
+ (gchar **)argv_array->pdata,
+ NULL,
+ G_SPAWN_SEARCH_PATH,
+ NULL, NULL, NULL,
+ -1,
+ priv->spawn_cancellable,
+ (VteTerminalSpawnAsyncCallback)spawn_async_callback,
+ self);
+ nemo_terminal_widget_ensure_terminal_focus(self);
+}
+
+static void
+save_terminal_font_size(int font_size_pts)
+{
+ g_settings_set_int(nemo_window_state, "terminal-font-size", font_size_pts);
+}
+
+static void
+on_color_scheme_changed(GtkCheckMenuItem *menuitem, gpointer user_data)
+{
+ NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data);
+ if (gtk_check_menu_item_get_active(menuitem))
+ {
+ const gchar *scheme_name = g_object_get_data(G_OBJECT(menuitem), DATA_KEY_VALUE);
+ nemo_terminal_widget_set_color_scheme(self, scheme_name);
+ }
+}
+
+static void
+on_font_size_changed(GtkCheckMenuItem *menuitem, gpointer user_data)
+{
+ NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data);
+ NemoTerminalWidgetPrivate *priv = self->priv;
+ if (gtk_check_menu_item_get_active(menuitem))
+ {
+ int font_size_pts = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem), DATA_KEY_VALUE));
+ g_autoptr(PangoFontDescription) font_desc = pango_font_description_copy(vte_terminal_get_font(priv->terminal));
+ pango_font_description_set_size(font_desc, font_size_pts * PANGO_SCALE);
+ vte_terminal_set_font(priv->terminal, font_desc);
+ save_terminal_font_size(font_size_pts);
+ }
+}
+
+static void
+on_enum_pref_changed(GtkCheckMenuItem *menuitem, gpointer user_data)
+{
+ const gchar *pref_name = user_data;
+ if (gtk_check_menu_item_get_active(menuitem))
+ {
+ gint new_value = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem), DATA_KEY_VALUE));
+ g_settings_set_enum(nemo_window_state, pref_name, new_value);
+ }
+}
+
+static void
+on_ssh_connect_activate(GtkMenuItem *menuitem, gpointer user_data)
+{
+ NemoTerminalWidget *self = NEMO_TERMINAL_WIDGET(user_data);
+ const gchar *hostname = g_object_get_data(G_OBJECT(menuitem), DATA_KEY_SSH_HOSTNAME);
+ const gchar *username = g_object_get_data(G_OBJECT(menuitem), DATA_KEY_SSH_USERNAME);
+ const gchar *port = g_object_get_data(G_OBJECT(menuitem), DATA_KEY_SSH_PORT);
+ NemoTerminalSyncMode sync_mode = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem), DATA_KEY_SSH_SYNC_MODE));
+
+ if (hostname)
+ _initiate_ssh_connection(self, hostname, username, port, sync_mode);
+}
+
+static void
+setup_terminal_font(VteTerminal *terminal)
+{
+ g_autoptr(PangoFontDescription) font_desc = NULL;
+ g_autofree gchar *font_name = g_settings_get_string(nemo_window_state, "terminal-font");
+ int font_size = get_terminal_font_size();
+
+ if (font_name && *font_name) {
+ font_desc = pango_font_description_from_string(font_name);
+ } else {
+ font_desc = pango_font_description_from_string("Monospace");
+ }
+
+ pango_font_description_set_size(font_desc, font_size * PANGO_SCALE);
+ vte_terminal_set_font(terminal, font_desc);
+}
+
+static const gchar *
+nemo_terminal_widget_get_color_scheme(NemoTerminalWidget *self)
+{
+ NemoTerminalWidgetPrivate *priv = self->priv;
+ if (priv->color_scheme == NULL) {
+ priv->color_scheme = g_settings_get_string(nemo_window_state, "terminal-color-scheme");
+ }
+ return priv->color_scheme;
+}
+
+static void
+nemo_terminal_widget_set_color_scheme(NemoTerminalWidget *self, const gchar *scheme)
+{
+ NemoTerminalWidgetPrivate *priv = self->priv;
+ g_settings_set_string(nemo_window_state, "terminal-color-scheme", scheme);
+ g_free(priv->color_scheme);
+ priv->color_scheme = g_strdup(scheme);
+ nemo_terminal_widget_apply_color_scheme(self);
+}
+
+static void
+nemo_terminal_widget_apply_color_scheme(NemoTerminalWidget *self)
+{
+ NemoTerminalWidgetPrivate *priv = self->priv;
+ const gchar *scheme_id = nemo_terminal_widget_get_color_scheme(self);
+ const MenuSchemeEntry *scheme = NULL;
+ GtkStyleContext *context;
+ GdkRGBA fg, bg;
+
+ for (gsize i = 0; i < G_N_ELEMENTS(COLOR_SCHEME_ENTRIES); ++i) {
+ if (g_strcmp0(COLOR_SCHEME_ENTRIES[i].id, scheme_id) == 0) {
+ scheme = &COLOR_SCHEME_ENTRIES[i];
+ break;
+ }
+ }
+
+ if (scheme == NULL) { /* Fallback to system */
+ scheme = &COLOR_SCHEME_ENTRIES[0];
+ }
+
+ if (scheme->palette.use_system_colors) {
+ context = gtk_widget_get_style_context(GTK_WIDGET(priv->terminal));
+ gtk_style_context_get_color(context, gtk_widget_get_state_flags(GTK_WIDGET(priv->terminal)), &fg);
+ gtk_style_context_get_background_color(context, gtk_widget_get_state_flags(GTK_WIDGET(priv->terminal)), &bg);
+ vte_terminal_set_colors(priv->terminal, &fg, &bg, NULL, 0);
+ } else {
+ vte_terminal_set_colors(priv->terminal,
+ &scheme->palette.foreground,
+ &scheme->palette.background,
+ (GdkRGBA *)scheme->palette.palette,
+ G_N_ELEMENTS(scheme->palette.palette));
+ }
+}
+
+NemoTerminalWidget *
+nemo_terminal_widget_new(void)
+{
+ return g_object_new(NEMO_TYPE_TERMINAL_WIDGET, NULL);
+}
+
+NemoTerminalWidget *
+nemo_terminal_widget_new_with_location(GFile *location)
+{
+ return g_object_new(NEMO_TYPE_TERMINAL_WIDGET, "current-location", location, NULL);
+}
+
+void
+nemo_terminal_widget_set_current_location(NemoTerminalWidget *self, GFile *location)
+{
+ NemoTerminalWidgetPrivate *priv;
+ g_autofree gchar *hostname = NULL, *username = NULL, *port = NULL;
+ g_autofree gchar *scheme = NULL;
+
+ g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self));
+ priv = self->priv;
+
+ if (location != NULL && (priv->current_location == NULL || !g_file_equal(location, priv->current_location)))
+ {
+ g_set_object(&priv->current_location, location);
+ g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_CURRENT_LOCATION]);
+ }
+
+ if (location) {
+ scheme = g_file_get_uri_scheme(location);
+ }
+
+ if (priv->state == NEMO_TERMINAL_STATE_IN_SSH) {
+ if (scheme && g_strcmp0(scheme, "sftp") == 0) {
+ if (parse_gvfs_ssh_path(location, &hostname, &username, &port) &&
+ g_strcmp0(hostname, priv->ssh_hostname) == 0) {
+ change_directory_in_terminal(self, location);
+ } else {
+ _reset_to_local_state(self);
+ spawn_terminal_async(self);
+ }
+ } else {
+ _reset_to_local_state(self);
+ spawn_terminal_async(self);
+ }
+ } else { /* Local state */
+ if (scheme && g_strcmp0(scheme, "sftp") == 0 &&
+ priv->ssh_auto_connect_mode != NEMO_TERMINAL_SSH_AUTOCONNECT_OFF &&
+ parse_gvfs_ssh_path(location, &hostname, &username, &port)) {
+
+ NemoTerminalSyncMode sync_mode;
+ switch (priv->ssh_auto_connect_mode) {
+ case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH: sync_mode = NEMO_TERMINAL_SYNC_BOTH; break;
+ case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM: sync_mode = NEMO_TERMINAL_SYNC_FM_TO_TERM; break;
+ case NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM: sync_mode = NEMO_TERMINAL_SYNC_TERM_TO_FM; break;
+ default: sync_mode = NEMO_TERMINAL_SYNC_NONE; break;
+ }
+
+ if (priv->child_pid != -1) {
+ _initiate_ssh_connection(self, hostname, username, port, sync_mode);
+ } else {
+ priv->pending_ssh_hostname = g_strdup(hostname);
+ priv->pending_ssh_username = g_strdup(username);
+ priv->pending_ssh_port = g_strdup(port);
+ priv->pending_ssh_sync_mode = sync_mode;
+ }
+ } else {
+ change_directory_in_terminal(self, location);
+ }
+ }
+}
+
+void
+nemo_terminal_widget_set_container_paned(NemoTerminalWidget *self, GtkWidget *paned)
+{
+ NemoTerminalWidgetPrivate *priv;
+ g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self));
+ priv = self->priv;
+ g_weak_ref_set(&priv->paned_weak_ref, paned);
+}
+
+void
+nemo_terminal_widget_toggle_visible(NemoTerminalWidget *self)
+{
+ NemoTerminalWidgetPrivate *priv;
+ g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self));
+ priv = self->priv;
+
+ priv->is_visible = !priv->is_visible;
+ g_settings_set_boolean(nemo_window_state, "terminal-visible", priv->is_visible);
+
+ if (!priv->is_visible) {
+ g_autoptr(GtkWidget) paned = g_weak_ref_get(&priv->paned_weak_ref);
+ if (paned && GTK_IS_PANED(paned)) {
+ int position = gtk_paned_get_position(GTK_PANED(paned));
+ int total_height = gtk_widget_get_allocated_height(paned);
+ if (total_height > 0) {
+ g_settings_set_int(nemo_window_state, "terminal-pane-size", total_height - position);
+ }
+ }
+ }
+
+ nemo_terminal_widget_ensure_state(self);
+ g_signal_emit(self, signals[TOGGLE_VISIBILITY], 0, priv->is_visible);
+}
+
+void
+nemo_terminal_widget_ensure_state(NemoTerminalWidget *self)
+{
+ NemoTerminalWidgetPrivate *priv;
+ g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self));
+ priv = self->priv;
+
+ priv->is_visible = g_settings_get_boolean(nemo_window_state, "terminal-visible");
+
+ if (priv->is_visible) {
+ if (priv->needs_respawn) {
+ spawn_terminal_async(self);
+ }
+ gtk_widget_show(GTK_WIDGET(self));
+ nemo_terminal_widget_ensure_terminal_focus(self);
+ } else {
+ gtk_widget_hide(GTK_WIDGET(self));
+ }
+}
+
+static gboolean
+_ensure_focus_timeout(gpointer user_data)
+{
+ NemoTerminalWidget *self = user_data;
+ NemoTerminalWidgetPrivate *priv = self->priv;
+ priv->focus_timeout_id = 0;
+ if (priv->is_visible) {
+ gtk_widget_grab_focus(GTK_WIDGET(priv->terminal));
+ }
+ return G_SOURCE_REMOVE;
+}
+
+void
+nemo_terminal_widget_ensure_terminal_focus(NemoTerminalWidget *self)
+{
+ NemoTerminalWidgetPrivate *priv;
+ g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self));
+ priv = self->priv;
+
+ if (priv->focus_timeout_id > 0) {
+ g_source_remove(priv->focus_timeout_id);
+ }
+ priv->focus_timeout_id = g_timeout_add(100, _ensure_focus_timeout, self);
+}
+
+gboolean
+nemo_terminal_widget_get_visible(NemoTerminalWidget *self)
+{
+ g_return_val_if_fail(NEMO_IS_TERMINAL_WIDGET(self), FALSE);
+ return self->priv->is_visible;
+}
+
+void
+nemo_terminal_widget_apply_new_size(NemoTerminalWidget *self)
+{
+ NemoTerminalWidgetPrivate *priv;
+ g_autoptr(GtkWidget) paned = NULL;
+ gint term_size;
+
+ g_return_if_fail(NEMO_IS_TERMINAL_WIDGET(self));
+ priv = self->priv;
+
+ paned = g_weak_ref_get(&priv->paned_weak_ref);
+ if (!paned || !GTK_IS_PANED(paned) || !gtk_widget_get_realized(paned)) {
+ return;
+ }
+
+ if (priv->is_visible) {
+ gint total_height = gtk_widget_get_allocated_height(paned);
+ term_size = g_settings_get_int(nemo_window_state, "terminal-pane-size");
+ if (term_size <= 0) {
+ term_size = 200; /* Default fallback */
+ }
+ gtk_paned_set_position(GTK_PANED(paned), total_height - term_size);
+ }
+}
diff --git a/src/nemo-terminal-widget.h b/src/nemo-terminal-widget.h
new file mode 100644
index 000000000..b5517d845
--- /dev/null
+++ b/src/nemo-terminal-widget.h
@@ -0,0 +1,108 @@
+/* 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
+
+/**
+ * NemoTerminalSyncMode:
+ * @NEMO_TERMINAL_SYNC_NONE: No synchronization between file manager and terminal.
+ * @NEMO_TERMINAL_SYNC_FM_TO_TERM: File manager navigation changes the terminal's directory.
+ * @NEMO_TERMINAL_SYNC_TERM_TO_FM: Terminal `cd` commands change the file manager's location.
+ * @NEMO_TERMINAL_SYNC_BOTH: Synchronization is bidirectional.
+ *
+ * Defines the synchronization behavior for the terminal's current directory.
+ */
+typedef enum
+{
+ NEMO_TERMINAL_SYNC_NONE,
+ NEMO_TERMINAL_SYNC_FM_TO_TERM,
+ NEMO_TERMINAL_SYNC_TERM_TO_FM,
+ NEMO_TERMINAL_SYNC_BOTH
+} NemoTerminalSyncMode;
+
+/**
+ * NemoTerminalSshAutoConnectMode:
+ * @NEMO_TERMINAL_SSH_AUTOCONNECT_OFF: Do not automatically connect to SSH when navigating to an SFTP location.
+ * @NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_BOTH: Automatically connect and sync both ways.
+ * @NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_FM_TO_TERM: Automatically connect and sync from file manager to terminal.
+ * @NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_TERM_TO_FM: Automatically connect and sync from terminal to file manager.
+ * @NEMO_TERMINAL_SSH_AUTOCONNECT_SYNC_NONE: Automatically connect but do not sync directories.
+ *
+ * Defines the auto-connection behavior when the file manager navigates to an SFTP location.
+ */
+typedef enum
+{
+ NEMO_TERMINAL_SSH_AUTOCONNECT_OFF,
+ 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 _NemoTerminalWidgetPrivate NemoTerminalWidgetPrivate;
+
+struct _NemoTerminalWidget
+{
+ GtkBox parent_instance;
+ NemoTerminalWidgetPrivate *priv;
+};
+
+struct _NemoTerminalWidgetClass
+{
+ GtkBoxClass parent_class;
+};
+
+GType nemo_terminal_widget_get_type(void) G_GNUC_CONST;
+
+NemoTerminalWidget *nemo_terminal_widget_new(void);
+NemoTerminalWidget *nemo_terminal_widget_new_with_location(GFile *location);
+
+void nemo_terminal_widget_set_current_location(NemoTerminalWidget *self,
+ GFile *location);
+void nemo_terminal_widget_set_container_paned(NemoTerminalWidget *self,
+ GtkWidget *paned);
+
+void nemo_terminal_widget_toggle_visible(NemoTerminalWidget *self);
+void nemo_terminal_widget_ensure_state(NemoTerminalWidget *self);
+void nemo_terminal_widget_ensure_terminal_focus(NemoTerminalWidget *self);
+
+gboolean nemo_terminal_widget_get_visible(NemoTerminalWidget *self);
+
+void nemo_terminal_widget_apply_new_size(NemoTerminalWidget *self);
+
+G_END_DECLS
+
+#endif /* __NEMO_TERMINAL_WIDGET_H__ */
diff --git a/src/nemo-window-manage-views.c b/src/nemo-window-manage-views.c
index 96c2e575a..213cd1c9d 100644
--- a/src/nemo-window-manage-views.c
+++ b/src/nemo-window-manage-views.c
@@ -1558,6 +1558,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..9836fb5fc 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);
+}
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..5904d7bbd 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
@@ -343,6 +344,8 @@ nemo_window_slot_init (NemoWindowSlot *slot)
G_CALLBACK (floating_bar_action_cb), slot);
slot->cache_bar = NULL;
+ slot->terminal_widget = NULL;
+ slot->terminal_visible = FALSE;
slot->title = g_strdup (_("Loading..."));
}
@@ -653,6 +656,15 @@ 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) {
+ /* Defer terminal initialization to an idle callback. This ensures that the main
+ * window and its widgets have been allocated sizes before we attempt to create
+ * and size the terminal pane, preventing race conditions and sizing issues on startup. */
+ g_idle_add((GSourceFunc)nemo_window_slot_ensure_terminal_state, slot);
+ }
}
}
@@ -959,3 +971,153 @@ nemo_window_slot_new (NemoWindowPane *pane)
return slot;
}
+
+static void
+on_terminal_visibility_changed(NemoTerminalWidget *terminal,
+ gboolean visible,
+ NemoWindowSlot *slot)
+{
+ slot->terminal_visible = visible;
+}
+
+static void
+on_terminal_directory_changed(NemoTerminalWidget *terminal,
+ GFile *location,
+ NemoWindowSlot *slot)
+{
+ if (location != NULL) {
+ nemo_window_slot_open_location(slot, location, 0);
+ }
+}
+
+static void
+on_paned_size_allocated (GtkWidget *paned, GtkAllocation *allocation, gpointer user_data)
+{
+ NemoTerminalWidget *terminal = NEMO_TERMINAL_WIDGET (user_data);
+ nemo_terminal_widget_apply_new_size (terminal);
+ /* Disconnect after the first call to avoid re-applying the size on every allocation. */
+ g_signal_handlers_disconnect_by_func (paned, on_paned_size_allocated, terminal);
+}
+
+static gboolean
+on_paned_button_release (GtkWidget *paned, GdkEventButton *event, gpointer user_data)
+{
+ int position = gtk_paned_get_position (GTK_PANED (paned));
+ int total_height = gtk_widget_get_allocated_height (paned);
+
+ if (total_height > 0)
+ {
+ int height = total_height - position;
+ g_settings_set_int (nemo_window_state, "terminal-pane-size", height);
+ }
+
+ return FALSE;
+}
+
+/*
+ * _initialize_terminal_in_paned:
+ * @slot: The #NemoWindowSlot to add the terminal to.
+ *
+ * This function performs a delicate re-parenting of widgets to insert the
+ * terminal. It takes the existing view_overlay, removes it from its parent,
+ * creates a new GtkPaned, and places the view_overlay in the top pane and
+ * the new terminal widget in the bottom pane. This new GtkPaned is then
+ * inserted back into the original parent of the view_overlay.
+ */
+static void
+_initialize_terminal_in_paned(NemoWindowSlot *slot)
+{
+ GtkWidget *paned_container;
+ GtkWidget *parent_of_overlay;
+ gint position;
+ GList *children;
+
+ parent_of_overlay = gtk_widget_get_parent(slot->view_overlay);
+ if (!GTK_IS_BOX(parent_of_overlay)) {
+ g_warning("Cannot initialize terminal in paned: parent of view_overlay is not a GtkBox.");
+ return;
+ }
+
+ children = gtk_container_get_children(GTK_CONTAINER(parent_of_overlay));
+ position = g_list_index(children, slot->view_overlay);
+ g_list_free(children);
+
+ paned_container = gtk_paned_new(GTK_ORIENTATION_VERTICAL);
+
+ g_object_ref(slot->view_overlay);
+ gtk_container_remove(GTK_CONTAINER(parent_of_overlay), slot->view_overlay);
+
+ gtk_paned_pack1(GTK_PANED(paned_container), slot->view_overlay, TRUE, TRUE);
+ g_object_unref(slot->view_overlay);
+
+ gtk_paned_pack2(GTK_PANED(paned_container), GTK_WIDGET(slot->terminal_widget), FALSE, TRUE);
+
+ gtk_box_pack_start(GTK_BOX(parent_of_overlay), paned_container, TRUE, TRUE, 0);
+ if (position != -1) {
+ gtk_box_reorder_child(GTK_BOX(parent_of_overlay), paned_container, position);
+ }
+
+ g_signal_connect (paned_container, "size-allocate", G_CALLBACK (on_paned_size_allocated), slot->terminal_widget);
+ g_signal_connect (paned_container, "button-release-event", G_CALLBACK (on_paned_button_release), NULL);
+
+ gtk_widget_show_all(paned_container);
+
+ nemo_terminal_widget_set_container_paned(slot->terminal_widget, paned_container);
+}
+
+void
+nemo_window_slot_init_terminal (NemoWindowSlot *slot)
+{
+ if (slot->terminal_widget != NULL) {
+ return;
+ }
+
+ slot->terminal_widget = nemo_terminal_widget_new_with_location(slot->location);
+
+ g_signal_connect(slot->terminal_widget, "toggle-visibility",
+ G_CALLBACK(on_terminal_visibility_changed), slot);
+ g_signal_connect(slot->terminal_widget, "change-directory",
+ G_CALLBACK(on_terminal_directory_changed), slot);
+
+ _initialize_terminal_in_paned(slot);
+}
+
+void
+nemo_window_slot_toggle_terminal (NemoWindowSlot *slot)
+{
+ if (slot->terminal_widget == NULL) {
+ nemo_window_slot_init_terminal(slot);
+ }
+
+ if (slot->terminal_widget != NULL) {
+ nemo_terminal_widget_toggle_visible(slot->terminal_widget);
+ }
+}
+
+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);
+ }
+}
+
+gboolean
+nemo_window_slot_ensure_terminal_state (gpointer user_data)
+{
+ NemoWindowSlot *slot = user_data;
+ gboolean terminal_should_be_visible = g_settings_get_boolean(nemo_window_state, "terminal-visible");
+
+ if (terminal_should_be_visible) {
+ if (slot->terminal_widget == NULL) {
+ nemo_window_slot_init_terminal(slot);
+ }
+ nemo_terminal_widget_ensure_state(slot->terminal_widget);
+ nemo_window_slot_update_terminal_location(slot);
+ } else {
+ if (slot->terminal_widget != NULL) {
+ nemo_terminal_widget_ensure_state(slot->terminal_widget);
+ }
+ }
+ return G_SOURCE_REMOVE;
+}
\ No newline at end of file
diff --git a/src/nemo-window-slot.h b/src/nemo-window-slot.h
index a79cbb4e7..e875bd125 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,10 @@ struct NemoWindowSlot {
GtkWidget *cache_bar;
GtkWidget *no_search_results_box;
+ /* Terminal pane */
+ NemoTerminalWidget *terminal_widget;
+ gboolean terminal_visible;
+
guint set_status_timeout_id;
guint loading_timeout_id;
@@ -191,4 +196,9 @@ void nemo_window_slot_check_bad_cache_bar (NemoWindowSlot *slot);
void nemo_window_slot_set_show_thumbnails (NemoWindowSlot *slot,
gboolean show_thumbnails);
-#endif /* NEMO_WINDOW_SLOT_H */
+
+void nemo_window_slot_toggle_terminal (NemoWindowSlot *slot);
+void nemo_window_slot_update_terminal_location (NemoWindowSlot *slot);
+gboolean nemo_window_slot_ensure_terminal_state (gpointer user_data);
+
+#endif /* NEMO_WINDOW_SLOT_H */
\ No newline at end of file