Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/app_layout.c
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,12 @@ static void action_tool_cut(GtkWidget *widget, gpointer user_data) {
*/
static void action_tool_copy(GtkWidget *widget, gpointer user_data) {
(void)widget;
EditorWindow *win = user_data;
if (win && terminal_panel_has_focus(win->terminal_panel) &&
terminal_panel_copy_clipboard(win->terminal_panel)) {
return;
}

EditorTab *tab = tool_current_tab(user_data);
if (tab) editor_tab_copy_clipboard(tab);
}
Expand All @@ -330,6 +336,12 @@ static void action_tool_copy(GtkWidget *widget, gpointer user_data) {
*/
static void action_tool_paste(GtkWidget *widget, gpointer user_data) {
(void)widget;
EditorWindow *win = user_data;
if (win && terminal_panel_has_focus(win->terminal_panel) &&
terminal_panel_paste_clipboard(win->terminal_panel)) {
return;
}

EditorTab *tab = tool_current_tab(user_data);
if (tab) editor_tab_paste_clipboard(tab);
}
Expand Down
94 changes: 94 additions & 0 deletions src/terminal_panel.c
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,23 @@ static void terminal_panel_close_session(TerminalPanel *panel,
static void terminal_panel_finish_close_session(TerminalPanel *panel,
TerminalSession *session);

/**
* @brief Handle terminal-local shortcuts.
* @details The terminal keeps Ctrl+C for the shell. Clipboard actions use the
* common terminal chords so app shortcuts do not swallow shell input.
* @param controller The controller supplied by the caller.
* @param keyval The keyval supplied by the caller.
* @param keycode The keycode supplied by the caller.
* @param state The state supplied by the caller.
* @param user_data The callback context passed through GTK signal data.
* @return TRUE when the terminal handled the key.
*/
static gboolean terminal_session_key_pressed(GtkEventControllerKey *controller,
guint keyval,
guint keycode,
GdkModifierType state,
gpointer user_data);

/**
* @brief Resolve the shell used by the integrated terminal.
* @details Terminal handling crosses GTK widgets, VTE child processes, and project directories. The comment keeps the lifetime rule visible so closing tabs and shell exits do not race each other.
Expand All @@ -130,6 +147,35 @@ static const char *terminal_panel_shell(void) {
return (shell && shell[0] != '\0') ? shell : "/bin/sh";
}

static gboolean terminal_session_key_pressed(GtkEventControllerKey *controller,
guint keyval,
guint keycode,
GdkModifierType state,
gpointer user_data) {
(void)controller;
(void)keycode;

TerminalSession *session = user_data;
guint key = gdk_keyval_to_lower(keyval);
gboolean ctrl = (state & GDK_CONTROL_MASK) != 0;
gboolean shift = (state & GDK_SHIFT_MASK) != 0;

if (ctrl && shift && key == GDK_KEY_c && session &&
session->terminal) {
vte_terminal_copy_clipboard_format(VTE_TERMINAL(session->terminal),
VTE_FORMAT_TEXT);
return TRUE;
}

if (ctrl && shift && key == GDK_KEY_v && session &&
session->terminal) {
vte_terminal_paste_clipboard(VTE_TERMINAL(session->terminal));
return TRUE;
}

return FALSE;
}

/**
* @brief Reap a terminal child that outlived its VTE widget.
* @details Closing Graptoς can race an async terminal spawn completion. Once the
Expand Down Expand Up @@ -627,6 +673,10 @@ static TerminalSession *terminal_session_new(TerminalPanel *panel,
vte_terminal_set_scrollback_lines(VTE_TERMINAL(session->terminal), 10000);
vte_terminal_set_scroll_on_output(VTE_TERMINAL(session->terminal), FALSE);
vte_terminal_set_scroll_on_keystroke(VTE_TERMINAL(session->terminal), TRUE);
GtkEventController *keys = gtk_event_controller_key_new();
g_signal_connect(keys, "key-pressed",
G_CALLBACK(terminal_session_key_pressed), session);
gtk_widget_add_controller(session->terminal, keys);
g_signal_connect(session->terminal, "child-exited",
G_CALLBACK(terminal_session_child_exited), session);
terminal_session_apply_colors(session);
Expand Down Expand Up @@ -922,6 +972,50 @@ void terminal_panel_apply_colors(TerminalPanel *panel) {
}
}

/**
* @brief Return whether a terminal owns keyboard focus.
* @details Toolbar actions are shared by editor and terminal widgets. This check
* lets copy route to VTE only when the user is working in a terminal.
* @param panel The panel instance affected by the operation.
* @return TRUE when the current terminal should receive focused actions.
*/
gboolean terminal_panel_has_focus(TerminalPanel *panel) {
TerminalSession *session = terminal_panel_current_session(panel);
return session && session->terminal &&
gtk_widget_has_focus(session->terminal);
}

/**
* @brief Copy the current terminal selection.
* @details VTE owns terminal selection state and clipboard formatting. The
* caller decides whether terminal focus should make this action active.
* @param panel The panel instance affected by the operation.
* @return TRUE when a terminal was available to receive the copy request.
*/
gboolean terminal_panel_copy_clipboard(TerminalPanel *panel) {
TerminalSession *session = terminal_panel_current_session(panel);
if (!session || !session->terminal) return FALSE;

vte_terminal_copy_clipboard_format(VTE_TERMINAL(session->terminal),
VTE_FORMAT_TEXT);
return TRUE;
}

/**
* @brief Paste clipboard text into the current terminal.
* @details VTE owns paste filtering and bracketed-paste behavior. This wrapper
* lets the app's Paste action stay focused on routing.
* @param panel The panel instance affected by the operation.
* @return TRUE when a terminal was available to receive the paste request.
*/
gboolean terminal_panel_paste_clipboard(TerminalPanel *panel) {
TerminalSession *session = terminal_panel_current_session(panel);
if (!session || !session->terminal) return FALSE;

vte_terminal_paste_clipboard(VTE_TERMINAL(session->terminal));
return TRUE;
}

/**
* @brief Create an integrated terminal panel for a window.
* @details Terminal handling crosses GTK widgets, VTE child processes, and project directories. The comment keeps the lifetime rule visible so closing tabs and shell exits do not race each other.
Expand Down
27 changes: 27 additions & 0 deletions src/terminal_panel.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,31 @@ void terminal_panel_sync_to_active_file(TerminalPanel *panel);
*/
void terminal_panel_apply_colors(TerminalPanel *panel);

/**
* @brief Return whether a terminal owns keyboard focus.
* @details Clipboard actions are routed at the app layer, so the panel exposes
* only this small focus check instead of leaking VTE widgets outward.
* @param panel The panel instance affected by the operation.
* @return TRUE when the current terminal should receive focused actions.
*/
gboolean terminal_panel_has_focus(TerminalPanel *panel);

/**
* @brief Copy the current terminal selection.
* @details VTE owns terminal selection state and clipboard formatting. This
* wrapper keeps the copy behavior local to the terminal boundary.
* @param panel The panel instance affected by the operation.
* @return TRUE when a terminal was available to receive the copy request.
*/
gboolean terminal_panel_copy_clipboard(TerminalPanel *panel);

/**
* @brief Paste clipboard text into the current terminal.
* @details VTE performs terminal-safe paste handling itself. The wrapper keeps
* app actions from depending on the terminal widget type.
* @param panel The panel instance affected by the operation.
* @return TRUE when a terminal was available to receive the paste request.
*/
gboolean terminal_panel_paste_clipboard(TerminalPanel *panel);

#endif /* GRAPTOS_TERMINAL_PANEL_H */