# ❌ Breaks in bundled builds
base = os.path.dirname(__file__)
qss_path = os.path.join(base, "ui", "ghost.qss")
# ❌ Breaks in bundled builds
base = os.path.dirname(__file__)
qss_path = os.path.join(base, "ui", "ghost.qss")
# ❌ Breaks in bundled builds
base = os.path.dirname(__file__)
qss_path = os.path.join(base, "ui", "ghost.qss")
Source mode: __file__ → /home/user/DotGhostBoard/core/app.py ✅ Frozen mode: __file__ → /tmp/_MEI3xk9/core/app.py ✅ ghost.qss → ??? ❌
Source mode: __file__ → /home/user/DotGhostBoard/core/app.py ✅ Frozen mode: __file__ → /tmp/_MEI3xk9/core/app.py ✅ ghost.qss → ??? ❌
Source mode: __file__ → /home/user/DotGhostBoard/core/app.py ✅ Frozen mode: __file__ → /tmp/_MEI3xk9/core/app.py ✅ ghost.qss → ??? ❌
# core/paths.py
import os
import sys def _base_dir() -> str: """ Detect whether we're running from source or a PyInstaller bundle. In frozen mode, all assets are extracted to sys._MEIPASS at runtime. """ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): return sys._MEIPASS # PyInstaller extraction folder return os.path.dirname( # two levels up from core/paths.py os.path.dirname(os.path.abspath(__file__)) ) def resource_path(*parts: str) -> str: """ Build an absolute path to any bundled asset, regardless of runtime mode. Usage: resource_path("ui", "ghost.qss") → /tmp/_MEIxxxx/ui/ghost.qss resource_path("data", "icon.png") → /home/user/Project/data/icon.png """ return os.path.join(_base_dir(), *parts)
# core/paths.py
import os
import sys def _base_dir() -> str: """ Detect whether we're running from source or a PyInstaller bundle. In frozen mode, all assets are extracted to sys._MEIPASS at runtime. """ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): return sys._MEIPASS # PyInstaller extraction folder return os.path.dirname( # two levels up from core/paths.py os.path.dirname(os.path.abspath(__file__)) ) def resource_path(*parts: str) -> str: """ Build an absolute path to any bundled asset, regardless of runtime mode. Usage: resource_path("ui", "ghost.qss") → /tmp/_MEIxxxx/ui/ghost.qss resource_path("data", "icon.png") → /home/user/Project/data/icon.png """ return os.path.join(_base_dir(), *parts)
# core/paths.py
import os
import sys def _base_dir() -> str: """ Detect whether we're running from source or a PyInstaller bundle. In frozen mode, all assets are extracted to sys._MEIPASS at runtime. """ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): return sys._MEIPASS # PyInstaller extraction folder return os.path.dirname( # two levels up from core/paths.py os.path.dirname(os.path.abspath(__file__)) ) def resource_path(*parts: str) -> str: """ Build an absolute path to any bundled asset, regardless of runtime mode. Usage: resource_path("ui", "ghost.qss") → /tmp/_MEIxxxx/ui/ghost.qss resource_path("data", "icon.png") → /home/user/Project/data/icon.png """ return os.path.join(_base_dir(), *parts)
App requests: resource_path("ui", "ghost.qss") │ is_frozen? / \ Yes No │ │ sys._MEIPASS Project Root │ │ /tmp/_MEIxxxx/ui/ghost.qss /home/user/.../ui/ghost.qss
App requests: resource_path("ui", "ghost.qss") │ is_frozen? / \ Yes No │ │ sys._MEIPASS Project Root │ │ /tmp/_MEIxxxx/ui/ghost.qss /home/user/.../ui/ghost.qss
App requests: resource_path("ui", "ghost.qss") │ is_frozen? / \ Yes No │ │ sys._MEIPASS Project Root │ │ /tmp/_MEIxxxx/ui/ghost.qss /home/user/.../ui/ghost.qss
# ❌ v1.5.1 — ghost.qss never made it into the bundle
- name: Build run: | pyinstaller --noconsole --onedir \ --add-data "data:data" \ --hidden-import "PyQt6.sip" \ --name dotghostboard main.py
# ❌ v1.5.1 — ghost.qss never made it into the bundle
- name: Build run: | pyinstaller --noconsole --onedir \ --add-data "data:data" \ --hidden-import "PyQt6.sip" \ --name dotghostboard main.py
# ❌ v1.5.1 — ghost.qss never made it into the bundle
- name: Build run: | pyinstaller --noconsole --onedir \ --add-data "data:data" \ --hidden-import "PyQt6.sip" \ --name dotghostboard main.py
# ✅ v1.5.3 — explicitly map the UI directory
- name: Build run: | pyinstaller --noconsole --onedir \ --add-data "data:data" \ --add-data "ui/ghost.qss:ui" \ # ← this line was the missing piece --hidden-import "PyQt6.sip" \ --hidden-import "cryptography" \ --hidden-import "cryptography.hazmat.primitives.asymmetric.x25519" \ --hidden-import "cryptography.hazmat.primitives.ciphers.aead" \ --name dotghostboard main.py
# ✅ v1.5.3 — explicitly map the UI directory
- name: Build run: | pyinstaller --noconsole --onedir \ --add-data "data:data" \ --add-data "ui/ghost.qss:ui" \ # ← this line was the missing piece --hidden-import "PyQt6.sip" \ --hidden-import "cryptography" \ --hidden-import "cryptography.hazmat.primitives.asymmetric.x25519" \ --hidden-import "cryptography.hazmat.primitives.ciphers.aead" \ --name dotghostboard main.py
# ✅ v1.5.3 — explicitly map the UI directory
- name: Build run: | pyinstaller --noconsole --onedir \ --add-data "data:data" \ --add-data "ui/ghost.qss:ui" \ # ← this line was the missing piece --hidden-import "PyQt6.sip" \ --hidden-import "cryptography" \ --hidden-import "cryptography.hazmat.primitives.asymmetric.x25519" \ --hidden-import "cryptography.hazmat.primitives.ciphers.aead" \ --name dotghostboard main.py
dpkg -c dotghostboard_1.5.3_amd64.deb | grep ui/
# Expected: ... _internal/ui/ghost.qss
dpkg -c dotghostboard_1.5.3_amd64.deb | grep ui/
# Expected: ... _internal/ui/ghost.qss
dpkg -c dotghostboard_1.5.3_amd64.deb | grep ui/
# Expected: ... _internal/ui/ghost.qss
# ❌ v1.5.1
download_path = f"/tmp/dotghostboard_{version}_amd64.deb"
# ❌ v1.5.1
download_path = f"/tmp/dotghostboard_{version}_amd64.deb"
# ❌ v1.5.1
download_path = f"/tmp/dotghostboard_{version}_amd64.deb"
# ✅ v1.5.2+
import os def get_update_staging_dir() -> str: """ Returns a user-specific, restricted directory for staging -weight: 500;">update packages. Created with mode 0o700 — only the owning user can read or write. """ path = os.path.join( os.path.expanduser("~"), ".local", "share", "dotghostboard", "updates" ) os.makedirs(path, mode=0o700, exist_ok=True) return path SAFE_PATH = os.path.join(get_update_staging_dir(), f"dotghostboard_{version}_amd64.deb")
# ✅ v1.5.2+
import os def get_update_staging_dir() -> str: """ Returns a user-specific, restricted directory for staging -weight: 500;">update packages. Created with mode 0o700 — only the owning user can read or write. """ path = os.path.join( os.path.expanduser("~"), ".local", "share", "dotghostboard", "updates" ) os.makedirs(path, mode=0o700, exist_ok=True) return path SAFE_PATH = os.path.join(get_update_staging_dir(), f"dotghostboard_{version}_amd64.deb")
# ✅ v1.5.2+
import os def get_update_staging_dir() -> str: """ Returns a user-specific, restricted directory for staging -weight: 500;">update packages. Created with mode 0o700 — only the owning user can read or write. """ path = os.path.join( os.path.expanduser("~"), ".local", "share", "dotghostboard", "updates" ) os.makedirs(path, mode=0o700, exist_ok=True) return path SAFE_PATH = os.path.join(get_update_staging_dir(), f"dotghostboard_{version}_amd64.deb")
#!/bin/sh
# dotghostboard_install.sh — self-destructs after use SAFE_PATH="$1" dpkg -i "$SAFE_PATH" # Install the package
rm -f "$SAFE_PATH" # Wipe the .deb from disk
rm -f "$0" # Wipe this script itself
#!/bin/sh
# dotghostboard_install.sh — self-destructs after use SAFE_PATH="$1" dpkg -i "$SAFE_PATH" # Install the package
rm -f "$SAFE_PATH" # Wipe the .deb from disk
rm -f "$0" # Wipe this script itself
#!/bin/sh
# dotghostboard_install.sh — self-destructs after use SAFE_PATH="$1" dpkg -i "$SAFE_PATH" # Install the package
rm -f "$SAFE_PATH" # Wipe the .deb from disk
rm -f "$0" # Wipe this script itself
#!/bin/sh
# DEBIAN/postinst set -e fix_pkexec() { PKEXEC="/usr/bin/pkexec" if [ -f "$PKEXEC" ]; then CURRENT_PERMS=$(stat -c "%a" "$PKEXEC") if [ "$CURRENT_PERMS" != "4755" ]; then echo "dotghostboard: repairing pkexec SUID bit ($CURRENT_PERMS → 4755)" chmod 4755 "$PKEXEC" fi fi
} case "$1" in configure) fix_pkexec ;;
esac exit 0
#!/bin/sh
# DEBIAN/postinst set -e fix_pkexec() { PKEXEC="/usr/bin/pkexec" if [ -f "$PKEXEC" ]; then CURRENT_PERMS=$(stat -c "%a" "$PKEXEC") if [ "$CURRENT_PERMS" != "4755" ]; then echo "dotghostboard: repairing pkexec SUID bit ($CURRENT_PERMS → 4755)" chmod 4755 "$PKEXEC" fi fi
} case "$1" in configure) fix_pkexec ;;
esac exit 0
#!/bin/sh
# DEBIAN/postinst set -e fix_pkexec() { PKEXEC="/usr/bin/pkexec" if [ -f "$PKEXEC" ]; then CURRENT_PERMS=$(stat -c "%a" "$PKEXEC") if [ "$CURRENT_PERMS" != "4755" ]; then echo "dotghostboard: repairing pkexec SUID bit ($CURRENT_PERMS → 4755)" chmod 4755 "$PKEXEC" fi fi
} case "$1" in configure) fix_pkexec ;;
esac exit 0
dpkg -i dotghostboard_1.5.3_amd64.deb │ └── postinst runs as root │ /usr/bin/pkexec exists? │ current perms == 4755? / \ Yes No │ │ Skip chmod 4755 │ "Download & Install" works
dpkg -i dotghostboard_1.5.3_amd64.deb │ └── postinst runs as root │ /usr/bin/pkexec exists? │ current perms == 4755? / \ Yes No │ │ Skip chmod 4755 │ "Download & Install" works
dpkg -i dotghostboard_1.5.3_amd64.deb │ └── postinst runs as root │ /usr/bin/pkexec exists? │ current perms == 4755? / \ Yes No │ │ Skip chmod 4755 │ "Download & Install" works
# ui/update_log_screen.py
from PyQt6.QtCore import QThread, pyqtSignal
import subprocess class InstallWorker(QThread): log_line = pyqtSignal(str) # emitted for every line of dpkg output finished = pyqtSignal(int) # exit code def __init__(self, script_path: str): super().__init__() self.script_path = script_path def run(self): process = subprocess.Popen( ["bash", self.script_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # merge stderr into stdout text=True, bufsize=1 # line-buffered ) for line in process.stdout: self.log_line.emit(line.rstrip()) process.wait() self.finished.emit(process.returncode)
# ui/update_log_screen.py
from PyQt6.QtCore import QThread, pyqtSignal
import subprocess class InstallWorker(QThread): log_line = pyqtSignal(str) # emitted for every line of dpkg output finished = pyqtSignal(int) # exit code def __init__(self, script_path: str): super().__init__() self.script_path = script_path def run(self): process = subprocess.Popen( ["bash", self.script_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # merge stderr into stdout text=True, bufsize=1 # line-buffered ) for line in process.stdout: self.log_line.emit(line.rstrip()) process.wait() self.finished.emit(process.returncode)
# ui/update_log_screen.py
from PyQt6.QtCore import QThread, pyqtSignal
import subprocess class InstallWorker(QThread): log_line = pyqtSignal(str) # emitted for every line of dpkg output finished = pyqtSignal(int) # exit code def __init__(self, script_path: str): super().__init__() self.script_path = script_path def run(self): process = subprocess.Popen( ["bash", self.script_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # merge stderr into stdout text=True, bufsize=1 # line-buffered ) for line in process.stdout: self.log_line.emit(line.rstrip()) process.wait() self.finished.emit(process.returncode)
# Color mapping — Regex-based, no AI required
import re
from PyQt6.QtGui import QColor COLOR_MAP = [ (re.compile(r'\berror\b', re.I), "#FF5555"), # red (re.compile(r'\bwarning\b', re.I), "#FFB86C"), # amber (re.compile(r'\bSetting up\b|\bProcessing\b'), "#50FA7B"), # green (re.compile(r'^--'), "#8BE9FD"), # cyan for steps
] def colorize(line: str) -> str: for pattern, color in COLOR_MAP: if pattern.search(line): return f'<span style="color:{color}">{line}</span>' return line # default — white def on_log_line(self, line: str): self.log_box.append(colorize(line)) # Auto-scroll to bottom sb = self.log_box.verticalScrollBar() sb.setValue(sb.maximum())
# Color mapping — Regex-based, no AI required
import re
from PyQt6.QtGui import QColor COLOR_MAP = [ (re.compile(r'\berror\b', re.I), "#FF5555"), # red (re.compile(r'\bwarning\b', re.I), "#FFB86C"), # amber (re.compile(r'\bSetting up\b|\bProcessing\b'), "#50FA7B"), # green (re.compile(r'^--'), "#8BE9FD"), # cyan for steps
] def colorize(line: str) -> str: for pattern, color in COLOR_MAP: if pattern.search(line): return f'<span style="color:{color}">{line}</span>' return line # default — white def on_log_line(self, line: str): self.log_box.append(colorize(line)) # Auto-scroll to bottom sb = self.log_box.verticalScrollBar() sb.setValue(sb.maximum())
# Color mapping — Regex-based, no AI required
import re
from PyQt6.QtGui import QColor COLOR_MAP = [ (re.compile(r'\berror\b', re.I), "#FF5555"), # red (re.compile(r'\bwarning\b', re.I), "#FFB86C"), # amber (re.compile(r'\bSetting up\b|\bProcessing\b'), "#50FA7B"), # green (re.compile(r'^--'), "#8BE9FD"), # cyan for steps
] def colorize(line: str) -> str: for pattern, color in COLOR_MAP: if pattern.search(line): return f'<span style="color:{color}">{line}</span>' return line # default — white def on_log_line(self, line: str): self.log_box.append(colorize(line)) # Auto-scroll to bottom sb = self.log_box.verticalScrollBar() sb.setValue(sb.maximum())
self._cursor_visible = True
self._cursor_timer = QTimer()
self._cursor_timer.timeout.connect(self._blink_cursor)
self._cursor_timer.-weight: 500;">start(500) # 500ms interval def _blink_cursor(self): self._cursor_visible = not self._cursor_visible self.cursor_label.setText("█" if self._cursor_visible else " ")
self._cursor_visible = True
self._cursor_timer = QTimer()
self._cursor_timer.timeout.connect(self._blink_cursor)
self._cursor_timer.-weight: 500;">start(500) # 500ms interval def _blink_cursor(self): self._cursor_visible = not self._cursor_visible self.cursor_label.setText("█" if self._cursor_visible else " ")
self._cursor_visible = True
self._cursor_timer = QTimer()
self._cursor_timer.timeout.connect(self._blink_cursor)
self._cursor_timer.-weight: 500;">start(500) # 500ms interval def _blink_cursor(self): self._cursor_visible = not self._cursor_visible self.cursor_label.setText("█" if self._cursor_visible else " ")
# 1. Confirm ghost.qss is inside the bundle
dpkg -c dotghostboard_1.5.3_amd64.deb | grep ui/
# Expected: ./opt/dotghostboard/_internal/ui/ghost.qss # 2. Verify SUID bit post--weight: 500;">install
ls -l /usr/bin/pkexec
# Expected: -rwsr-xr-x (the 's' in position 4 is the SUID bit) # 3. Test resource_path in frozen mode (AppImage)
./DotGhostBoard-1.5.3-x86_64.AppImage --debug-paths
# Should print resolved paths for ghost.qss and icon_256.png
# 1. Confirm ghost.qss is inside the bundle
dpkg -c dotghostboard_1.5.3_amd64.deb | grep ui/
# Expected: ./opt/dotghostboard/_internal/ui/ghost.qss # 2. Verify SUID bit post--weight: 500;">install
ls -l /usr/bin/pkexec
# Expected: -rwsr-xr-x (the 's' in position 4 is the SUID bit) # 3. Test resource_path in frozen mode (AppImage)
./DotGhostBoard-1.5.3-x86_64.AppImage --debug-paths
# Should print resolved paths for ghost.qss and icon_256.png
# 1. Confirm ghost.qss is inside the bundle
dpkg -c dotghostboard_1.5.3_amd64.deb | grep ui/
# Expected: ./opt/dotghostboard/_internal/ui/ghost.qss # 2. Verify SUID bit post--weight: 500;">install
ls -l /usr/bin/pkexec
# Expected: -rwsr-xr-x (the 's' in position 4 is the SUID bit) # 3. Test resource_path in frozen mode (AppImage)
./DotGhostBoard-1.5.3-x86_64.AppImage --debug-paths
# Should print resolved paths for ghost.qss and icon_256.png - Isolated vault.db — separate file, separate connection, locked when idle
- Pattern-based secret detection — JWT, AWS keys (AKIA...), GitHub tokens (ghp_...), high-entropy hex — shape-based, not keyword-based
- Auto-clear: wipes clipboard 30 seconds after a Vault paste
- Paranoia Mode: suspends all DB writes on demand