Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Builds are **unsigned**, so SmartScreen and Gatekeeper warn on first launch (the
| **Keep** | Saved diffs: encrypted, tagged, optionally auto-expiring. Drag a row onto another to arrange the list yourself; starred rows stay above the rest. Your open tabs come back on the next launch, and the strip can be told to close the oldest comparison to make room for a new one. |
| **Share** | One signed file only the recipients you ticked can open, carrying the expiry you chose so every copy dies at the same moment. Give a trusted key an email address and Diff Bro opens an addressed message in your own mail app with the sealed file on the clipboard — it never sends anything itself. The key swap rides the same rails: email your key from the My key dialog, and a key copied out of any chat app is offered — fingerprint first — when you press + Trusted key. |
| **Export as image** | A real screenshot of the diff view — your theme, panes and highlighting — cropped to the change and stitched if it's taller than the window. Snippets go the same way, and a Mermaid snippet leaves as its rendered diagram. |
| **Snippets** | An encrypted, tagged text library you can drag straight into the diff pane — two snippets compare like any two files, and editing one updates the comparison on screen. Per-language highlighting, live Mermaid (readable light or dark whatever the app is wearing), Markdown/Jira preview, and secret snippets that render as `****`. Every edit keeps the version it replaced — History in the snippet window lists them by timestamp, each diffed against its predecessor, any of them a copy away. Name one `Standup {{today}}` and the placeholder resolves as you save — `{{now}}`, `{{week}}`, `{{weekday}}` and the rest are listed under the field as you type. Naming is completed inline: type a few characters and the rest of the shared head of your existing names appears ahead of the caret, Tab to take it. Drag a row onto another to arrange the library by hand. |
| **Snippets** | An encrypted, tagged text library you can drag straight into the diff pane — two snippets compare like any two files, and editing one updates the comparison on screen. Per-language highlighting, live Mermaid (readable light or dark whatever the app is wearing), Markdown/Jira preview — headings, nested lists, task lists, tables, strikethrough and code, with a formatting row that writes them for you — and secret snippets that render as `****`. Every edit keeps the version it replaced — History in the snippet window lists them by timestamp, each diffed against its predecessor, any of them a copy away. Name one `Standup {{today}}` and the placeholder resolves as you save — `{{now}}`, `{{week}}`, `{{weekday}}` and the rest are listed under the field as you type. Naming is completed inline: type a few characters and the rest of the shared head of your existing names appears ahead of the caret, Tab to take it. Drag a row onto another to arrange the library by hand, and right-click one to give it a colour — six of them, painted across the whole row, so a handful of snippets group by eye across whatever tags they carry. Nothing filters or searches by it; it is there to be seen. |
| **Quick look-up** | A global shortcut searches your snippets and diffs without raising the app; copy one straight to the clipboard, or capture a new one with `Ctrl/Cmd+N` — whatever you searched for becomes its name, and the body is syntax-coloured as you type in whatever language it turns out to be. |
| **Stays out of the way** | On Windows, closing the window keeps Diff Bro in the notification area so the quick look-up shortcut still answers — right-click the icon to exit, and turn either that or start-at-sign-in off in Settings ▸ Desktop. |
| **Language** | Every menu, dialog and label reads from one message catalogue, and Settings ▸ Appearance switches it — menus included, without a restart. English ships today; a new language is a data file, not a code change. |
Expand All @@ -72,7 +72,7 @@ Builds are **unsigned**, so SmartScreen and Gatekeeper warn on first launch (the
- **Drag & drop** files onto the window; it warns before discarding unsaved work.
- **Ctrl/Cmd+V** pastes straight into a comparison — including pasted text against a real file.
- **Copy diff** puts a git-style unified patch on the clipboard.
- **Quick look-up keys** — ↑/↓ browse, **→** steps into a preview or the tools, **←** steps back out, **Enter** opens, **Ctrl/Cmd+N** captures a new snippet without raising the app (or arrow to the _Create snippet_ row), **Tab** accepts the inline name completion, and **Ctrl/Cmd+Enter** saves it.
- **Quick look-up keys** — ↑/↓ browse, **→** steps into a preview or the tools, **←** steps back out, **Enter** opens, **Ctrl/Cmd+N** captures a new snippet without raising the app (or arrow to the _Create snippet_ row), **Tab** accepts the inline name completion, and **Ctrl/Cmd+Enter** saves it. The card grows for writing, a click opens whatever row it lands on, and a snippet you were part-way through is still there the next time you summon it. A Jira or Markdown snippet gets the same formatting row the editor gives it, with the rendered form drawn beside the syntax as you type.
- **Resizable dialogs** — the snippet editor and tool windows resize from any edge and remember their size; existing snippets open read-only until you press Edit.
- **Save a tool's output** — anything a tool produced goes straight into the snippet library from its own window; you supply the name, the app fills the rest.
- **Uniform snippet names** — every name is sentence-cased on save, so a library grown over months still reads consistently.
Expand Down
Binary file added docs/screenshots/themes/snippet-colour-amber.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/themes/snippet-colour-beacon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/themes/snippet-colour-bloom.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/themes/snippet-colour-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/themes/snippet-colour-dim.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/themes/snippet-colour-ember.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/themes/snippet-colour-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/themes/snippet-colour-linen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/themes/snippet-colour-matrix.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/themes/snippet-colour-neon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/themes/snippet-colour-nord.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/themes/snippet-colour-nyan.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/themes/snippet-colour-sepia.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/themes/snippet-colour-tide.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/themes/snippet-colour-vector.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 84 additions & 0 deletions e2e/quick-look-draft.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { test, expect } from './fixtures.mjs'

// Composing in the launcher is WRITING, and it was being done in a card sized to
// read one line in. Two things have to hold: the window grows for the job, and
// pressing the chord twice — which is how the launcher is dismissed and brought
// back — does not take an unsaved draft with it.

const launcher = (app, read) =>
app.evaluate(({ BrowserWindow }) => {
const w = BrowserWindow.getAllWindows().find((x) =>
x.webContents.getURL().includes('quicklook')
)
return w ? { bounds: w.getBounds(), visible: w.isVisible() } : null
}, read)

const bounds = async (app) => (await launcher(app)).bounds
const isVisible = async (app) => (await launcher(app))?.visible === true
const toggle = (page) => page.evaluate(() => window.api.quickLookToggle())

async function summon(app, page) {
const [ql] = await Promise.all([app.waitForEvent('window'), toggle(page)])
await ql.waitForLoadState('domcontentloaded')
await expect(ql.locator('.ql-input')).toBeVisible()
return ql
}

// Main owns the resize, so the height is polled off the OS window rather than
// off anything the page can report about itself.
const height = (app) => expect.poll(async () => (await bounds(app)).height, { timeout: 4000 })

test('composing grows the card, and leaving it gives the size back', async ({ app, page }) => {
const ql = await summon(app, page)
const resting = await bounds(app)

await ql.locator('.ql-add').click()
await expect(ql.locator('.ql-compose')).toBeVisible()
await height(app).toBeGreaterThan(resting.height * 1.4)

// It grew from where it already was — down and to the right. Re-centring
// would slide the row under the pointer away mid-click, which is exactly what
// ate two clicks on Edit.
const grown = await bounds(app)
expect(grown.width).toBeGreaterThan(resting.width)
expect({ x: grown.x, y: grown.y }).toEqual({ x: resting.x, y: resting.y })

await ql.getByRole('button', { name: 'Cancel' }).click()
await height(app).toBe(resting.height)
})

test('a re-summon keeps an unsaved draft, and the card it was written in', async ({
app,
page
}) => {
const ql = await summon(app, page)
await ql.locator('.ql-add').click()
await ql.locator('.ql-compose-text').fill('line one\nline two\nline three')
await height(app).toBeGreaterThan(600)
const composing = await bounds(app)

await toggle(page)
await expect.poll(() => isVisible(app), { timeout: 4000 }).toBe(false)
await toggle(page)
await expect.poll(() => isVisible(app), { timeout: 4000 }).toBe(true)

await expect(ql.locator('.ql-compose')).toBeVisible()
await expect(ql.locator('.ql-compose-text')).toHaveValue('line one\nline two\nline three')
expect((await bounds(app)).height).toBe(composing.height)
})

// The other half of the rule: with nothing at stake a summon is still a fresh
// start, or a stale query outlives the search that typed it.
test('a summon with nothing typed into it still starts clean', async ({ app, page }) => {
const ql = await summon(app, page)
await ql.locator('.ql-input').fill('mermaid')
await ql.locator('.ql-add').click()
await expect(ql.locator('.ql-compose')).toBeVisible()

await toggle(page)
await expect.poll(() => isVisible(app), { timeout: 4000 }).toBe(false)
await toggle(page)

await expect(ql.locator('.ql-compose')).toBeHidden()
await expect(ql.locator('.ql-input')).toHaveValue('')
})
20 changes: 12 additions & 8 deletions e2e/quick-look-keyboard.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,20 @@ test('an unmodified inline edit does not rewrite the stored language', async ({
await expect(ql.locator('.ql-compose-lang option:checked')).toHaveText(/SQL/)
await ql.keyboard.press('ControlOrMeta+Enter')

// Searched, not clicked: a click on a row now does what ↵ does — it opens the
// snippet in the main window — and this test wants the preview, not the hand-off.
await ql.locator('.ql-input').fill('orders probe')
await ql.locator('.ql-res:not(.ql-res-create)', { hasText: 'Orders probe' }).first().click()
await expect(ql.locator('.ql-pv-name')).toHaveText('Orders probe')
await ql.locator('.ql-pv-head button', { hasText: 'Edit' }).click()

// Still Auto — the picker would read a bare "SQL" if the language had frozen.
await expect(ql.locator('.ql-compose-lang')).toHaveValue('auto')
await ql.keyboard.press('ControlOrMeta+Enter')

// Searched, not clicked: a click on a row now does what ↵ does — it opens the
// snippet in the main window — and this test wants the preview, not the hand-off.
await ql.locator('.ql-input').fill('orders probe')
await ql.locator('.ql-res:not(.ql-res-create)', { hasText: 'Orders probe' }).first().click()
await expect(ql.locator('.ql-pv-name')).toHaveText('Orders probe')
await ql.locator('.ql-pv-head button', { hasText: 'Edit' }).click()
await expect(ql.locator('.ql-compose-lang')).toHaveValue('auto')
})
Expand All @@ -200,19 +204,19 @@ test('changing the language with an arrow key does not discard the draft', async
// The launcher hides on blur and is kept warm, so a summon must land on a fresh
// search band. This could not pass until focusInput() stopped throwing: it runs
// first in onMounted, so it took the quicklook:show registration down with it
// and refresh() never ran at all.
test("a re-summon starts fresh, not on last time's draft", async ({ app, page }) => {
// and refresh() never ran at all. What a summon deliberately KEEPS — an unsaved
// draft, a tool panel mid-conversion — is quick-look-draft.spec.mjs.
test("a re-summon starts fresh, not on last time's search", async ({ app, page }) => {
const ql = await summon(app, page)

await ql.locator('.ql-input').fill('marker-query')
await ql.keyboard.press('ControlOrMeta+n')
await ql.locator('.ql-compose-text').fill('half-written thought')
await expect(ql.locator('.ql-compose')).toBeVisible()
await ql.keyboard.press('ArrowDown')
await expect(ql.locator('.ql-res.sel')).toBeVisible()

await page.evaluate(() => window.api.quickLookToggle())
await page.evaluate(() => window.api.quickLookToggle())

await expect(ql.locator('.ql-compose')).toBeHidden()
await expect(ql.locator('.ql-input')).toBeVisible()
await expect(ql.locator('.ql-input')).toHaveValue('')
await expect(ql.locator('.ql-res').first()).toHaveClass(/sel/)
})
Loading
Loading