# LabelTender Qt Desktop Shell Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a Qt desktop shell around the current LabelTender frontend so it can open/save files natively and print through system printers on UOS/deepin.

**Architecture:** Keep the existing frontend as the renderer, add a JS desktop bridge, implement a Qt WebEngine shell that exposes file and printer APIs over `QWebChannel`, and replace the browser print exit path with Qt-backed system printing using rasterized page images.

**Tech Stack:** HTML/JS frontend, C++17, CMake, Qt WebEngine, Qt WebChannel, Qt PrintSupport, CPack Debian

---

### Task 1: Add Desktop Bridge Files

**Files:**
- Create: `static/desktop-bridge.js`
- Modify: `index.html`
- Test: `static/desktop-bridge.js`

- [ ] **Step 1: Write a failing bridge smoke test**

```bash
node - <<'EOF'
const fs = require('fs');
const src = fs.readFileSync('static/desktop-bridge.js', 'utf8');
if (!src.includes('window.desktopBridge')) {
  throw new Error('window.desktopBridge missing');
}
EOF
```

Expected: FAIL because `static/desktop-bridge.js` does not exist yet.

- [ ] **Step 2: Create the bridge implementation**

```javascript
(function () {
  const qt = window.qtBridge || null;
  const noop = async () => ({ ok: false, reason: "desktop bridge unavailable" });

  const desktopBridge = {
    isQt() {
      return !!qt;
    },
    async openProject() {
      return qt && qt.openProject ? JSON.parse(await qt.openProject()) : noop();
    },
    async saveProject(payload) {
      return qt && qt.saveProject ? JSON.parse(await qt.saveProject(JSON.stringify(payload))) : noop();
    },
  };

  window.desktopBridge = desktopBridge;
})();
```

- [ ] **Step 3: Load the bridge from the page**

```html
<script src="./static/desktop-bridge.js"></script>
```

- [ ] **Step 4: Run the bridge smoke test**

```bash
node - <<'EOF'
const fs = require('fs');
const src = fs.readFileSync('static/desktop-bridge.js', 'utf8');
if (!src.includes('window.desktopBridge')) {
  throw new Error('window.desktopBridge missing');
}
console.log('bridge ok');
EOF
```

Expected: PASS with `bridge ok`.

- [ ] **Step 5: Commit**

```bash
git add index.html static/desktop-bridge.js
git commit -m "feat: add desktop bridge shim"
```

### Task 2: Switch Frontend File and Window Actions to the Bridge

**Files:**
- Modify: `index.html`
- Test: `index.html`

- [ ] **Step 1: Write a failing text check for bridge-based file handling**

```bash
node - <<'EOF'
const fs = require('fs');
const src = fs.readFileSync('index.html', 'utf8');
if (!src.includes('desktopBridge.openProject')) {
  throw new Error('desktop open path missing');
}
EOF
```

Expected: FAIL because the frontend still uses browser-only file handling.

- [ ] **Step 2: Add bridge-first window and file code paths**

```javascript
window: {
  minimize() { return window.desktopBridge?.window.minimize?.(); },
  maximize() { return window.desktopBridge?.window.maximize?.(); },
  close() { return window.desktopBridge?.window.close?.(); },
},
```

```javascript
if (window.desktopBridge?.isQt()) {
  const result = await window.desktopBridge.openProject();
}
```

- [ ] **Step 3: Update recent file storage to support desktop paths**

```javascript
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(list.map(({ path, ...rest }) => ({ path, ...rest }))));
```

- [ ] **Step 4: Run the bridge-based file handling check**

```bash
node - <<'EOF'
const fs = require('fs');
const src = fs.readFileSync('index.html', 'utf8');
if (!src.includes('desktopBridge.openProject')) {
  throw new Error('desktop open path missing');
}
if (!src.includes('window: {')) {
  throw new Error('editor.window bridge missing');
}
console.log('frontend bridge paths ok');
EOF
```

Expected: PASS with `frontend bridge paths ok`.

- [ ] **Step 5: Commit**

```bash
git add index.html
git commit -m "feat: route frontend file actions through desktop bridge"
```

### Task 3: Replace Browser Printing with Qt Print Payload Submission

**Files:**
- Modify: `index.html`
- Test: `index.html`

- [ ] **Step 1: Write a failing print-exit test**

```bash
node - <<'EOF'
const fs = require('fs');
const src = fs.readFileSync('index.html', 'utf8');
if (src.includes('contentWindow.print()')) {
  throw new Error('browser print fallback still present');
}
EOF
```

Expected: FAIL because the browser print path still exists.

- [ ] **Step 2: Add printer listing and print job submission through the bridge**

```javascript
const printers = await window.desktopBridge.print.listPrinters();
await window.desktopBridge.print.submitJob({
  printerName,
  copies,
  pageWidthMm,
  pageHeightMm,
  pages,
});
```

- [ ] **Step 3: Keep PDF export but stop using iframe print for Qt**

```javascript
if (!window.desktopBridge?.isQt()) {
  iframe.contentWindow.print();
  return;
}
```

- [ ] **Step 4: Run the print-exit test**

```bash
node - <<'EOF'
const fs = require('fs');
const src = fs.readFileSync('index.html', 'utf8');
if (!src.includes('desktopBridge.print.submitJob')) {
  throw new Error('desktop print submission missing');
}
console.log('desktop print path ok');
EOF
```

Expected: PASS with `desktop print path ok`.

- [ ] **Step 5: Commit**

```bash
git add index.html
git commit -m "feat: submit print jobs through desktop bridge"
```

### Task 4: Add Qt Shell Sources and CMake Build

**Files:**
- Create: `qt-shell/CMakeLists.txt`
- Create: `qt-shell/src/main.cpp`
- Create: `qt-shell/src/bridge.h`
- Create: `qt-shell/src/bridge.cpp`
- Create: `qt-shell/src/resources.qrc`
- Test: `qt-shell/CMakeLists.txt`

- [ ] **Step 1: Write a failing file-existence check**

```bash
node - <<'EOF'
const fs = require('fs');
for (const file of ['qt-shell/CMakeLists.txt', 'qt-shell/src/main.cpp', 'qt-shell/src/bridge.h', 'qt-shell/src/bridge.cpp']) {
  if (!fs.existsSync(file)) {
    throw new Error(`missing ${file}`);
  }
}
EOF
```

Expected: FAIL because the Qt shell files do not exist yet.

- [ ] **Step 2: Create the CMake project with Qt5/Qt6 compatibility**

```cmake
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS WebEngineWidgets WebChannel PrintSupport Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS WebEngineWidgets WebChannel PrintSupport Widgets)
```

- [ ] **Step 3: Create the bridge object and Qt shell entry point**

```cpp
class DesktopBridge : public QObject {
  Q_OBJECT
  Q_INVOKABLE QString openProject();
  Q_INVOKABLE QString saveProject(const QString &payload);
  Q_INVOKABLE QString listPrinters() const;
  Q_INVOKABLE QString submitPrintJob(const QString &payload);
};
```

- [ ] **Step 4: Run the file-existence check**

```bash
node - <<'EOF'
const fs = require('fs');
for (const file of ['qt-shell/CMakeLists.txt', 'qt-shell/src/main.cpp', 'qt-shell/src/bridge.h', 'qt-shell/src/bridge.cpp']) {
  if (!fs.existsSync(file)) {
    throw new Error(`missing ${file}`);
  }
}
console.log('qt shell files ok');
EOF
```

Expected: PASS with `qt shell files ok`.

- [ ] **Step 5: Commit**

```bash
git add qt-shell
git commit -m "feat: add qt desktop shell scaffold"
```

### Task 5: Add Debian Packaging Metadata

**Files:**
- Create: `qt-shell/packaging/labelnova.desktop.in`
- Create: `qt-shell/packaging/labelnova.png`
- Modify: `qt-shell/CMakeLists.txt`
- Test: `qt-shell/CMakeLists.txt`

- [ ] **Step 1: Write a failing packaging check**

```bash
node - <<'EOF'
const fs = require('fs');
if (!fs.existsSync('qt-shell/packaging/labelnova.desktop.in')) {
  throw new Error('desktop file template missing');
}
EOF
```

Expected: FAIL because the packaging files do not exist yet.

- [ ] **Step 2: Add Debian packaging configuration**

```cmake
set(CPACK_GENERATOR "DEB")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5webenginewidgets5 | libqt6webenginewidgets6")
include(CPack)
```

- [ ] **Step 3: Add the launcher template and icon asset**

```ini
[Desktop Entry]
Type=Application
Name=LabelNova
Exec=labelnova
Icon=labelnova
Categories=Graphics;Office;
```

- [ ] **Step 4: Run the packaging check**

```bash
node - <<'EOF'
const fs = require('fs');
if (!fs.existsSync('qt-shell/packaging/labelnova.desktop.in')) {
  throw new Error('desktop file template missing');
}
console.log('packaging files ok');
EOF
```

Expected: PASS with `packaging files ok`.

- [ ] **Step 5: Commit**

```bash
git add qt-shell/CMakeLists.txt qt-shell/packaging
git commit -m "feat: add deb packaging metadata"
```
