<?xml version="1.0" encoding="UTF-8"?>
<shelfDocument>
  <!-- Kuukan Bridge shelf.

       Install: save this file into  $HOME/houdiniX.Y/toolbar/  (e.g.
       ~/houdini22.0/toolbar/kuukan.shelf), restart Houdini, then click the
       "+" at the end of the shelf bar and enable "Kuukan" under Shelves.

       One button: Kuukan Panel. Opening it creates the drop folder
       (kuukan_drop in your user home), shows its exact path, and opens a
       floating window - a light that turns green when something new
       arrives, the list of setups, select and Paste (or double-click).
       The Copy button in the panel writes the selection as Houdini's
       native clipboard AND into the drop folder, so plain Ctrl+V works
       locally and the card appears on the open Kuukan canvas.

       The Kuukan side is one click: Profile & Settings -> Integrations ->
       Houdini -> pick that folder. Kuukan verifies the link via the
       .kuukan_bridge marker this shelf stamps, so picking a same-named
       twin folder shows up immediately instead of failing silently.
       Folder-only - no network, no tokens. -->

  <!-- One button on the shelf: the Panel. The copy and paste TOOLS stay
       defined below (the panel's Copy button executes kuukan_copy via
       hou.shelves.tool), they just no longer clutter the strip. -->
  <toolshelf name="kuukan" label="Kuukan">
    <memberTool name="kuukan_panel"/>
  </toolshelf>

  <tool name="kuukan_copy" label="Copy to Kuukan" icon="kuukan.png">
    <helpText><![CDATA[Copy the selected items to the Kuukan canvas (and the native clipboard).]]></helpText>
    <script scriptType="python"><![CDATA[
import hou, os, json, shutil, tempfile

# Bumped when this shelf changes. Kuukan compares the marker's copy against
# its own (SHELF_VERSION in WatchFolder.js) and tells the user to re-download
# -- so a stale installed shelf announces itself instead of mysteriously
# missing features.
SHELF_VERSION = 13

# The REAL user home. Houdini's own HOME points elsewhere on Windows (often
# Documents, or another drive entirely), and two same-named drop folders on
# one machine fail silently in both directions.
_HOME = os.environ.get("USERPROFILE") or os.path.expanduser("~")
DROP_DIR = os.path.join(_HOME, "kuukan_drop")
STUDIO_STORE = None   # e.g. "/cache/copypaste_store" to also feed the studio store


def _ensure_drop():
    """Identical twin in the paste tool -- a shelf file has no shared module.
    Creates the drop folder and stamps the .kuukan_bridge marker: the link's
    calling card. Kuukan reads it to verify "you picked the folder the shelf
    actually writes to", to show the full path, to warn when this shelf is
    outdated, and to tag non-commercial setups on their cards. On first
    creation, shows the exact path to pick."""
    first = not os.path.isdir(DROP_DIR)
    os.makedirs(DROP_DIR, exist_ok=True)
    try:
        with open(os.path.join(DROP_DIR, ".kuukan_bridge"), "w") as f:
            json.dump({
                "path": DROP_DIR,
                "houdini": hou.applicationVersionString(),
                "license": str(hou.licenseCategory()).split(".")[-1],
                "shelf": SHELF_VERSION,
            }, f)
    except Exception:
        pass
    if first:
        hou.ui.displayMessage(
            "Created the Kuukan drop folder:\n\n%s\n\nPick this exact folder in"
            " Kuukan:\nProfile & Settings -> Integrations -> Houdini -> Choose"
            " folder." % DROP_DIR)


_ensure_drop()


def _expand_selection(items):
    """Selected items plus contents of any selected network boxes -- nobody
    means to copy an empty frame."""
    out = list(items)
    seen = set(out)
    stack = [i for i in items if isinstance(i, hou.NetworkBox)]
    while stack:
        box = stack.pop()
        for it in box.items():
            if it not in seen:
                seen.add(it)
                out.append(it)
                if isinstance(it, hou.NetworkBox):
                    stack.append(it)
    return out


# Failures POP UP (displayMessage); successes stay on the quiet status bar.
# A person who just clicked expects something to happen -- a one-line status
# message at the bottom of the window is where bad news goes to be missed.
items = hou.selectedItems()
if not items:
    hou.ui.displayMessage("Select something first.",
                          severity=hou.severityType.Warning,
                          title="Copy to Kuukan")
else:
    parent = items[0].parent()
    if any(i.parent() != parent for i in items):
        hou.ui.displayMessage("The selected items must all be in one network.",
                              severity=hou.severityType.Warning,
                              title="Copy to Kuukan")
    else:
        items = _expand_selection(items)
        # The clipboard prefix is NOT always the category name: /obj is
        # category "Object" but the native clipboard file is OBJ_copy, and
        # ROPs are category "Driver" with a ROP_copy file. Deriving the name
        # straight from the category "succeeds" and pastes nothing at those
        # levels -- the same silent miss the studio's own tool has.
        _cat = parent.childTypeCategory().name().upper()
        context = {"OBJECT": "OBJ", "DRIVER": "ROP"}.get(_cat, _cat)

        # Write the selection AS the native clipboard: saveItemsToFile makes
        # the same cpio container native copy does, and brings network boxes
        # and sticky notes along. The clipboard lives in HOUDINI_TEMP_DIR --
        # on Windows that is $TEMP/houdini_temp, NOT %TEMP% itself -- and
        # non-commercial builds name it .cpionc (Indie: .cpiolc). Write every
        # name the running build might read; guessing wrong makes Ctrl+V
        # paste stale air.
        tmp = hou.getenv("HOUDINI_TEMP_DIR") or tempfile.gettempdir()
        try:
            os.makedirs(tmp, exist_ok=True)
        except Exception:
            pass
        lic = str(hou.licenseCategory()).split(".")[-1]
        ext = {"Apprentice": "cpionc", "ApprenticeHD": "cpionc",
               "Education": "cpionc", "Indie": "cpiolc"}.get(lic, "cpio")
        clip = os.path.join(tmp, "%s_copy.%s" % (context, ext))
        parent.saveItemsToFile(items, clip)
        if ext != "cpio":
            shutil.copyfile(clip, os.path.join(tmp, "%s_copy.cpio" % context))

        # Ask for a name -- the card on the canvas is FOR other people, and
        # "__netbox3" tells them nothing. Default: the first real node in the
        # selection, never a network box's internal name.
        default = next((i.name() for i in items if isinstance(i, hou.Node)),
                       items[0].name())
        choice, text = hou.ui.readInput("Name this setup:",
                                        buttons=("Copy", "Cancel"),
                                        default_choice=0, close_choice=1,
                                        initial_contents=default,
                                        title="Copy to Kuukan")
        if choice != 0:
            hou.ui.setStatusMessage("Kuukan copy cancelled "
                                    "(selection is still on the native clipboard).")
        else:
            # The name becomes a filename -- strip what filesystems refuse.
            name = "".join(c for c in text.strip() if c not in '\\/:*?"<>|').strip() or default

            os.makedirs(DROP_DIR, exist_ok=True)
            # The context travels IN THE FILENAME -- Kuukan reads the badge from it.
            dropped = os.path.join(DROP_DIR, "%s.%s.cpio" % (name, context))
            shutil.copyfile(clip, dropped)
            # Tell the panel this one is OURS -- your own export lighting up
            # the "new arrivals" lamp taught nobody anything.
            try:
                if not hasattr(hou.session, "_kuukan_own"):
                    hou.session._kuukan_own = set()
                hou.session._kuukan_own.add(os.path.basename(dropped))
            except Exception:
                pass

            if STUDIO_STORE:
                user = os.environ.get("USER") or os.environ.get("USERNAME") or "unknown"
                d = os.path.join(STUDIO_STORE, user, context)
                os.makedirs(d, exist_ok=True)
                shutil.copyfile(clip, os.path.join(d, "%s_copy.cpio" % context))

            n = sum(1 for i in items if isinstance(i, hou.Node))
            # Name the folder every time -- the path is otherwise shown only
            # in the one first-run dialog, and "where does this go?" should
            # never require remembering that.
            hou.ui.setStatusMessage("Kuukan copy: %s (%d nodes, %s) -> %s (+ clipboard)"
                                    % (name, n, context, DROP_DIR))
]]></script>
  </tool>

  <tool name="kuukan_paste" label="Paste from Kuukan" icon="kuukan.png">
    <helpText><![CDATA[Paste the newest setup sent from Kuukan (Send to Houdini on a card).]]></helpText>
    <script scriptType="python"><![CDATA[
import hou, os, json, glob, shutil, tempfile
import toolutils

# Bumped when this shelf changes -- see the copy tool's twin comment.
SHELF_VERSION = 13

# The REAL user home. Houdini's own HOME points elsewhere on Windows (often
# Documents, or another drive entirely), and two same-named drop folders on
# one machine fail silently in both directions.
_HOME = os.environ.get("USERPROFILE") or os.path.expanduser("~")
DROP_DIR = os.path.join(_HOME, "kuukan_drop")
PASTE_DIRS = [DROP_DIR, os.path.join(_HOME, "Downloads")]


def _ensure_drop():
    """Identical twin in the copy tool -- a shelf file has no shared module.
    Creates the drop folder and stamps the .kuukan_bridge marker: the link's
    calling card (path + Houdini version + license + shelf version). On first
    creation, shows the exact path to pick."""
    first = not os.path.isdir(DROP_DIR)
    os.makedirs(DROP_DIR, exist_ok=True)
    try:
        with open(os.path.join(DROP_DIR, ".kuukan_bridge"), "w") as f:
            json.dump({
                "path": DROP_DIR,
                "houdini": hou.applicationVersionString(),
                "license": str(hou.licenseCategory()).split(".")[-1],
                "shelf": SHELF_VERSION,
            }, f)
    except Exception:
        pass
    if first:
        hou.ui.displayMessage(
            "Created the Kuukan drop folder:\n\n%s\n\nPick this exact folder in"
            " Kuukan:\nProfile & Settings -> Integrations -> Houdini -> Choose"
            " folder." % DROP_DIR)


_ensure_drop()

# Clipboard semantics on purpose: the newest file wins, always.
newest, newest_t = None, 0
for d in PASTE_DIRS:
    for f in glob.glob(os.path.join(d, "*.cpio")):
        t = os.path.getmtime(f)
        if t > newest_t:
            newest, newest_t = f, t

# Failures POP UP (displayMessage); successes stay on the quiet status bar.
if not newest:
    hou.ui.displayMessage("Nothing to paste yet.\n\nThe drop folder is:\n%s\n\n"
                          "Press \"Send to Houdini\" on a card in Kuukan first."
                          % DROP_DIR,
                          severity=hou.severityType.Warning,
                          title="Paste from Kuukan")
else:
    # toolutils gives the network editor the user is actually working in;
    # the paneTab lookup is the fallback for headless-ish edge cases.
    try:
        pane = toolutils.networkEditor()
    except Exception:
        pane = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)
    if pane is None:
        hou.ui.displayMessage("Open a network editor first.",
                              severity=hou.severityType.Warning,
                              title="Paste from Kuukan")
    else:
        parent = pane.pwd()
        # OBJ and ROP: the clipboard prefix differs from the category
        # name (Object -> OBJ, Driver -> ROP) -- see the copy tool.
        _cat = parent.childTypeCategory().name().upper()
        net_ctx = {"OBJECT": "OBJ", "DRIVER": "ROP"}.get(_cat, _cat)

        # The filename carries the context (name.SOP.cpio) -- catch a
        # wrong-network paste with a clear question, before Houdini fails
        # with a murkier error.
        base = os.path.basename(newest)
        parts = base.split(".")
        file_ctx = parts[-2].upper() if len(parts) >= 3 else net_ctx
        proceed = True
        if file_ctx.isalpha() and file_ctx != net_ctx:
            ok = hou.ui.displayMessage(
                "'%s' is a %s setup; the open network is %s.\nPaste anyway?"
                % (base, file_ctx, net_ctx),
                buttons=("Cancel", "Paste anyway"),
            )
            proceed = (ok == 1)

        if proceed:
            # Stage as the native clipboard, then paste AT THE CENTRE OF THE
            # VIEW -- pasteItemsFromClipboard takes a position, so what arrives
            # lands where you are looking instead of wherever the last paste
            # happened to fall. Selection and UNDO still behave like Ctrl+V.
            #
            # The clipboard lives in HOUDINI_TEMP_DIR (Windows:
            # $TEMP/houdini_temp, NOT %TEMP%), and non-commercial builds read
            # the .cpionc name (Indie: .cpiolc). Stage every name the running
            # build might read -- staging the wrong one "succeeds" and pastes
            # nothing, silently.
            tmp = hou.getenv("HOUDINI_TEMP_DIR") or tempfile.gettempdir()
            try:
                os.makedirs(tmp, exist_ok=True)
            except Exception:
                pass
            lic = str(hou.licenseCategory()).split(".")[-1]
            ext = {"Apprentice": "cpionc", "ApprenticeHD": "cpionc",
                   "Education": "cpionc", "Indie": "cpiolc"}.get(lic, "cpio")
            for e in set(["cpio", ext]):
                shutil.copyfile(newest, os.path.join(tmp, "%s_copy.%s" % (net_ctx, e)))
            try:
                try:
                    center = pane.visibleBounds().center()
                    parent.pasteItemsFromClipboard(center)
                except AttributeError:
                    # Older Houdini without the positioned variant.
                    hou.pasteNodesFromClipboard(parent)
                hou.ui.setStatusMessage("Kuukan paste: %s -> %s" % (base, parent.path()))
            except hou.OperationFailed as e:
                hou.ui.displayMessage("Paste failed:\n%s" % e,
                                      severity=hou.severityType.Error)
]]></script>
  </tool>

  <tool name="kuukan_panel" label="Kuukan Panel" icon="kuukan.png">
    <helpText><![CDATA[A floating window: what's in the drop folder, what's new, double-click to paste.]]></helpText>
    <script scriptType="python"><![CDATA[
import hou, os, json, glob, shutil, tempfile, time
import toolutils

# Houdini 20.x ships PySide2, newer builds PySide6 -- same API surface here.
try:
    from PySide2 import QtWidgets, QtCore, QtGui
except ImportError:
    from PySide6 import QtWidgets, QtCore, QtGui

# Twins of the other two tools' constants -- a shelf file has no shared module.
SHELF_VERSION = 13
_HOME = os.environ.get("USERPROFILE") or os.path.expanduser("~")
DROP_DIR = os.path.join(_HOME, "kuukan_drop")

_NC = ("Apprentice", "ApprenticeHD", "Education")

# The Kuukan mark (kuukan-mark-badge-192.png), embedded -- no asset files.
_LOGO_B64 = "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"


def _ensure_drop():
    """Third twin -- see the copy tool for the full story."""
    first = not os.path.isdir(DROP_DIR)
    os.makedirs(DROP_DIR, exist_ok=True)
    try:
        with open(os.path.join(DROP_DIR, ".kuukan_bridge"), "w") as f:
            json.dump({
                "path": DROP_DIR,
                "houdini": hou.applicationVersionString(),
                "license": str(hou.licenseCategory()).split(".")[-1],
                "shelf": SHELF_VERSION,
            }, f)
    except Exception:
        pass
    if first:
        hou.ui.displayMessage(
            "Created the Kuukan drop folder:\n\n%s\n\nPick this exact folder in"
            " Kuukan:\nProfile & Settings -> Integrations -> Houdini -> Choose"
            " folder." % DROP_DIR)


def _read_marker():
    try:
        with open(os.path.join(DROP_DIR, ".kuukan_bridge")) as f:
            return json.load(f)
    except Exception:
        return {}


def _install_icon():
    """Write the embedded mark to config/Icons, then pin it onto the tools
    with hou.Tool.setIcon and an ABSOLUTE path. The bare-name form relied
    on the icon search path finding the file, which held on one machine
    and not the next (Linux said no) -- and before the first panel open
    the file did not exist at all, leaving the button blank. setIcon
    persists into the installed shelf copy, so this self-heals on every
    open and survives restarts."""
    try:
        pref = hou.getenv("HOUDINI_USER_PREF_DIR")
        if not pref:
            return
        icons = os.path.join(pref, "config", "Icons")
        os.makedirs(icons, exist_ok=True)
        ipath = os.path.join(icons, "kuukan.png")
        if not os.path.isfile(ipath):
            import base64
            with open(ipath, "wb") as f:
                f.write(base64.b64decode(_LOGO_B64))
        for tname in ("kuukan_copy", "kuukan_paste", "kuukan_panel"):
            try:
                t = hou.shelves.tool(tname)
                if t is not None and t.icon() != ipath:
                    t.setIcon(ipath)
            except Exception:
                pass
    except Exception:
        pass


class KuukanPanel(QtWidgets.QWidget):
    """The drop folder, visible: a light for arrivals, a list of setups,
    double-click to paste the one you mean (instead of newest-wins blind).
    Reads ONLY the folder -- no network, so it works behind any studio wall
    and on any license. It never touches the clipboard until you ask."""

    def __init__(self, parent=None):
        super(KuukanPanel, self).__init__(parent)
        self.setWindowTitle("Kuukan")
        # Frameless: the white OS title bar has no business on a Kuukan
        # window. The header row below is the drag handle, the x closes,
        # and a size grip in the corner brings resizing back.
        self.setWindowFlags(QtCore.Qt.Tool | QtCore.Qt.FramelessWindowHint)
        self.setMinimumSize(330, 380)
        self._drag = None
        self._known = set()     # every sig ever seen this session
        self._new = set()       # arrived after open, not yet pasted
        self._first_scan = True

        # The house look: dark teal ground, teal accents, soft corners.
        # WA_StyledBackground is what makes a QWidget subclass actually
        # paint its stylesheet background.
        self.setAttribute(QtCore.Qt.WA_StyledBackground, True)
        self.setStyleSheet(
            "KuukanPanel { background: #0e181b; }"
            " QLabel { color: #9adcd6; }"
            " QListWidget { background: #10191d; border: 1px solid #24343a;"
            "   border-radius: 8px; color: #cfe6e2; }"
            " QListWidget::item { padding: 6px 6px; border-radius: 5px; }"
            " QListWidget::item:selected { background: rgba(78,205,196,0.18);"
            "   color: #e8fffc; }"
            " QCheckBox { color: #7a969b; font-size: 10px; }"
            " QPushButton { background: rgba(78,205,196,0.12);"
            "   border: 1px solid #2e5b57; border-radius: 8px; color: #9adcd6;"
            "   padding: 7px; }"
            " QPushButton:hover { background: rgba(78,205,196,0.25);"
            "   color: #e8fffc; }")

        lay = QtWidgets.QVBoxLayout(self)
        lay.setContentsMargins(12, 12, 12, 12)
        lay.setSpacing(8)

        head = QtWidgets.QHBoxLayout()
        # The mark in the corner -- embedded as base64 so the shelf stays
        # one self-contained ASCII file.
        try:
            pm = QtGui.QPixmap()
            pm.loadFromData(QtCore.QByteArray.fromBase64(_LOGO_B64.encode("ascii")))
            logo = QtWidgets.QLabel()
            logo.setPixmap(pm.scaled(22, 22, QtCore.Qt.KeepAspectRatio,
                                     QtCore.Qt.SmoothTransformation))
            head.addWidget(logo)
            self.setWindowIcon(QtGui.QIcon(pm))
        except Exception:
            pass
        self.titleLabel = QtWidgets.QLabel("Kuukan")
        self.titleLabel.setStyleSheet("font-size: 12px; font-weight: 600; color: #cfe6e2;")
        head.addWidget(self.titleLabel)
        self.light = QtWidgets.QLabel()
        self.light.setFixedSize(12, 12)
        head.addWidget(self.light)
        head.addStretch(1)
        closeBtn = QtWidgets.QPushButton("x")
        closeBtn.setFixedSize(20, 20)
        closeBtn.setStyleSheet(
            "QPushButton { background: transparent; border: none;"
            "   color: #7a969b; font-weight: bold; }"
            " QPushButton:hover { color: #ff7b6b; }")
        closeBtn.clicked.connect(self.close)
        head.addWidget(closeBtn)
        lay.addLayout(head)

        self.linkLabel = QtWidgets.QLabel("")
        self.linkLabel.setStyleSheet("font-size: 10px; color: #7a969b;")
        self.linkLabel.setWordWrap(True)
        lay.addWidget(self.linkLabel)

        self.listw = QtWidgets.QListWidget()
        self.listw.itemDoubleClicked.connect(self._paste_item)
        self.listw.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.listw.customContextMenuRequested.connect(self._menu)
        lay.addWidget(self.listw, 1)

        hint = QtWidgets.QLabel("Select a setup and press Paste - or double-click it. Right-click deletes.")
        hint.setStyleSheet("font-size: 10px; color: #8a8a8a;")
        hint.setWordWrap(True)
        lay.addWidget(hint)

        self.showOld = QtWidgets.QCheckBox("Show older than 10 min")
        self.showOld.stateChanged.connect(lambda *_: self._refresh())
        lay.addWidget(self.showOld)

        bottom = QtWidgets.QHBoxLayout()
        # The primary action, discoverable: selecting a row wakes this up.
        # Double-click stays as the fast path -- but nobody should need to
        # guess it.
        self.pasteBtn = QtWidgets.QPushButton("Paste into network")
        self.pasteBtn.setEnabled(False)
        self.pasteBtn.setStyleSheet(
            "QPushButton { background: rgba(78,205,196,0.28); color: #e8fffc;"
            "   border: 1px solid #4ecdc4; border-radius: 8px; padding: 7px;"
            "   font-weight: 600; }"
            " QPushButton:hover { background: rgba(78,205,196,0.42); }"
            " QPushButton:disabled { background: rgba(78,205,196,0.06);"
            "   color: #5f7276; border-color: #2e5b57; }")
        self.pasteBtn.clicked.connect(self._paste_selected)
        self.listw.itemSelectionChanged.connect(
            lambda: self.pasteBtn.setEnabled(self.listw.currentItem() is not None))
        # Copy on the left, Paste on the right -- the shelf's own order.
        copyBtn = QtWidgets.QPushButton("Copy selection to Kuukan")
        copyBtn.clicked.connect(self._copy_selection)
        bottom.addWidget(copyBtn, 1)
        bottom.addWidget(self.pasteBtn, 1)
        bottom.addWidget(QtWidgets.QSizeGrip(self), 0, QtCore.Qt.AlignBottom)
        lay.addLayout(bottom)

        self.timer = QtCore.QTimer(self)
        self.timer.timeout.connect(self._refresh)
        self.timer.start(2000)
        self._refresh()

    def closeEvent(self, event):
        self.timer.stop()
        try:
            if getattr(hou.session, "_kuukan_panel", None) is self:
                hou.session._kuukan_panel = None
        except Exception:
            pass
        super(KuukanPanel, self).closeEvent(event)

    # ---- frameless window drag (the header is the handle; the list and
    # buttons consume their own clicks, so empty chrome is what drags) -----
    @staticmethod
    def _gpos(event):
        if hasattr(event, "globalPosition"):
            return event.globalPosition().toPoint()   # PySide6
        return event.globalPos()                      # PySide2

    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.LeftButton:
            self._drag = self._gpos(event) - self.frameGeometry().topLeft()
        super(KuukanPanel, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if self._drag is not None and (event.buttons() & QtCore.Qt.LeftButton):
            self.move(self._gpos(event) - self._drag)
        super(KuukanPanel, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        self._drag = None
        super(KuukanPanel, self).mouseReleaseEvent(event)

    # ---- folder data -----------------------------------------------------
    @staticmethod
    def _files():
        out = []
        for p in glob.glob(os.path.join(DROP_DIR, "*.cpio")):
            try:
                st = os.stat(p)
                out.append((p, os.path.basename(p), st.st_mtime, st.st_size))
            except OSError:
                pass
        out.sort(key=lambda r: -r[2])
        return out

    @staticmethod
    def _age(t):
        d = max(0, time.time() - t)
        if d < 60:
            return "%ds" % int(d)
        if d < 3600:
            return "%d min" % int(d // 60)
        if d < 86400:
            return "%d h" % int(d // 3600)
        return "%d d" % int(d // 86400)

    @staticmethod
    def _ctx(name):
        parts = name.split(".")
        if len(parts) >= 3 and parts[-2].isalpha():
            return parts[-2].upper()
        return ""

    @staticmethod
    def _disp(name):
        parts = name.split(".")
        if len(parts) >= 3 and parts[-1].lower() == "cpio" and parts[-2].isalpha():
            return ".".join(parts[:-2])
        return name[:-5] if name.lower().endswith(".cpio") else name

    # ---- refresh ---------------------------------------------------------
    def _refresh(self):
        files = self._files()
        sigs = set()
        for p, n, m, s in files:
            sig = "%s|%s|%s" % (n, s, m)
            sigs.add(sig)
            if sig not in self._known:
                self._known.add(sig)
                # Present at open = history, not news -- same rule as the
                # Kuukan watcher's baseline pass. And your OWN exports are
                # not news either: the copy tool registers what it writes.
                own = getattr(hou.session, "_kuukan_own", set())
                if not self._first_scan and n not in own:
                    self._new.add(sig)
        self._first_scan = False
        self._new &= sigs   # deleted files stop being news

        # Old setups are clutter by default -- the folder is a clipboard,
        # not an archive. New arrivals always show regardless of age.
        cutoff = time.time() - 600
        show_old = self.showOld.isChecked()
        hidden = 0
        cur = self.listw.currentRow()
        self.listw.clear()
        own = getattr(hou.session, "_kuukan_own", set())
        for p, n, m, s in files:
            sig = "%s|%s|%s" % (n, s, m)
            if not show_old and m < cutoff and sig not in self._new:
                hidden += 1
                continue
            ctx = self._ctx(n)
            label = self._disp(n)
            if ctx:
                label += "    %s" % ctx
            label += " - %s" % self._age(m)
            item = QtWidgets.QListWidgetItem(label)
            item.setData(QtCore.Qt.UserRole, p)
            item.setData(QtCore.Qt.UserRole + 1, sig)
            if sig in self._new:
                f = item.font()
                f.setBold(True)
                item.setFont(f)
                item.setForeground(QtGui.QColor("#7ee8c0"))
                item.setText("* " + label)
            elif n in own:
                # Your own export: shown as a receipt that it reached the
                # folder, but dimmed and pointing OUT -- it is cargo leaving,
                # not work arriving.
                item.setForeground(QtGui.QColor("#5f7276"))
                item.setText(label + "   -> Kuukan")
            self.listw.addItem(item)
        if 0 <= cur < self.listw.count():
            self.listw.setCurrentRow(cur)
        self.showOld.setText("Show older than 10 min (%d)" % hidden if hidden
                             else "Show older than 10 min")

        # Header: link line + the light. GREEN = something new to paste,
        # calm grey = nothing waiting, amber = folder missing. The light
        # answers exactly one question: "is there work for me here?"
        info = _read_marker()
        lic = info.get("license", "")
        suffix = " - Non-Commercial" if lic in _NC else (" - Indie" if lic == "Indie" else "")
        ok = os.path.isdir(DROP_DIR)
        if ok:
            self.linkLabel.setText((info.get("path") or DROP_DIR) + suffix)
        else:
            self.linkLabel.setText("Drop folder missing - click the Kuukan shelf button again.")
        if not ok:
            col = "#f0b34c"
        elif self._new:
            col = "#34d399"
        else:
            col = "#5a6a74"
        self.light.setStyleSheet("background: %s; border-radius: 6px;" % col)
        title = "Kuukan -- %d new" % len(self._new) if self._new else "Kuukan"
        self.setWindowTitle(title)   # still feeds the taskbar
        self.titleLabel.setText(title)

    # ---- actions ---------------------------------------------------------
    def _paste_item(self, item):
        """Twin of the Paste tool's core, minus newest-wins: THIS file."""
        path = item.data(QtCore.Qt.UserRole)
        if not path or not os.path.isfile(path):
            return
        try:
            pane = toolutils.networkEditor()
        except Exception:
            pane = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)
        if pane is None:
            hou.ui.displayMessage("Open a network editor first.",
                                  severity=hou.severityType.Warning,
                                  title="Kuukan Panel")
            return
        parent = pane.pwd()
        # OBJ and ROP: the clipboard prefix differs from the category
        # name (Object -> OBJ, Driver -> ROP) -- see the copy tool.
        _cat = parent.childTypeCategory().name().upper()
        net_ctx = {"OBJECT": "OBJ", "DRIVER": "ROP"}.get(_cat, _cat)
        base = os.path.basename(path)
        file_ctx = self._ctx(base) or net_ctx
        if file_ctx.isalpha() and file_ctx != net_ctx:
            ok = hou.ui.displayMessage(
                "'%s' is a %s setup; the open network is %s.\nPaste anyway?"
                % (base, file_ctx, net_ctx),
                buttons=("Cancel", "Paste anyway"))
            if ok != 1:
                return
        tmp = hou.getenv("HOUDINI_TEMP_DIR") or tempfile.gettempdir()
        try:
            os.makedirs(tmp, exist_ok=True)
        except Exception:
            pass
        lic = str(hou.licenseCategory()).split(".")[-1]
        ext = {"Apprentice": "cpionc", "ApprenticeHD": "cpionc",
               "Education": "cpionc", "Indie": "cpiolc"}.get(lic, "cpio")
        for e in set(["cpio", ext]):
            shutil.copyfile(path, os.path.join(tmp, "%s_copy.%s" % (net_ctx, e)))
        try:
            try:
                center = pane.visibleBounds().center()
                parent.pasteItemsFromClipboard(center)
            except AttributeError:
                hou.pasteNodesFromClipboard(parent)
            hou.ui.setStatusMessage("Kuukan paste: %s -> %s" % (base, parent.path()))
            self._new.discard(item.data(QtCore.Qt.UserRole + 1))
            self._refresh()
        except hou.OperationFailed as e:
            hou.ui.displayMessage("Paste failed:\n%s" % e,
                                  severity=hou.severityType.Error)

    def _paste_selected(self):
        item = self.listw.currentItem()
        if item is not None:
            self._paste_item(item)

    def _copy_selection(self):
        # Run the Copy tool itself instead of growing a third copy of its
        # logic -- the shelf is the module, exec is the import.
        tool = hou.shelves.tool("kuukan_copy")
        if tool is None:
            hou.ui.displayMessage("The Kuukan shelf's Copy tool was not found.",
                                  title="Kuukan Panel")
            return
        exec(tool.script(), {})
        self._refresh()

    def _menu(self, pos):
        item = self.listw.itemAt(pos)
        if item is None:
            return
        menu = QtWidgets.QMenu(self)
        delAct = menu.addAction("Delete from folder")
        chosen = getattr(menu, "exec_", menu.exec)(self.listw.mapToGlobal(pos))
        if chosen == delAct:
            path = item.data(QtCore.Qt.UserRole)
            base = os.path.basename(path)
            ok = hou.ui.displayMessage("Delete %s from the drop folder?" % base,
                                       buttons=("Cancel", "Delete"))
            if ok == 1:
                try:
                    os.remove(path)
                except OSError as e:
                    hou.ui.displayMessage("Delete failed: %s" % e,
                                          severity=hou.severityType.Error)
                self._refresh()


# Singleton: a second click raises the existing window instead of stacking
# twins. The reference lives on hou.session so it survives between clicks.
_panel = getattr(hou.session, "_kuukan_panel", None)
if _panel is not None:
    try:
        # Re-ensure on every click: if someone deleted the drop folder
        # mid-session, the shelf button brings it back.
        _ensure_drop()
        _panel.show()
        _panel.raise_()
        _panel.activateWindow()
    except Exception:
        _panel = None
if _panel is None:
    _ensure_drop()
    _install_icon()
    _w = KuukanPanel(parent=hou.qt.mainWindow())
    hou.session._kuukan_panel = _w
    _w.show()
]]></script>
  </tool>

</shelfDocument>
