Skip to content

Instantly share code, notes, and snippets.

@artandmath
Last active March 12, 2026 06:47
Show Gist options
  • Select an option

  • Save artandmath/22d9bf83128c7a73cca3136506c4d559 to your computer and use it in GitHub Desktop.

Select an option

Save artandmath/22d9bf83128c7a73cca3136506c4d559 to your computer and use it in GitHub Desktop.
Copy/paste utilities for Nuke that preserve and restore node input connections.
"""
"SideFX Houdini-Style" copy/paste.
https://gist.github.com/artandmath/22d9bf83128c7a73cca3136506c4d559
Copy/paste tools for Nuke that preserve and restore node input connections.
How it works
------------
When copying with input restore, input connection metadata is written to a
temporary ``.nk`` file alongside the standard node data. The system clipboard
receives the normal (unmodified) node text with a single comment prepended:
# copy_with_input_restore: /path/to/temp.nk
Nuke treats lines beginning with ``#`` as comments and ignores them, so the
clipboard is valid for ordinary paste on any installation.
When pasting with input restore, the first clipboard line is inspected. If the
header is present the temp file is loaded into the clipboard, nodes are pasted,
and original input connections are re-established from the embedded metadata.
The clipboard is then restored to its previous state (header + normal .nk text).
If no header is present the clipboard is pasted as-is with no extra processing.
Menu integration overrides ``Ctrl+X`` with the tracking cut, ``Ctrl+C`` with
the tracking copy, and ``Ctrl+V`` with :func:`paste_without_input_restore`
(strips knobs, no reconnection). Additional paste variants are grouped under
``Edit > Paste Special``:
- ``Paste and Restore Inputs`` (Alt+Shift+V) — paste and reconnect original inputs.
- ``Paste to Selected`` (Ctrl+Shift+V) — pastes the clipboard once per
originally-selected node, connecting the pasted nodes to each target in turn.
- ``Paste to Selected and Restore Inputs`` (Ctrl+Alt+Shift+V) — same as above
but also restores original input connections after each individual paste.
"""
TRACKING_HEADER_PREFIX = "# copy_with_input_restore: "
# Keyboard shortcuts — override any of these before the menu register functions are called.
# HOTKEY_CUT = "Ctrl+X"
# HOTKEY_COPY = "Ctrl+C"
# HOTKEY_PASTE = "Ctrl+V"
# HOTKEY_PASTE_AND_RESTORE_INPUTS = "Alt+Shift+V"
# HOTKEY_PASTE_TO_SELECTED = "Ctrl+Shift+V"
# HOTKEY_PASTE_TO_SELECTED_AND_RESTORE = "Ctrl+Alt+Shift+V"
import re
import tempfile
import nuke
try:
from PySide6 import QtWidgets
except ImportError:
from PySide2 import QtWidgets
def copy_with_input_restore():
"""Copy selected nodes with input-connection metadata written to a temp file.
For each selected node, the indices and names of its connected inputs are
serialised into a hidden ``addUserKnob`` entry injected into the ``.nk`` text
and saved to a temporary file. The system clipboard receives the normal
(unmodified) node text prefixed with a single comment line:
# copy_with_input_restore: /path/to/temp.nk
Nuke ignores comment lines, so the clipboard remains valid for ordinary
paste on installations that do not have this toolset. A subsequent call to
:func:`paste_and_restore_inputs` reads the header, loads the temp file, and
reconnects the nodes automatically.
Does nothing when no nodes are selected.
"""
sel = nuke.selectedNodes()
if not sel:
return
input_map = {}
for node in sel:
inputs = []
for i in range(node.inputs()):
inp = node.input(i)
if inp:
inputs.append(f"{i}:{inp.name()}")
input_map[node.name()] = "\\n".join(inputs)
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".nk")
path = tmp.name
tmp.close()
nuke.nodeCopy(path)
with open(path, "r") as f:
nk = f.read()
lines = nk.split("\n")
node_name = None
new_lines = []
for line in lines:
name_match = re.match(r"\s*name\s+(\S+)", line)
if name_match:
node_name = name_match.group(1)
if line.strip() == "}" and node_name in input_map:
inputs_text = input_map[node_name]
new_lines.append(" addUserKnob {20 original_inputs_tracking_tab}")
new_lines.append(
f' addUserKnob {{26 original_inputs_tracking_knob -STARTLINE T "{inputs_text}"}}'
)
node_name = None
new_lines.append(line)
with open(path, "w") as f:
f.write("\n".join(new_lines))
nuke.nodeCopy("%clipboard%")
clipboard = QtWidgets.QApplication.clipboard()
normal_nk = clipboard.text()
clipboard.setText(f"{TRACKING_HEADER_PREFIX}{path}\n{normal_nk}")
def cut_with_input_restore():
"""Copy selected nodes with input-connection metadata, then delete them.
Delegates to :func:`copy_with_input_restore` to write the temp file and
prepare the clipboard, then deletes every originally-selected node.
Does nothing when no nodes are selected.
"""
sel = nuke.selectedNodes()
if not sel:
return
copy_with_input_restore()
for node in sel:
nuke.delete(node)
def _strip_tracking_knobs(node):
"""Remove input-tracking knobs from *node* if present.
Removes ``original_inputs_tracking_tab`` first (so the tab is gone before
the child knob), then ``original_inputs_tracking_knob``. Safe to call on
nodes that carry neither knob.
"""
if "original_inputs_tracking_knob" in node.knobs():
node.removeKnob(node["original_inputs_tracking_knob"])
if "original_inputs_tracking_tab" in node.knobs():
node.removeKnob(node["original_inputs_tracking_tab"])
def _prepare_clipboard_for_paste():
"""Check the clipboard for an input-tracking header and swap in the temp file.
If the clipboard's first line matches ``# copy_with_input_restore: <path>``,
the temp file at *path* is loaded into the clipboard so that
``nuke.nodePaste("%clipboard%")`` will receive nodes with tracking knobs
embedded. Returns ``True`` when the swap was performed, ``False`` when the
clipboard contains no such header and should be pasted as-is.
"""
clipboard = QtWidgets.QApplication.clipboard()
text = clipboard.text()
first_line = text.split("\n", 1)[0]
if not first_line.startswith(TRACKING_HEADER_PREFIX):
return False
path = first_line[len(TRACKING_HEADER_PREFIX):].strip()
try:
with open(path, "r") as f:
nk = f.read()
except OSError:
return False
clipboard.setText(nk)
return True
def _restore_inputs(nodes):
"""Restore input connections from tracking metadata for a list of *nodes*.
For each node that carries an ``original_inputs_tracking_knob`` knob, the
encoded connection data is parsed and each input is reconnected to the named
source node (if that node exists in the current script and the input slot is
not already occupied). Tracking knobs are removed after processing.
"""
for node in nodes:
if "original_inputs_tracking_knob" not in node.knobs():
_strip_tracking_knobs(node)
continue
data = node["original_inputs_tracking_knob"].value().strip()
if not data:
_strip_tracking_knobs(node)
continue
for line in data.split("\n"):
try:
idx_str, input_name = line.split(":", 1)
idx = int(idx_str)
except ValueError:
continue
if node.input(idx):
continue
target = nuke.toNode(input_name)
if target:
node.setInput(idx, target)
_strip_tracking_knobs(node)
def paste_and_restore_inputs():
"""Paste nodes from the clipboard and restore their original input connections.
If the clipboard begins with a ``# copy_with_input_restore:`` header the
associated temp file (which carries the tracking knobs) is loaded into the
clipboard first, then a standard paste is performed and
:func:`_restore_inputs` reconnects inputs and strips the tracking knobs.
The clipboard is restored to its original state (the header line + normal
.nk text) after the operation completes.
If no header is present the clipboard is pasted as-is with no further
processing, matching ordinary Nuke paste behaviour.
Does nothing when the clipboard yields no new nodes.
"""
clipboard = QtWidgets.QApplication.clipboard()
original_clipboard = clipboard.text()
has_tracking = _prepare_clipboard_for_paste()
for n in nuke.allNodes():
n["selected"].setValue(False)
nuke.nodePaste("%clipboard%")
pasted = nuke.selectedNodes()
if pasted and has_tracking:
_restore_inputs(pasted)
if has_tracking:
clipboard.setText(original_clipboard)
def paste_without_input_restore():
"""Paste nodes from the clipboard and strip input-tracking knobs without reconnecting.
If the clipboard begins with a ``# copy_with_input_restore:`` header the
associated temp file is loaded into the clipboard first so that the tracking
knobs are present and can be cleaned up after the paste. No attempt is made
to restore input connections. The clipboard is restored to its original
state (the header line + normal .nk text) after the operation completes.
If no header is present the clipboard is pasted as-is with no further
processing (there are no tracking knobs to clean up).
Does nothing when the clipboard yields no new nodes.
"""
clipboard = QtWidgets.QApplication.clipboard()
original_clipboard = clipboard.text()
has_tracking = _prepare_clipboard_for_paste()
for n in nuke.allNodes():
n["selected"].setValue(False)
nuke.nodePaste("%clipboard%")
pasted = nuke.selectedNodes()
if has_tracking:
for node in pasted:
_strip_tracking_knobs(node)
clipboard.setText(original_clipboard)
def paste_to_selected():
"""Paste clipboard nodes once per originally-selected node, then re-select all pasted nodes.
Captures the current selection, deselects everything, then iterates over
each originally-selected node. For each one, that node is selected in
isolation and a paste is performed so that Nuke connects the incoming nodes
to it. All pasted nodes are collected across every iteration. Once the
loop is complete, only the pasted nodes are selected.
If the clipboard begins with a ``# copy_with_input_restore:`` header the
temp file is loaded into the clipboard before the loop so that tracking
knobs are available to clean up. If no header is present, nodes are pasted
as-is with no extra processing.
Does nothing when no nodes are selected.
"""
original_selection = nuke.selectedNodes()
if not original_selection:
return
has_tracking = _prepare_clipboard_for_paste()
for n in nuke.allNodes():
n["selected"].setValue(False)
all_pasted = []
for target in original_selection:
target["selected"].setValue(True)
nuke.nodePaste("%clipboard%")
pasted = nuke.selectedNodes()
all_pasted.extend(pasted)
for n in nuke.allNodes():
n["selected"].setValue(False)
for node in all_pasted:
if has_tracking:
_strip_tracking_knobs(node)
node["selected"].setValue(True)
def paste_to_selected_and_restore_inputs():
"""Paste clipboard nodes once per originally-selected node and restore input connections.
Identical to :func:`paste_to_selected` except that after each individual
paste the input-connection metadata embedded by :func:`copy_with_input_restore`
is read and the connections are re-established before moving on to the next
target node.
If the clipboard begins with a ``# copy_with_input_restore:`` header the
temp file is loaded into the clipboard before the loop and the clipboard is
restored to its original state (the header line + normal .nk text) after
all pastes complete. If no header is present, nodes are pasted as-is and
no input restore is attempted.
Does nothing when no nodes are selected.
"""
original_selection = nuke.selectedNodes()
if not original_selection:
return
clipboard = QtWidgets.QApplication.clipboard()
original_clipboard = clipboard.text()
has_tracking = _prepare_clipboard_for_paste()
for n in nuke.allNodes():
n["selected"].setValue(False)
all_pasted = []
for target in original_selection:
target["selected"].setValue(True)
nuke.nodePaste("%clipboard%")
pasted = nuke.selectedNodes()
if has_tracking:
_restore_inputs(pasted)
all_pasted.extend(pasted)
for n in nuke.allNodes():
n["selected"].setValue(False)
for node in all_pasted:
node["selected"].setValue(True)
if has_tracking:
clipboard.setText(original_clipboard)
# ---------------------------------------------------------------------------
# Menu integration
# ---------------------------------------------------------------------------
def _register_menu():
"""Register menu commands.
Overrides:
- ``Edit > Cut`` (Ctrl+X) — cut with tracking metadata.
- ``Edit > Copy`` (Ctrl+C) — copy with tracking metadata.
- ``Edit > Paste`` (Ctrl+V) — paste; strips tracking knobs but does not
restore connections.
Adds ``Edit > Paste Special`` immediately after Paste, containing:
- ``Paste and Restore Inputs`` (Alt+Shift+V) — paste with full reconnection.
- ``Paste to Selected`` (Ctrl+Shift+V) — paste once per selected node.
- ``Paste to Selected and Restore Inputs`` (Ctrl+Alt+Shift+V) — paste once
per selected node and restore input connections.
"""
menu = nuke.menu("Nuke")
edit_menu = menu.findItem("Edit")
cut_index = None
copy_index = None
paste_index = None
for i, item in enumerate(edit_menu.items()):
name = item.name()
if name == "Cut":
cut_index = i
elif name == "Copy":
copy_index = i
elif name == "Paste":
paste_index = i
if cut_index is not None:
menu.addCommand(
"Edit/Cut",
"cut_with_input_restore()",
globals().get("HOTKEY_CUT", "Ctrl+X"),
)
if copy_index is not None:
menu.addCommand(
"Edit/Copy",
"copy_with_input_restore()",
globals().get("HOTKEY_COPY", "Ctrl+C"),
)
if paste_index is not None:
menu.addCommand(
"Edit/Paste",
"paste_without_input_restore()",
globals().get("HOTKEY_PASTE", "Ctrl+V"),
)
menu.addMenu(
"Edit/Paste Special",
index=paste_index + 1,
)
menu.addCommand(
"Edit/Paste Special/Paste and Restore Inputs",
"paste_and_restore_inputs()",
globals().get("HOTKEY_PASTE_AND_RESTORE_INPUTS", "Alt+Shift+V"),
)
menu.addCommand(
"Edit/Paste Special/Paste to Selected",
"paste_to_selected()",
globals().get("HOTKEY_PASTE_TO_SELECTED", "Ctrl+Shift+V"),
)
menu.addCommand(
"Edit/Paste Special/Paste to Selected and Restore Inputs",
"paste_to_selected_and_restore_inputs()",
globals().get("HOTKEY_PASTE_TO_SELECTED_AND_RESTORE", "Ctrl+Alt+Shift+V"),
)
_register_menu()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment