diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..63745b6 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,174 @@ +# Copilot Instructions for NiimPrintX + +## Project Overview +NiimPrintX is a Python library for interfacing with NiimBot label printers via Bluetooth. It provides both CLI and GUI interfaces for designing and printing labels. + +## Requirements +- Python 3.12 or later +- ImageMagick library +- Poetry for dependency management + +## Setup for Testing + +### 1. Install System Dependencies + +**Linux (Ubuntu/Debian):** +```bash +sudo apt-get update +sudo apt-get install -y imagemagick libmagickwand-dev python3-tk xvfb libcairo2-dev pkg-config +``` + +**macOS:** +```bash +brew install libffi glib gobject-introspection cairo pkg-config imagemagick +export PKG_CONFIG_PATH="/usr/local/opt/libffi/lib/pkgconfig" +export LDFLAGS="-L/usr/local/opt/libffi/lib" +export CFLAGS="-I/usr/local/opt/libffi/include" +``` + +### 2. Install Python Dependencies + +```bash +# Using pip (Linux/CI) +pip install -r requirements-ci.txt + +# Using pip (macOS - includes pyobjc packages) +pip install -r requirements.txt + +# Or using Poetry (recommended) +python -m venv venv +poetry install +``` + +**Note:** `requirements.txt` includes macOS-specific packages (pyobjc-*) which will fail on Linux. Use `requirements-ci.txt` for Linux/CI environments. + +### 3. Running the Application + +**GUI Application:** +```bash +python -m NiimPrintX.ui +``` + +**CLI Application:** +```bash +# Print command +python -m NiimPrintX.cli print -m d110 -d 3 -n 1 -r 90 -i path/to/image.png + +# Info command +python -m NiimPrintX.cli info -m d110 +``` + +## Testing Guidelines + +### UI Testing (Headless Environment) + +For GUI testing in CI/CD or headless environments: + +```bash +# Start virtual display +Xvfb :99 -screen 0 1024x768x24 & +export DISPLAY=:99 + +# Run the UI +python -m NiimPrintX.ui +``` + +### Linting + +```bash +# Install flake8 +pip install flake8 + +# Run linter (critical errors only) +flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + +# Run full lint (warnings as info) +flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics +``` + +### Security Scanning + +Use CodeQL for security analysis: +```bash +# CodeQL analysis is configured in GitHub Actions +# Check .github/workflows/python-app.yml +``` + +## Supported Printer Models +- D11 +- D110 +- B21 +- B1 +- B18 + +## Key Modules + +### UI Components +- `NiimPrintX/ui/main.py` - Main application window +- `NiimPrintX/ui/widget/TextTab.py` - Text input tab +- `NiimPrintX/ui/widget/TextOperation.py` - Text rendering operations +- `NiimPrintX/ui/widget/PrintOption.py` - Print settings + +### CLI Components +- `NiimPrintX/cli/__main__.py` - CLI entry point + +### Printer Interface +- `NiimPrintX/nimmy/printer.py` - Printer communication + +## Testing Multi-line Text Feature + +```python +import tkinter as tk +from NiimPrintX.ui.main import LabelPrinterApp + +# Create app +app = LabelPrinterApp() +app.load_resources() + +# Access text tab +text_tab = app.text_tab + +# Test multi-line input +multi_line_text = "Line 1\nLine 2\nLine 3" +text_tab.content_entry.delete("1.0", tk.END) +text_tab.content_entry.insert("1.0", multi_line_text) + +# Add to canvas +text_tab.add_button.invoke() + +# Verify +assert len(app.app_config.text_items) > 0 +``` + +## Common Issues + +### Missing tkinter +```bash +sudo apt-get install python3-tk # Linux +# tkinter is included with Python on macOS/Windows +``` + +### Missing ImageMagick +```bash +# Linux +sudo apt-get install imagemagick libmagickwand-dev + +# macOS +brew install imagemagick +``` + +### Display not available (for testing) +```bash +# Use Xvfb for headless testing +Xvfb :99 -screen 0 1024x768x24 & +export DISPLAY=:99 +``` + +## CI/CD Integration + +The project uses GitHub Actions with the following workflows: +- `python-app.yml` - Linting and testing +- `tag.yaml` - Release builds +- Build workflows for Linux, macOS, and Windows + +See `.github/workflows/` for configuration details. diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml new file mode 100644 index 0000000..7847ce1 --- /dev/null +++ b/.github/workflows/python-app.yml @@ -0,0 +1,44 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python application + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v3 + with: + python-version: "3.12" + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y imagemagick libmagickwand-dev python3-tk xvfb libcairo2-dev pkg-config + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + # Install core dependencies (excluding macOS-specific packages) + if [ -f requirements-ci.txt ]; then pip install -r requirements-ci.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest || echo "No tests found, skipping" diff --git a/NiimPrintX/ui/widget/TextOperation.py b/NiimPrintX/ui/widget/TextOperation.py index 41fe734..e4db567 100644 --- a/NiimPrintX/ui/widget/TextOperation.py +++ b/NiimPrintX/ui/widget/TextOperation.py @@ -29,13 +29,14 @@ def create_text_image(self, font_props, text): draw.text_kerning = font_props["kerning"] draw.fill_color = Color('black') # Set font color to black draw.resolution = (300, 300) # 300 DPI for high quality text rendering - metrics = draw.get_font_metrics(WandImage(width=1, height=1), text, multiline=False) + metrics = draw.get_font_metrics(WandImage(width=1, height=1), text, multiline=True) text_width = int(metrics.text_width) + 5 text_height = int(metrics.text_height) + 5 # Create a new WandImage with WandImage(width=text_width, height=text_height, background=Color('transparent')) as img: - draw.text(x=2, y=int(text_height / 2 + metrics.ascender / 2), body=text) + # Position text at top using ascender for proper multi-line rendering + draw.text(x=2, y=int(metrics.ascender), body=text) draw(img) # Ensure the image is in RGBA format @@ -47,8 +48,8 @@ def create_text_image(self, font_props, text): tk_image = tk.PhotoImage(data=img_blob) return tk_image def add_text_to_canvas(self): - # Get the current text in the content_entry Entry widget - text = self.parent.content_entry.get() + # Get the current text in the content_entry Text widget + text = self.parent.content_entry.get("1.0", "end-1c") font_obj, font_props = self.parent.get_font_properties() if not text: @@ -87,8 +88,8 @@ def update_widgets(self, text_id): font_prop = self.config.text_items[text_id]['font_props'] text = self.config.text_items[text_id]['content'] - self.parent.content_entry.delete(0, tk.END) - self.parent.content_entry.insert(0, text) + self.parent.content_entry.delete("1.0", tk.END) + self.parent.content_entry.insert("1.0", text) self.parent.font_family_dropdown.set(font_prop["family"]) # self.parent.font_dropdown.set(font_prop["font"]) @@ -113,7 +114,7 @@ def update_widgets(self, text_id): self.parent.add_button.config(text="Update", command=lambda t_id=text_id: self.update_canvas_text(t_id)) def update_canvas_text(self, text_id): - text = self.parent.content_entry.get() + text = self.parent.content_entry.get("1.0", "end-1c") self.config.text_items[text_id]['content'] = text font_props = self.config.text_items[text_id]['font_props'] tk_image = self.create_text_image(font_props, text) diff --git a/NiimPrintX/ui/widget/TextTab.py b/NiimPrintX/ui/widget/TextTab.py index 2cd2c81..1b5c55f 100644 --- a/NiimPrintX/ui/widget/TextTab.py +++ b/NiimPrintX/ui/widget/TextTab.py @@ -26,10 +26,22 @@ def create_widgets(self): elif self.config.os_system == "Windows": default_bg = 'systemButtonFace' - tk.Label(self.frame, text="Content", bg=default_bg).grid(row=0, column=0, sticky='w') - self.content_entry = tk.Entry(self.frame, highlightbackground=default_bg) - self.content_entry.grid(row=0, column=1, sticky='ew', padx=5) - self.content_entry.insert(0, "Text") + # Content label and multi-line text entry with scrollbar + tk.Label(self.frame, text="Content", bg=default_bg).grid(row=0, column=0, sticky='nw') + + # Create frame to hold text widget and scrollbar + text_frame = tk.Frame(self.frame, bg=default_bg) + text_frame.grid(row=0, column=1, sticky='ew', padx=5) + + self.content_entry = tk.Text(text_frame, highlightbackground=default_bg, height=3, width=30) + self.content_entry.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # Add scrollbar for longer text + scrollbar = tk.Scrollbar(text_frame, command=self.content_entry.yview) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + self.content_entry.config(yscrollcommand=scrollbar.set) + + self.content_entry.insert("1.0", "Text") self.sample_text_label = tk.Label(self.frame, text="Sample Text", font=('Arial', 14), bg=default_bg) self.sample_text_label.grid(row=0, column=2, sticky='w', columnspan=3) @@ -96,14 +108,14 @@ def update_font_list(self, event=None): # self.font_dropdown['values'] = list(self.fonts[font_family]["fonts"].keys()) # self.font_dropdown.current(0) - content = self.content_entry.get() + content = self.content_entry.get("1.0", "end-1c") label_font = tk_font.Font(family=font_family, size=14) self.sample_text_label.config(font=label_font, text=f"{content} in {font_family}") def update_text_properties(self, event=None, widget_name=None): font_obj, font_props = self.get_font_properties() - content = self.content_entry.get() + content = self.content_entry.get("1.0", "end-1c") label_font = tk_font.Font(family=font_props['family'], size=14, weight=font_props['weight'], slant=font_props['slant']) self.sample_text_label.config(font=label_font, diff --git a/requirements-ci.txt b/requirements-ci.txt new file mode 100644 index 0000000..2639ed3 --- /dev/null +++ b/requirements-ci.txt @@ -0,0 +1,19 @@ +# Core dependencies for CI/CD (platform-agnostic) +# Excludes macOS-specific packages (pyobjc-*) + +appdirs==1.4.4 +asttokens==2.4.1 +bleak==0.21.1 +click==8.1.7 +devtools==0.12.2 +executing==2.0.1 +loguru==0.7.2 +markdown-it-py==3.0.0 +mdurl==0.1.2 +packaging==24.0 +pillow==10.3.0 +pycairo==1.26.0 +Pygments==2.17.2 +rich==13.7.1 +six==1.16.0 +Wand==0.6.13