diff --git a/src/app_layout.c b/src/app_layout.c index 209e160..4e06909 100644 --- a/src/app_layout.c +++ b/src/app_layout.c @@ -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); } @@ -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); } diff --git a/src/terminal_panel.c b/src/terminal_panel.c index fb64109..a9d5aff 100644 --- a/src/terminal_panel.c +++ b/src/terminal_panel.c @@ -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. @@ -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 @@ -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); @@ -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. diff --git a/src/terminal_panel.h b/src/terminal_panel.h index 98822a8..b9f329d 100644 --- a/src/terminal_panel.h +++ b/src/terminal_panel.h @@ -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 */