diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000..edee078c --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,37 @@ +# Changesets + +Add a `.md` file here in any PR that has user-visible changes. +Name it anything — `my-feature.md`, `fix-datatable.md`, etc. + +When the PR is merged with a `release:*` label, `auto-release.yml` +folds all changeset files into `CHANGELOG.md` under the new version +and deletes the files. + +## Format + +Use the same headings as CHANGELOG.md: + +```markdown +### New Features + +#### Canvas Table +- **Feature name** — What it does and why it's useful + +#### SQL Editor +- **Another feature** — Description + +### Bug Fixes +- Fixed something that was broken + +### Changes +- Removed or changed behaviour +``` + +Supported top-level headings (case-insensitive): +- `### New Features` — shows as ✨ New Features in the app +- `### Bug Fixes` — shows as 🐛 Bug Fixes +- `### Changes` — shows as 🔧 Improvements + +Sub-sections (`####`) are optional but group items nicely in the release notes page. + +If a PR has no user-visible changes (docs, CI, refactors), skip the changeset file. diff --git a/.changeset/improvements-and-fixes.md b/.changeset/improvements-and-fixes.md new file mode 100644 index 00000000..a73c070f --- /dev/null +++ b/.changeset/improvements-and-fixes.md @@ -0,0 +1,17 @@ +### New Features + +#### Release Pipeline + +- **In-app release notes** — the update dialog now shows a structured release notes page with ✨ New Features, 🐛 Bug Fixes, and 🔧 Improvements sections parsed from the changelog +- **Code signing** — builds are now signed on macOS (Developer ID + notarization) and Windows (certificate thumbprint via cert store) in CI + +### Bug Fixes +- Fixed rel columns remaining visible in the canvas after being hidden from the columns panel — the toolbar was capped at showing 5 rel entries, so hiding all 5 caused DataTable to promote the next batch of FK tables into view with no way to hide them +- Fixed the `rel` badge not dimming visually when a relationship column is toggled to hidden in the columns panel +- Fixed the "Hide all" button in the columns panel not including relationship columns — clicking Hide now hides both regular and rel columns together + +### Changes +- Renamed app references from **DB Studio** to **Stroke** across all files, workflows, and documentation +- Updated domain from `dbstudio.app` to `stroke.app` in license gate and about dialog +- Updated GitHub repo references from `broisnischal/studio` to `broisnischal/stroke` in updater endpoint, release workflow, and README +- Updated macOS bundle artifact name from `db-studio.app` to `Stroke.app` and all installer filenames from `db-studio_*` to `stroke_*` to match the `productName` in tauri config diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 00000000..11c08d88 --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,185 @@ +# Triggered when a PR is merged into master. +# +# Label the PR with one of: +# release:patch → 0.0.x (bug fixes) +# release:minor → 0.x.0 (new features, backwards-compatible) +# release:major → x.0.0 (breaking changes) +# +# The workflow will: +# 1. Bump the version in tauri.conf.json + package.json +# 2. Fold .changeset/*.md files into CHANGELOG.md under the new version +# 3. Commit the bump + changelog to master +# 4. Push tag vX.Y.Z → triggers release.yml to build and publish + +name: Auto Release + +on: + pull_request: + types: [closed] + branches: [master] + +permissions: + contents: write + +jobs: + auto-release: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + # If master has branch protection that blocks the default bot token, + # replace with: token: ${{ secrets.RELEASE_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + # ── 1. Determine bump type from PR labels ───────────────────────────── + - name: Resolve bump type + id: bump + run: | + LABELS='${{ toJson(github.event.pull_request.labels.*.name) }}' + echo "PR labels: $LABELS" + + if echo "$LABELS" | grep -q '"release:major"'; then echo "type=major" >> "$GITHUB_OUTPUT" + elif echo "$LABELS" | grep -q '"release:minor"'; then echo "type=minor" >> "$GITHUB_OUTPUT" + elif echo "$LABELS" | grep -q '"release:patch"'; then echo "type=patch" >> "$GITHUB_OUTPUT" + else + echo "type=skip" >> "$GITHUB_OUTPUT" + echo "No release:* label — skipping release." + fi + + # ── 2. Bump versions + fold changesets into CHANGELOG.md ────────────── + - name: Bump version and update changelog + if: steps.bump.outputs.type != 'skip' + id: version + env: + BUMP_TYPE: ${{ steps.bump.outputs.type }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + node - <<'EOF' + const fs = require('fs') + const path = require('path') + const bump = process.env.BUMP_TYPE + + // ── Compute new version ────────────────────────────────────────── + const cfgPath = 'src-tauri/tauri.conf.json' + const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')) + const [maj, min, pat] = cfg.version.split('.').map(Number) + + const next = + bump === 'major' ? `${maj + 1}.0.0` : + bump === 'minor' ? `${maj}.${min + 1}.0` : + `${maj}.${min}.${pat + 1}` + + // ── Update tauri.conf.json ─────────────────────────────────────── + cfg.version = next + fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n') + + // ── Update package.json ────────────────────────────────────────── + const pkgPath = 'package.json' + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) + pkg.version = next + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n') + + // ── Collect .changeset/*.md files ──────────────────────────────── + const csDir = '.changeset' + const files = fs.existsSync(csDir) + ? fs.readdirSync(csDir).filter(f => f.endsWith('.md') && f !== 'README.md') + : [] + + // sections: { 'New Features': { '#### Canvas Table': ['- item'] } } + /** @type {Record>} */ + const sections = {} + + for (const file of files) { + const content = fs.readFileSync(path.join(csDir, file), 'utf8') + let h3 = null + let h4 = null + + for (const line of content.split('\n')) { + const t = line.trim() + if (t.startsWith('### ')) { + h3 = t.slice(4).trim() + if (!sections[h3]) sections[h3] = {} + h4 = null + } else if (t.startsWith('#### ')) { + h4 = t // keep full "#### Foo" + if (h3 && !sections[h3][h4]) sections[h3][h4] = [] + } else if (/^[-*]\s/.test(t) && h3) { + const key = h4 ?? '__top__' + if (!sections[h3][key]) sections[h3][key] = [] + sections[h3][key].push(t) + } + } + } + + // ── Build the new CHANGELOG entry ──────────────────────────────── + const today = new Date().toISOString().slice(0, 10) + const order = ['New Features', 'Bug Fixes', 'Changes'] + const allKeys = [...new Set([...order, ...Object.keys(sections)])] + let entry = `## [${next}] - ${today}\n` + let hasContent = false + + for (const heading of allKeys) { + if (!sections[heading] || Object.keys(sections[heading]).length === 0) continue + hasContent = true + entry += `\n### ${heading}\n` + + // Top-level bullets (no sub-section) + const top = sections[heading]['__top__'] ?? [] + for (const item of top) entry += `${item}\n` + + // Sub-sections + for (const [sub, items] of Object.entries(sections[heading])) { + if (sub === '__top__') continue + entry += `\n${sub}\n` + for (const item of items) entry += `${item}\n` + } + } + + if (!hasContent) { + // No changeset files — generate a minimal entry from PR title + const pr = process.env.PR_TITLE || 'No changelog provided' + const num = process.env.PR_NUMBER + entry += `\n### Changes\n- ${pr}${num ? ` (#${num})` : ''}\n` + } + + // ── Inject at top of CHANGELOG.md (after the "---" separator) ─── + const clPath = 'CHANGELOG.md' + const cl = fs.existsSync(clPath) ? fs.readFileSync(clPath, 'utf8') : '# Changelog\n\n---\n' + const sep = '---\n' + const idx = cl.indexOf(sep) + const before = idx !== -1 ? cl.slice(0, idx + sep.length) : cl + const after = idx !== -1 ? cl.slice(idx + sep.length) : '' + fs.writeFileSync(clPath, `${before}\n${entry}\n${after}`) + + // ── Delete consumed changeset files ────────────────────────────── + for (const file of files) fs.unlinkSync(path.join(csDir, file)) + + console.log(`v${next} — merged ${files.length} changeset(s)`) + fs.appendFileSync(process.env.GITHUB_OUTPUT, `new_version=${next}\n`) + EOF + + # ── 3. Commit everything and push the release tag ───────────────────── + - name: Commit and tag + if: steps.bump.outputs.type != 'skip' + env: + NEW_TAG: v${{ steps.version.outputs.new_version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add src-tauri/tauri.conf.json package.json CHANGELOG.md .changeset/ + git commit -m "chore: release ${{ steps.version.outputs.new_version }}" + + git tag "$NEW_TAG" + git push origin master + git push origin "$NEW_TAG" + + echo "✅ $NEW_TAG pushed — release.yml will now build and publish." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a684b7b..62d47074 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -# Build DB Studio for Linux, Windows, and macOS and publish to GitHub Releases. +# Build Stroke for Linux, Windows, and macOS and publish to GitHub Releases. # # Create a release: # git tag v0.1.0 @@ -66,12 +66,12 @@ jobs: ### Install **macOS** - - Apple Silicon: \`db-studio_*_aarch64.dmg\` - - Intel Mac: \`db-studio_*_x64.dmg\` + - Apple Silicon: \`stroke_*_aarch64.dmg\` + - Intel Mac: \`stroke_*_x64.dmg\` - > **"damaged and can't be opened"?** Run once: \`xattr -cr /Applications/db-studio.app\` + > **"damaged and can't be opened"?** Run once: \`xattr -cr /Applications/Stroke.app\` - **Linux:** \`.deb\` · \`.rpm\` · \`.AppImage\` · \`.pkg.tar.zst\` (Arch) + **Linux:** \`.deb\` · \`.rpm\` · \`.AppImage\` **Windows:** \`.exe\` NSIS setup (recommended) · \`.msi\` @@ -79,7 +79,7 @@ jobs: EOF gh release create "$TAG" \ - --title "DB Studio $TAG" \ + --title "Stroke $TAG" \ --target "$SHA" \ --notes-file /tmp/notes.md \ $PRERELEASE @@ -132,12 +132,64 @@ jobs: - run: npm ci + # ── macOS: set proper signing identity when cert secret is present ──────── + - name: Configure macOS code signing + if: startsWith(matrix.name, 'macos') + env: + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + run: | + if [ -z "$APPLE_SIGNING_IDENTITY" ]; then + echo "APPLE_SIGNING_IDENTITY not set — keeping ad-hoc signing" + else + node -e " + const fs = require('fs'); + const cfg = JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json', 'utf8')); + cfg.bundle.macOS.signingIdentity = process.env.APPLE_SIGNING_IDENTITY; + fs.writeFileSync('src-tauri/tauri.conf.json', JSON.stringify(cfg, null, 2)); + console.log('Signing identity set to:', process.env.APPLE_SIGNING_IDENTITY); + " + fi + + # ── Windows: import PFX into cert store, patch config with thumbprint ───── + - name: Import Windows signing certificate + if: matrix.name == 'windows' + shell: pwsh + env: + WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} + WINDOWS_CERTIFICATE_PWD: ${{ secrets.WINDOWS_CERTIFICATE_PWD }} + run: | + if (-not $env:WINDOWS_CERTIFICATE) { + Write-Host "WINDOWS_CERTIFICATE not set — skipping Windows signing" + exit 0 + } + $certBytes = [System.Convert]::FromBase64String($env:WINDOWS_CERTIFICATE) + $certPath = Join-Path $env:TEMP "signing.pfx" + [System.IO.File]::WriteAllBytes($certPath, $certBytes) + $secPwd = ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PWD -Force -AsPlainText + $cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password $secPwd + Remove-Item $certPath -Force + + $env:WIN_CERT_THUMB = $cert.Thumbprint + node -e " + const fs = require('fs'); + const cfg = JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json', 'utf8')); + cfg.bundle.windows.certificateThumbprint = process.env.WIN_CERT_THUMB; + fs.writeFileSync('src-tauri/tauri.conf.json', JSON.stringify(cfg, null, 2)); + console.log('Certificate thumbprint set:', process.env.WIN_CERT_THUMB); + " + - name: Build run: npx tauri build ${{ matrix.args }} env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} NO_STRIP: ${{ matrix.name == 'linux' && '1' || '' }} + # macOS code signing + notarization (Tauri auto-imports APPLE_CERTIFICATE) + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} # ── macOS aarch64 ─────────────────────────────────────────────────────── - name: Upload macOS aarch64 artifacts @@ -153,16 +205,16 @@ jobs: DIR="src-tauri/target/aarch64-apple-darwin/release/bundle" pushd "$DIR/macos" - tar czf "db-studio.app.tar.gz" "db-studio.app" + tar czf "Stroke.app.tar.gz" "Stroke.app" popd - npx tauri signer sign "$DIR/macos/db-studio.app.tar.gz" + npx tauri signer sign "$DIR/macos/Stroke.app.tar.gz" - cp "$DIR/macos/db-studio.app.tar.gz" db-studio_aarch64.app.tar.gz - cp "$DIR/macos/db-studio.app.tar.gz.sig" updater.sig + cp "$DIR/macos/Stroke.app.tar.gz" stroke_aarch64.app.tar.gz + cp "$DIR/macos/Stroke.app.tar.gz.sig" updater.sig gh release upload "$TAG" \ - "$DIR/dmg/db-studio_${VERSION}_aarch64.dmg" \ - db-studio_aarch64.app.tar.gz \ + "$DIR/dmg/stroke_${VERSION}_aarch64.dmg" \ + stroke_aarch64.app.tar.gz \ --clobber # ── macOS x64 ─────────────────────────────────────────────────────────── @@ -179,16 +231,16 @@ jobs: DIR="src-tauri/target/x86_64-apple-darwin/release/bundle" pushd "$DIR/macos" - tar czf "db-studio.app.tar.gz" "db-studio.app" + tar czf "Stroke.app.tar.gz" "Stroke.app" popd - npx tauri signer sign "$DIR/macos/db-studio.app.tar.gz" + npx tauri signer sign "$DIR/macos/Stroke.app.tar.gz" - cp "$DIR/macos/db-studio.app.tar.gz" db-studio_x64.app.tar.gz - cp "$DIR/macos/db-studio.app.tar.gz.sig" updater.sig + cp "$DIR/macos/Stroke.app.tar.gz" stroke_x64.app.tar.gz + cp "$DIR/macos/Stroke.app.tar.gz.sig" updater.sig gh release upload "$TAG" \ - "$DIR/dmg/db-studio_${VERSION}_x64.dmg" \ - db-studio_x64.app.tar.gz \ + "$DIR/dmg/stroke_${VERSION}_x64.dmg" \ + stroke_x64.app.tar.gz \ --clobber # ── Linux ─────────────────────────────────────────────────────────────── @@ -205,18 +257,18 @@ jobs: DIR="src-tauri/target/release/bundle" pushd "$DIR/appimage" - tar czf "db-studio_${VERSION}_amd64.AppImage.tar.gz" "db-studio_${VERSION}_amd64.AppImage" + tar czf "stroke_${VERSION}_amd64.AppImage.tar.gz" "stroke_${VERSION}_amd64.AppImage" popd - npx tauri signer sign "$DIR/appimage/db-studio_${VERSION}_amd64.AppImage.tar.gz" + npx tauri signer sign "$DIR/appimage/stroke_${VERSION}_amd64.AppImage.tar.gz" - cp "$DIR/appimage/db-studio_${VERSION}_amd64.AppImage.tar.gz" db-studio_amd64.AppImage.tar.gz - cp "$DIR/appimage/db-studio_${VERSION}_amd64.AppImage.tar.gz.sig" updater.sig + cp "$DIR/appimage/stroke_${VERSION}_amd64.AppImage.tar.gz" stroke_amd64.AppImage.tar.gz + cp "$DIR/appimage/stroke_${VERSION}_amd64.AppImage.tar.gz.sig" updater.sig gh release upload "$TAG" \ - "$DIR/deb/db-studio_${VERSION}_amd64.deb" \ - "$DIR/rpm/db-studio-${VERSION}-1.x86_64.rpm" \ - "$DIR/appimage/db-studio_${VERSION}_amd64.AppImage" \ - db-studio_amd64.AppImage.tar.gz \ + "$DIR/deb/stroke_${VERSION}_amd64.deb" \ + "$DIR/rpm/stroke-${VERSION}-1.x86_64.rpm" \ + "$DIR/appimage/stroke_${VERSION}_amd64.AppImage" \ + stroke_amd64.AppImage.tar.gz \ --clobber # ── Windows ───────────────────────────────────────────────────────────── @@ -232,8 +284,8 @@ jobs: $version = (Get-Content src-tauri/tauri.conf.json | ConvertFrom-Json).version $dir = "src-tauri/target/release/bundle" - $exe = "$dir/nsis/db-studio_${version}_x64-setup.exe" - $zip = "$dir/nsis/db-studio_${version}_x64-setup.nsis.zip" + $exe = "$dir/nsis/stroke_${version}_x64-setup.exe" + $zip = "$dir/nsis/stroke_${version}_x64-setup.nsis.zip" # Use NoCompression (Store = method 0) — universally supported by every # ZIP implementation including Tauri's zip crate. @@ -264,13 +316,13 @@ jobs: npx tauri signer sign $zip - Copy-Item $zip "db-studio_x64-setup.nsis.zip" + Copy-Item $zip "stroke_x64-setup.nsis.zip" Copy-Item "$zip.sig" "updater.sig" gh release upload "$env:TAG" ` - "$dir/msi/db-studio_${version}_x64_en-US.msi" ` - "$dir/nsis/db-studio_${version}_x64-setup.exe" ` - "db-studio_x64-setup.nsis.zip" ` + "$dir/msi/stroke_${version}_x64_en-US.msi" ` + "$dir/nsis/stroke_${version}_x64-setup.exe" ` + "stroke_x64-setup.nsis.zip" ` --clobber - name: Save updater sig as artifact @@ -280,71 +332,6 @@ jobs: path: updater.sig if-no-files-found: warn - build-arch: - needs: create-release - runs-on: ubuntu-latest - name: build-linux-arch - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - - - run: npm ci - - - name: Build pacman package inside Arch container - env: - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - run: | - docker run --rm \ - --volume "$GITHUB_WORKSPACE:/workspace" \ - --workdir /workspace \ - --env TAURI_SIGNING_PRIVATE_KEY \ - --env TAURI_SIGNING_PRIVATE_KEY_PASSWORD \ - --env NO_STRIP=1 \ - archlinux/archlinux:latest \ - bash -uec ' - # Refresh package database and install build dependencies - pacman -Syu --noconfirm - pacman -S --noconfirm --needed \ - base-devel \ - nodejs \ - npm \ - rustup \ - webkit2gtk-4.1 \ - libayatana-appindicator \ - librsvg \ - patchelf - - # makepkg refuses to run as root; patch the check — safe in CI containers. - sed -i "s/if (( EUID == 0 ))/if (( 0 ))/" /usr/bin/makepkg - - # Set up stable Rust toolchain. - # The Arch rustup package does not create ~/.cargo/env (that is only - # produced by the rustup.rs installer script). Export the bin path directly. - rustup toolchain install stable --no-self-update - rustup default stable - export PATH="$HOME/.cargo/bin:$PATH" - - # node_modules were already installed on the host via "npm ci"; - # the workspace is bind-mounted so they are available here. - npx tauri build --bundles pacman - ' - - - name: Upload pacman artifact - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ env.RELEASE_TAG }} - run: | - VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version") - gh release upload "$TAG" \ - "src-tauri/target/release/bundle/pacman/db-studio-${VERSION}-1-x86_64.pkg.tar.zst" \ - --clobber - # ── 3. Combine signatures into latest.json ─────────────────────────────────── publish-updater-json: needs: build @@ -362,7 +349,7 @@ jobs: TAG: ${{ env.RELEASE_TAG }} run: | export VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version") - export BASE="https://github.com/broisnischal/studio/releases/download/${TAG}" + export BASE="https://github.com/broisnischal/stroke/releases/download/${TAG}" node - <<'JSEOF' const fs = require('fs') @@ -393,12 +380,12 @@ jobs: else console.warn(`⚠ no sig at ${sigFile} — skipping ${key}`) } - add('darwin-aarch64', 'sig-macos-aarch64/updater.sig', `${BASE}/db-studio_aarch64.app.tar.gz`) - add('darwin-x86_64', 'sig-macos-x64/updater.sig', `${BASE}/db-studio_x64.app.tar.gz`) - add('linux-x86_64', 'sig-linux/updater.sig', `${BASE}/db-studio_amd64.AppImage.tar.gz`) - add('windows-x86_64', 'sig-windows/updater.sig', `${BASE}/db-studio_x64-setup.nsis.zip`) + add('darwin-aarch64', 'sig-macos-aarch64/updater.sig', `${BASE}/stroke_aarch64.app.tar.gz`) + add('darwin-x86_64', 'sig-macos-x64/updater.sig', `${BASE}/stroke_x64.app.tar.gz`) + add('linux-x86_64', 'sig-linux/updater.sig', `${BASE}/stroke_amd64.AppImage.tar.gz`) + add('windows-x86_64', 'sig-windows/updater.sig', `${BASE}/stroke_x64-setup.nsis.zip`) - const notes = extractChangelog(VERSION) || `See https://github.com/broisnischal/studio/releases/tag/v${VERSION}` + const notes = extractChangelog(VERSION) || `See https://github.com/broisnischal/stroke/releases/tag/v${VERSION}` const out = { version: VERSION, diff --git a/.gitignore b/.gitignore index c5027684..cd745596 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,8 @@ packaging/aur/discerns/src/ packaging/aur/discerns/pkg/ packaging/aur/discerns/*.pkg.tar.zst packaging/aur/discerns/*.tar.zstlogs/ + +# Local secrets — never commit +.env +.env.local +.env.*.local diff --git a/AGENT.md b/AGENT.md index 1507bc59..07e63bad 100644 --- a/AGENT.md +++ b/AGENT.md @@ -1,10 +1,10 @@ -# AGENT.md — DB Studio +# AGENT.md — Stroke Instructions for AI agents working in this repository. Read this file before making changes. ## Project overview -**DB Studio** is a desktop database management app (Drizzle Studio–style) built with: +**Stroke** is a desktop database management app (Drizzle Studio–style) built with: | Layer | Stack | |-------|--------| @@ -175,7 +175,7 @@ Workflow: `.github/workflows/release.yml` | Tag push | `git tag v0.1.0 && git push origin v0.1.0` | | Manual | Actions → **Release** → Run workflow (set tag, e.g. `v0.1.0`) | -Builds **Linux** (`.deb`, `.AppImage`) on `ubuntu-22.04` and **Windows** (`.msi`) on `windows-latest`, then uploads assets to [GitHub Releases](https://github.com/broisnischal/studio/releases). +Builds **Linux** (`.deb`, `.AppImage`) on `ubuntu-22.04` and **Windows** (`.msi`) on `windows-latest`, then uploads assets to [GitHub Releases](https://github.com/broisnischal/stroke/releases). Local build: diff --git a/CHANGELOG.md b/CHANGELOG.md index a30233ea..5bceb070 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -All notable changes to DB Studio are listed here, newest first. +All notable changes to Stroke are listed here, newest first. --- diff --git a/CLAUDE.md b/CLAUDE.md index 79e58c29..7760f991 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,11 +1,11 @@ -# CLAUDE.md - DB Studio Project Guide +# CLAUDE.md - Stroke Project Guide This file is the source of truth for AI agents working in this repository. Use it to keep changes consistent with the current architecture and UI patterns. ## 1) Project snapshot -DB Studio is a Tauri desktop app for database work: +Stroke is a Tauri desktop app for database work: - connect to PostgreSQL, SQLite, Cloudflare D1, and MySQL flows in UI - browse schemas/tables and row counts diff --git a/README.md b/README.md index 448a15cf..578fde83 100644 --- a/README.md +++ b/README.md @@ -1,198 +1,108 @@ -# DB Studio +
-A desktop database client for PostgreSQL, SQLite, and Cloudflare D1. Browse tables, run SQL, edit rows inline, and connect AI tools directly to your database through a built-in MCP server. +# Stroke ---- - -## Download - -Go to the [Releases](../../releases) page and grab the installer for your platform: - -| Platform | File | -|----------|------| -| macOS (Apple Silicon) | `db-studio_x.x.x_aarch64.dmg` | -| macOS (Intel) | `db-studio_x.x.x_x64.dmg` | -| Windows | `db-studio_x.x.x_x64-setup.exe` | -| Linux (Debian/Ubuntu) | `db-studio_x.x.x_amd64.deb` | -| Linux (AppImage) | `db-studio_x.x.x_amd64.AppImage` | - -### macOS - -1. Open the `.dmg` file. -2. Drag **db-studio** into your Applications folder. -3. On first launch, right-click → Open if macOS shows an unverified developer warning. +**A fast, modern desktop database client.** -### Windows +Connect to PostgreSQL, MySQL, SQLite, Turso/LibSQL, and Cloudflare D1 — browse schemas, edit data inline, write SQL, visualize, and let AI tools talk to your database through a built-in MCP server. -Run the `.exe` installer and follow the prompts. No additional dependencies needed. - -### Linux (Debian/Ubuntu) - -```bash -sudo dpkg -i db-studio_*_amd64.deb -``` - -Then launch from your application menu or run `db-studio` in a terminal. - -### Linux (Arch) - -```bash -./db-studio_*_amd64.AppImage -``` +[Download](#install) · [Features](#features) · [Build from source](#build-from-source) · [Website](https://stroke.app) -Or build from source (see [Build from Source](#build-from-source) below). +
--- -## Connecting to a Database - -When you open DB Studio you'll see the connection screen. Choose your database type and fill in the details. - -### PostgreSQL - -| Field | Default | -|-------|---------| -| Host | `127.0.0.1` | -| Port | `5432` | -| Database | `postgres` | -| User | `postgres` | -| Password | _(your password)_ | -| SSL | off | - -Click **Test Connection** to verify before saving. - -### SQLite - -Enter the file path to your `.db` or `.sqlite` file. Use `:memory:` for an in-memory database. - -### Cloudflare D1 +## Supported databases -Enter your Cloudflare **Account ID**, **Database ID**, and **API Token** from the Cloudflare dashboard. - ---- - -Saved connections are stored locally on your machine. You can save as many connections as you like and switch between them at any time. DB Studio reopens your last-used connection automatically on the next launch. +| Database | Notes | +|----------|-------| +| **PostgreSQL** | Full schema, enums, sequences, triggers, indexes | +| **MySQL** | Standard host/port connections | +| **SQLite** | Local file or `:memory:` | +| **Turso / LibSQL** | Serverless SQLite at the edge | +| **Cloudflare D1** | Connect via OAuth or API token | --- ## Features -### Schema Explorer - -The left sidebar lists every schema in your database. Expand a schema to see all its tables, views, materialized views, and foreign tables — each showing a live row count. Use the search box to filter by name, or press **Cmd/Ctrl+K** to open the command palette and jump to any table instantly. - -### Table Data Grid - -Click any table to open a paginated data grid. You can: - -- Page through rows in steps of 25, 50, 100, 250, or up to 1,000 rows per page -- See total row count and current page position -- Resize columns by dragging their headers — widths are saved per table -- Hide or show individual columns from the toolbar -- Sort by any column (ascending or descending) by clicking its header - -### Filtering & Search +- **Schema explorer** — tables, views, materialized views, and foreign tables with live row counts, plus an index browser. +- **Data grid** — paginated browsing, resizable columns (saved per table), column show/hide, multi-column sort, full-text search, and a visual filter builder. +- **Inline editing** — type-aware editors for text, numbers, booleans, enums, dates, UUIDs, and JSON. Insert and delete rows with smart defaults. +- **Row inspector** — view any row in formatted, JSON, or preview mode. +- **Foreign keys** — click a value to jump to the referenced table with the filter applied. +- **SQL console** — Monaco editor with syntax highlighting, schema-aware autocomplete, formatting, execution time, and CSV/JSON export. +- **Query history & saved queries** — every query is saved automatically; bookmark the ones you keep. +- **Schema diagrams** — auto-generated ERDs of your tables and relationships. +- **Charts & dashboards** — turn query results into charts and pin them to a dashboard. +- **ORM runner** — run and preview ORM-style queries. +- **Docker launch** — spin up a local Postgres or MySQL container in one click. +- **Backup & restore** — export and import your data. +- **AI chat** — an assistant with direct access to your database that runs queries, explains schemas, generates SQL, and renders diagrams (works with any OpenAI-compatible API). +- **MCP server** — expose your database to Claude, Cursor, and other MCP clients (see below). +- **Command palette** — `Cmd/Ctrl+K` to jump anywhere instantly. -- **Search bar** — searches across all columns using a case-insensitive match -- **Filter builder** — add one or more column filters with operators: `equals`, `not equal`, `contains`, `does not contain`, `starts with`, `ends with`, `greater than`, `greater or equal`, `less than`, `less or equal`, `is null`, `is not null` - -Filters combine with AND logic. Sorting and filters work together. - -### Inline Cell Editing - -Double-click any cell to edit it in place. DB Studio knows the column type: - -- Text, numbers, and booleans each get the right input -- Enum columns show a dropdown of valid values -- Timestamp and date columns use a date/time picker -- UUID columns can be edited or regenerated -- JSON/JSONB columns are editable as text -- Binary (bytea) columns are display-only - -Changes are saved to the database immediately when you confirm. - -### Insert Rows - -Click **Insert row** to open the insert form. Every column is listed with its data type and whether it's required. DB Studio pre-fills sensible defaults: - -- Auto-increment and identity columns are skipped automatically -- `created_at` / `updated_at` style columns get the current timestamp -- UUID and CUID ID columns get a generated value -- Enum columns show a dropdown -- All NOT NULL columns with no default are marked as required - -### Delete Rows - -Select one or more rows using the checkboxes on the left, then click **Delete** in the toolbar. Single-row deletion is also available through the row context menu. Bulk deletes show a confirmation prompt. - -### Row Inspector - -Click any row to open the inspector panel on the right. It shows every column value in a readable layout. Switch between three views: - -- **Normal** — formatted values, one per line -- **JSON** — the full row as a JSON object -- **Preview** — large cell content or URL detection with external link opening - -The inspector panel is resizable and its state persists between sessions. - -### Foreign Key Navigation - -When a column is a foreign key, DB Studio detects the relationship and lets you click the cell value to jump to the referenced table with the matching filter already applied. +--- -### Index Browser +## Install -Open the **Indexes** tab in the schema explorer to see every index on your tables — including the type (B-tree, Hash, GIN, GIST, BRIN), the indexed columns, and whether it's unique or a primary key. +Grab the installer for your platform from the [Releases](https://github.com/broisnischal/stroke/releases) page. -### SQL Console +| Platform | File | +|----------|------| +| macOS (Apple Silicon) | `stroke_x.x.x_aarch64.dmg` | +| macOS (Intel) | `stroke_x.x.x_x64.dmg` | +| Windows | `stroke_x.x.x_x64-setup.exe` | +| Linux (Debian/Ubuntu) | `stroke_x.x.x_amd64.deb` | +| Linux (AppImage) | `stroke_x.x.x_amd64.AppImage` | -Press **Cmd/Ctrl+Shift+S** or open the SQL tab to get a full SQL editor: +**macOS** — open the `.dmg`, drag **Stroke** to Applications. On first launch, right-click → Open if you see an unverified developer warning. -- Syntax highlighting and schema-aware autocomplete -- Format your query automatically -- Run with **Cmd/Ctrl+Enter** -- See execution time and row count after each query -- Results appear in a table below — up to 5,000 rows, with a 30-second timeout to protect your connection -- Export results as CSV or JSON +**Windows** — run the `.exe` installer and follow the prompts. -### Query History & Saved Queries +**Linux (Debian/Ubuntu)** -Every query you execute is automatically saved to history. You can re-run any past query, or bookmark it as a saved query to keep it permanently. Both are accessible from the sidebar and the command palette. +```bash +sudo dpkg -i stroke_*_amd64.deb +``` -### Data Export +**Linux (AppImage)** -From either the table grid or the SQL results panel, export data as: +```bash +chmod +x stroke_*_amd64.AppImage +./stroke_*_amd64.AppImage +``` -- **CSV** — properly quoted and escaped -- **JSON** — formatted array of objects +--- -On desktop a native save dialog opens. On web it falls back to a browser download. +## Connecting -### AI Chat +Pick a database type on the connection screen and fill in the details. Hit **Test connection** to verify, then **Connect**. -Open the AI tab (**Cmd/Ctrl+Shift+A**) to chat with an AI assistant that has direct access to your database. The AI can: +- **PostgreSQL / MySQL** — host, port, database, user, password (optional SSL). You can also paste a full connection string and click **Parse**. +- **SQLite** — point to a `.db`/`.sqlite` file, or use an in-memory database. +- **Turso / LibSQL** — enter the database URL and (optionally) an auth token. +- **Cloudflare D1** — sign in with Cloudflare, or enter Account ID, Database ID, and an API token manually. -- Run queries and return real data in its answers -- Describe any table's schema -- Generate and explain SQL -- Render Mermaid diagrams and formatted Markdown +Connections are saved locally, and Stroke reopens your last one on launch. -Configure any OpenAI-compatible API (OpenAI, Mistral, Ollama, etc.) in Settings by entering your base URL, model name, and API key. +--- -### AI SQL Suggestions +## AI & MCP server -In the SQL editor, DB Studio can suggest relevant queries for the table you're viewing — common filters, joins to related tables, aggregations, and more. Click a suggestion to insert it into the editor. +Configure any OpenAI-compatible provider (OpenAI, Mistral, Ollama, …) in **Settings** with a base URL, model, and API key to enable AI chat and SQL suggestions. -### MCP Server (Connect AI Tools) +To connect external AI tools, Stroke ships a built-in **Model Context Protocol** server: -DB Studio includes a built-in **Model Context Protocol (MCP) server** that exposes your connected database to AI tools like Claude Desktop, Cursor, and any other MCP-compatible client. +1. Open **Settings → MCP Server**. +2. Click **Start**. +3. Copy the one-click config for Claude or Cursor into your tool's config. -1. Open **Settings → MCP Server** -2. Click **Start** — the server starts on port `39847` (or the next available port) -3. Copy the one-click config snippet for Claude or Cursor and paste it into your AI tool's config file +The server uses a stable bearer token, so you only configure it once. -The server uses bearer token authentication. The token is stable across restarts so you only need to configure your AI tool once. +--- -### Keyboard Shortcuts +## Keyboard shortcuts | Shortcut | Action | |----------|--------| @@ -201,35 +111,30 @@ The server uses bearer token authentication. The token is stable across restarts | `Cmd/Ctrl+Shift+S` | SQL console | | `Cmd/Ctrl+Shift+A` | AI chat | | `Cmd/Ctrl+Enter` | Run SQL query | -| `Cmd/Ctrl+?` | Show all shortcuts | | `Cmd/Ctrl+⌥+←/→` | Switch tabs | +| `Cmd/Ctrl+?` | Show all shortcuts | --- -## Build from Source +## Build from source -You'll need [Node.js](https://nodejs.org) (v18+) and the [Rust toolchain](https://rustup.rs). +Requires [Node.js](https://nodejs.org) 18+ and the [Rust toolchain](https://rustup.rs). ```bash -git clone https://github.com/broisnischal/studio -cd studio +git clone https://github.com/broisnischal/stroke +cd stroke npm install -npm run tauri dev -``` - -To build a release binary: - -```bash -npm run tauri build +npm run tauri # run in dev +npm run tauri:build # build a release binary ``` -**Arch Linux note:** The AppImage step can fail due to a linker incompatibility. Build only the `.deb` to avoid it: +**Arch Linux** — the AppImage step can fail due to a linker quirk. Build only the `.deb`: ```bash -npm run tauri build -- --bundles deb +npm run tauri:build -- --bundles deb ``` -Or use the included script that sets the right flag for AppImage on Arch: +…or use the included helper for AppImage on Arch: ```bash npm run tauri:build:arch @@ -237,6 +142,7 @@ npm run tauri:build:arch --- -## Reporting Issues +## Issues -Found a bug or want to request a feature? Open an issue at [github.com/broisnischal/studio/issues](https://github.com/broisnischal/studio/issues). +Found a bug or have a feature request? Open an issue at +[github.com/broisnischal/stroke/issues](https://github.com/broisnischal/stroke/issues). diff --git a/docs/canvas-zoom-resize-fix.md b/docs/canvas-zoom-resize-fix.md new file mode 100644 index 00000000..06165e91 --- /dev/null +++ b/docs/canvas-zoom-resize-fix.md @@ -0,0 +1,119 @@ +# Fix: Canvas table magnifies/blurs on macOS resize-handle hover + +**Status:** Resolved +**Platform:** macOS (WKWebView) — Windows/Linux unaffected +**Area:** `DataTable.svelte` canvas grid, app zoom, Tauri webview + +## Symptom + +Hovering or dragging a column resize handle made the **table alone** look huge +and blurry, while the sidebar and the rest of the UI stayed at normal size. + +The mismatch was the key tell: + +- The sidebar/UI scale via CSS (`--app-font-size`, derived from `--app-zoom`). +- The table is a ``. When it ballooned independently and looked + **blurry** (not crisp), a bitmap was being scaled by a layer *above* the app, + not the app's own renderer. + +## Root cause + +It was **WebKit page-zoom**, not the app's canvas zoom and not pinch +magnification. The diagnostic (`window.__zoomDiag()` while the table looked +wrong) confirmed it: + +- `visualViewport.scale` stayed at **1** (so not pinch magnification) +- `devicePixelRatio` **jumped** (the signature of page-zoom) +- macOS Accessibility → Zoom was **off** (so not a system setting) + +Two contributing paths fed it: + +1. **Tauri's injected zoom polyfill.** When `zoom_hotkeys_enabled` is on, Tauri + injects a script that listens on the **legacy `mousewheel` event** (not the + standard `wheel` event) and calls `set_webview_zoom` whenever the scroll has + `ctrlKey` set. On macOS a trackpad **pinch arrives as `ctrl`+wheel**, so a + stray finger movement near a resize handle page-zoomed the whole webview. + Earlier fixes only blocked `wheel`, which this polyfill never uses. + +2. **A separate, persisted canvas zoom.** `db-studio:canvas-zoom` in + `localStorage` could drift independently of the app's `settings.zoom`, so the + grid could stay "zoomed" across sessions while the sidebar did not. + +Because the resize hit-test lived **on the canvas header**, the pinch landed on +the canvas and triggered page-zoom before any JS guard could intervene. + +## The fix + +### 1. Column resize moved off the canvas to a DOM overlay +`src/lib/components/DataTable.svelte` + +- Resize handles are now absolutely-positioned `
`s (a sticky overlay at + `z-index: 2`) sitting on each column's right edge, derived from `geom`. +- The canvas no longer hit-tests or handles header resize pointer events. +- Each handle has `pointer-events: auto`, `touch-none`, shows `col-resize`, and + swallows `wheel` / `mousewheel` / `gesture*` while resetting webview zoom. + +### 2. Wheel/pinch zoom disabled app-wide — zoom is keyboard-only +`src/lib/stores/settings.js` + +- Global **capture-phase** listeners on `wheel`, `mousewheel`, and the WebKit + `gesturestart`/`gesturechange`/`gestureend` events call + `preventDefault()` + `stopImmediatePropagation()` (beats Tauri's polyfill), + then `resetWebviewZoom()`. +- Zoom is now driven **only** by `Cmd/Ctrl + = / - / 0`. +- `resetWebviewZoom()` snaps the native page-zoom back to `1` via + `getCurrentWebview().setZoom(1)`. + +### 3. App zoom is the single source of truth +`src/lib/stores/canvas-zoom.svelte.js`, `src/lib/stores/settings.js` + +- Removed the independent `db-studio:canvas-zoom` persistence. `zoomState` is + now a plain mirror of `settings.zoom`; `applySettings()` is the only writer. +- `applySettings()` clears the legacy `db-studio:canvas-zoom` key and calls + `resetWebviewZoom()` on every apply. + +### 4. devicePixelRatio watchdog +`src/main.js` + +- On launch and on every `visualViewport`/`window` resize, if + `visualViewport.scale !== 1` **or** `devicePixelRatio` drifts from baseline, + call `resetWebviewZoom()` and re-baseline (so a genuine monitor/DPI change + isn't fought). `window.__zoomDiag()` remains as a console probe. + +### 5. Native webview zoom disabled at the source +`src-tauri/src/lib.rs`, `src-tauri/Cargo.toml` + +- `.zoom_hotkeys_enabled(false)` — stops Tauri from injecting the `mousewheel` + zoom polyfill at all. +- macOS `with_webview`: `setAllowsMagnification(false)`, `setMagnification(1.0)`, + `setPageZoom(1.0)` via `objc2-web-kit`. + +## Files touched + +- `src/lib/components/DataTable.svelte` — DOM resize handles, canvas no longer + resizes columns, dpr re-sync of the backing store. +- `src/lib/stores/settings.js` — keyboard-only zoom, global wheel/pinch blockers, + `resetWebviewZoom()`. +- `src/lib/stores/canvas-zoom.svelte.js` — `zoomState` mirrors app zoom only. +- `src/main.js` — launch reset + dpr watchdog + `__zoomDiag()`. +- `src-tauri/src/lib.rs` — `zoom_hotkeys_enabled(false)`, WKWebView zoom off. +- `src-tauri/Cargo.toml` — `objc2-web-kit` dependency. + +## How to verify + +A **full restart** is required (`npm run tauri`) because Rust changes don't hot-reload. + +1. If the table is still large from a previous session, press **Cmd+0**. +2. Hover/drag a column resize edge — cursor shows `col-resize`, table size does + **not** change, no blur. +3. Pinch near a resize handle — nothing balloons. +4. **Cmd + =** / **Cmd + -** zoom the **whole app** (sidebar + table together). +5. Optional: run `__zoomDiag()` in DevTools — `devicePixelRatio` should stay put. + +## Notes + +- Tauri page-zoom listens on `mousewheel`, **not** `wheel`. Blocking only `wheel` + is insufficient on macOS. +- Page-zoom raises `devicePixelRatio` with `visualViewport.scale === 1`; pinch + magnification raises `visualViewport.scale`. They are distinct and need + different handling. diff --git a/package-lock.json b/package-lock.json index d76de961..718dc451 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "db-studio", - "version": "0.3.1", + "name": "stroke", + "version": "0.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "db-studio", - "version": "0.3.1", + "name": "stroke", + "version": "0.3.3", "dependencies": { "@dagrejs/dagre": "^3.0.0", "@fontsource-variable/geist": "^5.2.9", diff --git a/package.json b/package.json index 49289cdb..02f3714d 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "db-studio", + "name": "stroke", "private": true, "version": "0.3.3", "type": "module", diff --git a/packaging/aur/discerns/PKGBUILD b/packaging/aur/discerns/PKGBUILD index fe56a54d..0b6411b3 100644 --- a/packaging/aur/discerns/PKGBUILD +++ b/packaging/aur/discerns/PKGBUILD @@ -3,12 +3,12 @@ pkgver=0.1.22 pkgrel=4 pkgdesc="Cross-database desktop studio built with Tauri" arch=('x86_64') -url="https://github.com/broisnischal/studio" +url="https://github.com/broisnischal/stroke" license=('custom') depends=('fuse2') options=(!strip) -source=("${pkgname}-${pkgver}.AppImage::https://github.com/broisnischal/studio/releases/download/v${pkgver}/db-studio_${pkgver}_amd64.AppImage") +source=("${pkgname}-${pkgver}.AppImage::https://github.com/broisnischal/stroke/releases/download/v${pkgver}/stroke_${pkgver}_amd64.AppImage") sha256sums=('7214b7bd75519a191898b7f124c4667f358bef61b65f6b43b34baa06e51cf10d') package() { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 61092aee..4fdca7d1 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -97,6 +97,7 @@ dependencies = [ "objc2", "objc2-app-kit", "objc2-quartz-core", + "objc2-web-kit", "reqwest 0.12.28", "rust_decimal", "serde", @@ -2924,6 +2925,16 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-javascript-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" +dependencies = [ + "objc2", + "objc2-core-foundation", +] + [[package]] name = "objc2-metal" version = "0.3.2" @@ -2964,6 +2975,17 @@ dependencies = [ "objc2-metal", ] +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", +] + [[package]] name = "objc2-ui-kit" version = "0.3.2" @@ -3007,6 +3029,8 @@ dependencies = [ "objc2-app-kit", "objc2-core-foundation", "objc2-foundation", + "objc2-javascript-core", + "objc2-security", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 98a0597c..c0f2c560 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -48,6 +48,7 @@ getrandom = "0.2" objc2 = "0.6" objc2-app-kit = { version = "0.3", features = ["NSWindow", "NSView", "objc2-quartz-core"] } objc2-quartz-core = { version = "0.3", features = ["CALayer"] } +objc2-web-kit = { version = "0.3", features = ["WKWebView"] } [profile.release] strip = true diff --git a/src-tauri/src/cloudflare.rs b/src-tauri/src/cloudflare.rs index d9749f5a..c4dfb47f 100644 --- a/src-tauri/src/cloudflare.rs +++ b/src-tauri/src/cloudflare.rs @@ -1,5 +1,5 @@ /*! - * Cloudflare OAuth 2.0 + PKCE flow for DB Studio. + * Cloudflare OAuth 2.0 + PKCE flow for Stroke. * * Uses the same public client_id as the Wrangler CLI — Cloudflare's official * developer tool — which accepts localhost redirect URIs for desktop apps. @@ -49,7 +49,7 @@ static CF_CLIENT: OnceLock = OnceLock::new(); fn http() -> &'static reqwest::Client { CF_CLIENT.get_or_init(|| { reqwest::Client::builder() - .user_agent("db-studio/1.0") + .user_agent("stroke/1.0") .tcp_keepalive(std::time::Duration::from_secs(60)) .pool_max_idle_per_host(4) .build() @@ -108,7 +108,7 @@ async fn bind_callback_listener() -> Result<(TcpListener, String), String> { } Err(format!( "Could not bind to any of the pre-registered callback ports ({}-{}). \ - Close other Wrangler or DB Studio processes and try again.", + Close other Wrangler or Stroke processes and try again.", CF_CALLBACK_PORTS[0], CF_CALLBACK_PORTS[CF_CALLBACK_PORTS.len() - 1] )) @@ -121,24 +121,24 @@ async fn await_oauth_callback( ) -> Result { let success_html = r#" -DB Studio — authorized +Stroke — authorized

Authorization successful

-

You can close this tab and return to DB Studio.

+

You can close this tab and return to Stroke.

"#; let error_html = r#" -DB Studio — error +Stroke — error

Authorization failed

-

You can close this tab and try again in DB Studio.

+

You can close this tab and try again in Stroke.

"#; let send_html = |html: &str| -> String { diff --git a/src-tauri/src/copilot.rs b/src-tauri/src/copilot.rs index fdd10edb..bc0acfe3 100644 --- a/src-tauri/src/copilot.rs +++ b/src-tauri/src/copilot.rs @@ -13,7 +13,7 @@ static COPILOT_CLIENT: OnceLock = OnceLock::new(); fn http_client() -> &'static reqwest::Client { COPILOT_CLIENT.get_or_init(|| { reqwest::Client::builder() - .user_agent("db-studio/1.0") + .user_agent("stroke/1.0") .tcp_keepalive(std::time::Duration::from_secs(60)) .pool_max_idle_per_host(4) .build() @@ -147,7 +147,7 @@ pub async fn copilot_get_copilot_token(oauth_token: String) -> Result) -> String { let schema_line = schema.map_or_else(String::new, |s| format!("-- Schema : {s}\n")); format!( - "-- DB Studio Backup\n-- Engine : {engine}\n{schema_line}-- Restore : execute this file against a {engine} database\n\n" + "-- Stroke Backup\n-- Engine : {engine}\n{schema_line}-- Restore : execute this file against a {engine} database\n\n" ) } diff --git a/src-tauri/src/db/connection.rs b/src-tauri/src/db/connection.rs index 6c2d1ef0..d3ee7202 100644 --- a/src-tauri/src/db/connection.rs +++ b/src-tauri/src/db/connection.rs @@ -200,10 +200,11 @@ async fn open_pg(config: &PgConfig) -> Result { // halving this cuts FD and memory hold-time without affecting responsiveness. .idle_timeout(std::time::Duration::from_secs(30)) .max_lifetime(std::time::Duration::from_secs(300)) - // Kill runaway queries automatically so they don't pin connections forever. + // Kill truly runaway queries. 10 min covers bulk inserts / migrations while + // still bounding accidental full-table scans that would pin a connection. .after_connect(|conn, _meta| { Box::pin(async move { - sqlx::query("SET statement_timeout = '30s'") + sqlx::query("SET statement_timeout = '10min'") .execute(&mut *conn) .await?; Ok(()) diff --git a/src-tauri/src/docker.rs b/src-tauri/src/docker.rs index 577e0404..0cefe778 100644 --- a/src-tauri/src/docker.rs +++ b/src-tauri/src/docker.rs @@ -129,7 +129,7 @@ pub async fn docker_run_db( .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs(); - let container_name = format!("db-studio-{db_type}-{ts}"); + let container_name = format!("stroke-{db_type}-{ts}"); let port_map = format!("{host_port}:{cont_port}"); // ── Pull ────────────────────────────────────────────────────────────────── diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 52747208..3ba21585 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -30,9 +30,9 @@ pub fn run() { // setups (2× displays) and can cause rendering panics on Wayland compositors. } - // Set a human-readable process title so the app shows as "db-studio" in + // Set a human-readable process title so the app shows as "stroke" in // htop / ps / /proc — makes it easy to identify among WebKit helper processes. - let _ = metrics::set_process_title("db-studio".into()); + let _ = metrics::set_process_title("stroke".into()); // Create the shared connection Arc — both DbState and McpState point to the same lock. let db_conn: Arc>> = Arc::new(Mutex::new(None)); let db_state = DbState { conn: Arc::clone(&db_conn) }; @@ -53,7 +53,7 @@ pub fn run() { "main", tauri::WebviewUrl::App("/".into()), ) - .title("DB Studio") + .title("Stroke") .inner_size(1280.0, 800.0) .min_inner_size(960.0, 600.0) .resizable(true) @@ -70,6 +70,12 @@ pub fn run() { // keep disabled in release. The toggle_devtools command also exposes // them on demand via F12. .devtools(cfg!(debug_assertions)) + // The app implements its own CSS-based zoom; disable Tauri's injected + // zoom polyfill. On macOS/Linux that polyfill attaches a `mousewheel` + // (legacy event) listener that calls set_webview_zoom on ctrl+scroll — + // a stray trackpad pinch near a column resize handle would then page-zoom + // the whole webview (devicePixelRatio jumps, canvas renders blurry). + .zoom_hotkeys_enabled(false) .on_navigation(|url| { let scheme = url.scheme(); if matches!(scheme, "tauri" | "ipc") { @@ -101,6 +107,17 @@ pub fn run() { } } } + + // Defensive: disable every native WKWebView zoom path. App zoom is + // CSS-based (--app-zoom); stray pinch near column resize handles + // must never page-zoom the webview (devicePixelRatio drift → blur). + let _ = window.with_webview(|webview| unsafe { + use objc2_web_kit::WKWebView; + let view: &WKWebView = &*webview.inner().cast(); + view.setAllowsMagnification(false); + view.setMagnification(1.0); + view.setPageZoom(1.0); + }); } if cfg!(debug_assertions) { diff --git a/src-tauri/src/license.rs b/src-tauri/src/license.rs index ef31cd5f..c7592dea 100644 --- a/src-tauri/src/license.rs +++ b/src-tauri/src/license.rs @@ -39,7 +39,7 @@ const TRIAL_FILE: &str = ".trial"; // Note: revocation only takes effect after the user installs the update. // For immediate remote revocation you'll need the server endpoint (coming later). const REVOKED_KEY_HASHES: &[&str] = &[ - // "aabbcc..." — example: paste hash here, one per line + "ed13c9edc3bea01c6f82a415a10b21f85b85f829004f473cb78e8cd10056059f", // revoked 2026-06-05 (case: LIC-REV-2026-06-05-01) ]; // ── Data types ──────────────────────────────────────────────────────────────── diff --git a/src-tauri/src/mcp/server.rs b/src-tauri/src/mcp/server.rs index 0d2a39d4..d6d2ee94 100644 --- a/src-tauri/src/mcp/server.rs +++ b/src-tauri/src/mcp/server.rs @@ -153,7 +153,7 @@ async fn dispatch( "initialize" => Ok(json!({ "protocolVersion": "2024-11-05", "capabilities": { "tools": {} }, - "serverInfo": { "name": "DB Studio", "version": env!("CARGO_PKG_VERSION") } + "serverInfo": { "name": "Stroke", "version": env!("CARGO_PKG_VERSION") } })), "ping" => Ok(json!({})), @@ -180,7 +180,7 @@ async fn dispatch( if !tools::is_read_only_sql(sql) { return Err( "Read-only mode is enabled. Only SELECT queries are permitted. \ - Disable read-only mode in DB Studio's MCP settings to run write operations." + Disable read-only mode in Stroke's MCP settings to run write operations." .to_string(), ); } @@ -191,7 +191,7 @@ async fn dispatch( .lock() .map_err(|e| e.to_string())? .clone() - .ok_or("Not connected to a database. Call list_databases to see available connections and ask the user to select one in DB Studio.")?; + .ok_or("Not connected to a database. Call list_databases to see available connections and ask the user to select one in Stroke.")?; let text = tools::call_tool(&conn, name, &args).await?; Ok(json!({ "content": [{ "type": "text", "text": text }] })) } diff --git a/src-tauri/src/mcp/tools.rs b/src-tauri/src/mcp/tools.rs index febbbc96..8f0deb4d 100644 --- a/src-tauri/src/mcp/tools.rs +++ b/src-tauri/src/mcp/tools.rs @@ -10,7 +10,7 @@ pub fn tool_list() -> Value { json!([ { "name": "list_databases", - "description": "List all saved database connections and show which one is currently active. Call this at the start of a session when the user hasn't specified a database, or when you need to know what databases are available. If multiple databases exist, ask the user which one to use and instruct them to switch in DB Studio.", + "description": "List all saved database connections and show which one is currently active. Call this at the start of a session when the user hasn't specified a database, or when you need to know what databases are available. If multiple databases exist, ask the user which one to use and instruct them to switch in Stroke.", "inputSchema": { "type": "object", "properties": {} @@ -131,9 +131,9 @@ pub fn call_meta_tool( "active_id": active_id, "active_name": active_name, "note": if connections.len() > 1 { - "Multiple databases available. To switch, open DB Studio and connect to the desired database. Then retry your request." + "Multiple databases available. To switch, open Stroke and connect to the desired database. Then retry your request." } else if connections.is_empty() { - "No saved connections found. Open DB Studio and add a connection first." + "No saved connections found. Open Stroke and add a connection first." } else { "One database configured." } @@ -157,7 +157,7 @@ pub fn call_meta_tool( } None => Ok(json!({ "connected": false, - "note": "No database is currently connected. Open DB Studio and connect to a database, then retry." + "note": "No database is currently connected. Open Stroke and connect to a database, then retry." }).to_string()), } } diff --git a/src-tauri/src/metrics.rs b/src-tauri/src/metrics.rs index b0a3c1b1..05ce7100 100644 --- a/src-tauri/src/metrics.rs +++ b/src-tauri/src/metrics.rs @@ -1,5 +1,5 @@ /*! -Process naming and runtime metrics for DB Studio. +Process naming and runtime metrics for Stroke. Exposes two Tauri commands: - `set_process_title` — renames the process so it shows up as a recognisable @@ -32,7 +32,7 @@ fn sys() -> &'static Mutex { #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct AppMetrics { - /// OS process ID of this DB Studio process. + /// OS process ID of this Stroke process. pub pid: u32, /// Resident Set Size in bytes — actual physical RAM in use. pub rss_bytes: u64, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b247af33..7d0d41cb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,8 +1,8 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", - "productName": "db-studio", + "productName": "Stroke", "version": "0.3.3", - "identifier": "com.broisnischal.studio", + "identifier": "com.broisnischal.stroke", "build": { "frontendDist": "../dist", "devUrl": "http://localhost:8080", @@ -17,9 +17,9 @@ }, "plugins": { "updater": { - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlGMjlCQUIyQjA3Qjg4MjYKUldRbWlIdXdzcm9wbnlqVXRHc3lwWHRWdHVkdmFoMjZhM1lQZ3BnM1Ird0hlYXBxZWo5b1dLV3QK", + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDQyN0M0REY4NTUzODY4MDMKUldSQ2ZFMzRWVGhvQXlscG91dDk5MDdSbTBobWFvMjFDbFcyVTl6NDE5NEYweHNHS29hUVpQR04K", "endpoints": [ - "https://github.com/broisnischal/studio/releases/latest/download/latest.json" + "https://github.com/broisnischal/stroke/releases/latest/download/latest.json" ], "dialog": false } @@ -37,6 +37,11 @@ "macOS": { "signingIdentity": "-" }, + "windows": { + "certificateThumbprint": null, + "digestAlgorithm": "sha256", + "timestampUrl": "http://timestamp.digicert.com" + }, "android": { "debugApplicationIdSuffix": ".debug" } diff --git a/src/lib/ai.js b/src/lib/ai.js index 06badb7b..cad7b26f 100644 --- a/src/lib/ai.js +++ b/src/lib/ai.js @@ -910,7 +910,7 @@ Only use mermaid code blocks inside explanatory prose (e.g. "here's the syntax: - 2–5 words, title-case, descriptive. Examples: "User Order Flow", "E-Commerce ERD", "Auth Sequence", "Order State Machine", "Product Hierarchy" - Reflect the subject, not the diagram type ("Users & Orders" not "ER Diagram") -Always output diagrams in a \`\`\`mermaid code block — they render interactively in DB Studio. +Always output diagrams in a \`\`\`mermaid code block — they render interactively in Stroke. ### Entity-Relationship Diagrams (ERD) \`\`\`mermaid @@ -1352,7 +1352,7 @@ SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() ORDER B (ctx.userSkills ?? []).map((s) => `### ${s.name}\n${s.content}`).join('\n\n') : '' - return `You are an expert ${DB_LABEL[dbType] ?? 'SQL'} database assistant embedded in DB Studio — a database GUI. + return `You are an expert ${DB_LABEL[dbType] ?? 'SQL'} database assistant embedded in Stroke — a database GUI. Your job: help users explore, query, analyse, and visualise their database through tool calls and clear explanations. === DATABASE === @@ -1380,7 +1380,7 @@ ${otherTablesSection} 2. Never mix formats: if outputting a chart call \`render_chart\`, if outputting a diagram call \`render_diagram\`, if explaining use prose. 3. Always use fenced code blocks with language names: \`\`\`sql, \`\`\`json, \`\`\`mermaid, etc. 4. Prose responses: max 4 short paragraphs. Use **bold** for key terms. -5. Errors from tool calls: wrap in message here tags. +5. Errors from tool calls: acknowledge briefly in plain text (1 sentence), then either retry with a corrected query or ask the user for clarification. Do not repeat the raw error verbatim. 6. For destructive operations (DELETE, DROP, TRUNCATE): first write a ONE-LINE human description in what will be affected (e.g. This will permanently delete all inactive users from the users table), then show the SQL in a fenced sql code block separately. NEVER put SQL code inside tags — only short plain-text descriptions go there. The system already prompts users before executing destructive SQL. 7. If you lack enough context to answer accurately, say exactly: "I don't have enough context for that. Please provide [specific thing needed]." 8. NEVER mention library names, package names, or technical implementation details in your responses. Just use the tools and produce results silently. diff --git a/src/lib/canvas-table.js b/src/lib/canvas-table.js index f9c08054..b987c755 100644 --- a/src/lib/canvas-table.js +++ b/src/lib/canvas-table.js @@ -47,12 +47,26 @@ export function createColorReader(probe) { }; } +// Memoize results — (colour, alpha) combos are few and stable, but this runs in +// the per-frame draw loop, so caching avoids repeated regex parsing on scroll. +/** @type {Map} */ +const _alphaCache = new Map(); + /** * Apply an alpha to a CSS colour string regardless of notation (rgb/rgba/hsl, * functional space-separated, oklch/oklab/color(), or hex). Returns a string * the canvas understands. */ export function withAlpha(/** @type {string} */ color, /** @type {number} */ a) { + const key = color + "@" + a; + const cached = _alphaCache.get(key); + if (cached !== undefined) return cached; + const out = _computeAlpha(color, a); + _alphaCache.set(key, out); + return out; +} + +function _computeAlpha(/** @type {string} */ color, /** @type {number} */ a) { const c = color.trim(); const fnMatch = c.match(/^([a-z]+)\((.*)\)$/i); if (fnMatch) { diff --git a/src/lib/cloudflare.js b/src/lib/cloudflare.js index 05c247b8..daa6331a 100644 --- a/src/lib/cloudflare.js +++ b/src/lib/cloudflare.js @@ -1,5 +1,5 @@ /** - * Cloudflare OAuth token management for DB Studio. + * Cloudflare OAuth token management for Stroke. * * Auth is backed by the Rust cloudflare module which: * 1. Runs a local HTTP callback server diff --git a/src/lib/components/AboutDialog.svelte b/src/lib/components/AboutDialog.svelte index 3cc87cb2..293b91a9 100644 --- a/src/lib/components/AboutDialog.svelte +++ b/src/lib/components/AboutDialog.svelte @@ -56,7 +56,7 @@
-

DB Studio

+

Stroke

Database management, reimagined

@@ -183,10 +183,10 @@
-

© 2025 DB Studio

+

© 2025 Stroke

diff --git a/src/lib/components/CommandPalette.svelte b/src/lib/components/CommandPalette.svelte index 11c6eeb6..690886d6 100644 --- a/src/lib/components/CommandPalette.svelte +++ b/src/lib/components/CommandPalette.svelte @@ -134,16 +134,49 @@ // smooth even with thousands of tables. let paletteSearch = $state('') const TABLES_PAGE_CAP = 100 + + /** + * Substring score: exact > starts-with > contains. No fuzzy scattered-char matching. + * @param {string} name + * @param {string} query + * @returns {number} + */ + function filterScore(name, query) { + if (!query) return 1 + const n = name.toLowerCase() + const q = query.toLowerCase() + if (n === q) return 1000 + if (n.startsWith(q)) return 900 + const sub = n.indexOf(q) + return sub !== -1 ? 800 - sub : 0 + } + /** @param {{ name: string }[]} list */ function filterAndCap(list) { const q = paletteSearch.trim().toLowerCase() - const matched = q ? list.filter((t) => t.name.toLowerCase().includes(q)) : list - return { items: matched.slice(0, TABLES_PAGE_CAP), total: matched.length } + if (!q) return { items: list.slice(0, TABLES_PAGE_CAP), total: list.length } + const scored = list + .map((t) => ({ t, s: filterScore(t.name, q) })) + .filter((x) => x.s > 0) + .sort((a, b) => b.s - a.s) + return { items: scored.slice(0, TABLES_PAGE_CAP).map((x) => x.t), total: scored.length } } - const tablesPageRegular = $derived(filterAndCap(regularTables)) - const tablesPageViews = $derived(filterAndCap(viewTables)) + + const tablesPageRegular = $derived(filterAndCap(regularTables)) + const tablesPageViews = $derived(filterAndCap(viewTables)) const tablesPageMatViews = $derived(filterAndCap(matViewTables)) + // On tables page: shouldFilter=false so bits-ui skips its sort/filter pass entirely + // (no CSS `order` reordering, no keyboard-nav interference). We pre-filter in JS. + // On root page: bits-ui filters with a plain substring check against item value strings. + const shouldFilter = $derived(page !== 'tables') + + /** @type {(value: string, search: string) => number} */ + const commandFilter = (value, search) => { + if (!search) return 1 + return value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0 + } + /** @param {() => void} action */ function run(action) { open = false @@ -173,6 +206,8 @@ { paletteSearch = /** @type {HTMLInputElement} */ (e.currentTarget).value }} placeholder={ page === 'root' ? 'Search tables, schemas, commands…' : page === 'tables' ? 'Search tables and views…' @@ -361,7 +397,7 @@ run(onopenabout)}> - About DB Studio + About Stroke run(oncheckupdate)}> diff --git a/src/lib/components/ConnectionModal.svelte b/src/lib/components/ConnectionModal.svelte index cb0e2927..11c76baa 100644 --- a/src/lib/components/ConnectionModal.svelte +++ b/src/lib/components/ConnectionModal.svelte @@ -279,9 +279,10 @@ d1Databases = []; d1SelectedAccountId = ''; d1DbLoadPhase = 'idle' } - const lbl = 'mb-1.5 block text-[10px] font-semibold uppercase tracking-[0.07em] text-muted-foreground/50' - const inp = 'h-8 w-full border-border/25 bg-muted/[0.4] text-sm placeholder:text-muted-foreground/30 focus-visible:border-border/50 focus-visible:ring-0' - const divider = 'border-t border-border/25 my-0.5' + const lbl = 'mb-1.5 block text-[11px] font-medium uppercase tracking-[0.06em] text-muted-foreground/80' + const inp = 'h-9 w-full border-border/50 bg-muted/40 text-sm text-foreground placeholder:text-muted-foreground/40 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30' + const inpNum = inp + ' [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none' + const divider = 'border-t border-border/30 my-0.5' async function pickSqliteFile() { try { @@ -321,7 +322,7 @@

Connect

-

Select or add a connection

+

Select or add a connection

@@ -342,7 +343,7 @@ {#if saved.length > 0} -

+

Recent

{#each saved as conn (conn.id)} @@ -364,10 +365,10 @@

{conn.name}

{#if conn.id === lastId} - last + last {/if}
-

{connDetail(conn)}

+

{connDetail(conn)}