Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
6 changes: 3 additions & 3 deletions .github/workflows/cloudflare_pages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ name: Deploy to Cloudflare Pages
on:
pull_request:
push:
branches:
- main
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -47,4 +47,4 @@ jobs:
Preview URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }}

Automated deployment preview for the PR in the Cloudflare Pages.
emojis: 'rocket, eyes, +1, -1'
emojis: "rocket, eyes, +1, -1"
2 changes: 1 addition & 1 deletion .github/workflows/docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
branches:
- main
tags:
- 'v*.*.*'
- "v*.*.*"

env:
IMAGE_NAME: mattfly/obsidian
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
workflow_dispatch:
inputs:
google-playstore-version:
description: 'Optional: Override Google Play version code'
description: "Optional: Override Google Play version code"
required: false
type: string

Expand Down Expand Up @@ -357,8 +357,8 @@ jobs:
with:
workload_identity_provider: ${{ secrets.GOOGLE_WIF_PROVIDER }}
service_account: ${{ secrets.GOOGLE_WIF_SERVICE_ACCOUNT }}
token_format: 'access_token'
access_token_scopes: 'https://www.googleapis.com/auth/androidpublisher'
token_format: "access_token"
access_token_scopes: "https://www.googleapis.com/auth/androidpublisher"

- name: Setup Java
uses: actions/setup-java@v5
Expand Down Expand Up @@ -480,8 +480,8 @@ jobs:
with:
workload_identity_provider: ${{ secrets.GOOGLE_WIF_PROVIDER }}
service_account: ${{ secrets.GOOGLE_WIF_SERVICE_ACCOUNT }}
token_format: 'access_token'
access_token_scopes: 'https://www.googleapis.com/auth/androidpublisher'
token_format: "access_token"
access_token_scopes: "https://www.googleapis.com/auth/androidpublisher"

- name: Download signed AAB artifact
uses: actions/download-artifact@v8
Expand Down
129 changes: 97 additions & 32 deletions .github/workflows/workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,45 @@ name: Lint and Tests
on:
push:

# Every lint/format job is a thin wrapper around `lefthook run pre-commit`
# so lefthook.yml stays the single source of truth for what runs locally and in CI.

jobs:
biome:
name: Lint and Formating
name: Biome (lint & format)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Biome
uses: biomejs/setup-biome@v2
- name: Setup Node
uses: actions/setup-node@v4
with:
version: latest
- name: Run Biome
run: biome ci .
node-version: 22
cache: npm
- name: Install deps
run: npm ci
- name: Run lefthook check
run: npx lefthook run pre-commit --commands check --all-files
- name: Verify no formatting drift
run: git diff --exit-code

build:
name: Build and Test
timeout-minutes: 10
docs:
name: Docs (prettier & markdownlint)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Dependencies install
run: npm ci
- name: Test
run: npm run test
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install deps
run: npm ci
- name: Run lefthook docs hooks
run: npx lefthook run pre-commit --commands docs-format,docs-lint --all-files
- name: Verify no formatting drift
run: git diff --exit-code

nix-linux:
name: Nix package (Linux x86_64)
Expand All @@ -47,26 +61,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Dependencies install
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install deps
run: npm ci
- name: Verify catalog is up to date
# Fails if there are t`...` strings in source that haven't been extracted
# into en/messages.po yet. Fix: npm run i18n:extract && npm run i18n:compile
# and commit the updated src/locales/ files.
run: |
npm run i18n:extract
git diff --exit-code src/locales/en/messages.po || \
(echo "::error::Untranslated strings detected in source. Run 'npm run i18n:extract && npm run i18n:compile' and commit the updated src/locales/ files." && exit 1)
- name: Verify catalogs are compiled
# Fails if .mjs files are missing or stale relative to .po files.
run: |
npm run i18n:compile
git diff --exit-code src/locales/ || \
(echo "::error::Compiled catalogs are out of date. Run 'npm run i18n:compile' and commit the updated src/locales/ files." && exit 1)
- name: Run lefthook i18n hook
# Fails if extract/compile produces a diff against committed catalogs.
# Fix: npm run i18n:extract && npm run i18n:compile, commit src/locales/.
run: npx lefthook run pre-commit --commands i18n --all-files
- name: Verify catalogs are committed
run: git diff --exit-code src/locales/
- name: Report untranslated strings per locale
# Informational only — does not fail the build.
# Shows how many strings still need translation in each non-English locale.
run: |
echo "### Translation coverage" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
Expand All @@ -82,3 +92,58 @@ jobs:
echo "- **${locale}**: ${TRANSLATED}/${TOTAL} strings translated" >> $GITHUB_STEP_SUMMARY
fi
done

rust:
name: Rust fmt & clippy
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
workspaces: |
src-tauri
src-tauri/plugins/share-sheet
src-tauri/plugins/ios-keyboard
- name: Install Tauri system deps
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev
- name: Install deps
run: npm ci
- name: Run lefthook rust hooks
run: npx lefthook run pre-commit --commands rust-fmt,rust-clippy --all-files

build:
name: Build and Test
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install deps
run: npm ci
- name: Test
run: npm run test
18 changes: 18 additions & 0 deletions .markdownlint.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
// Line length — prettier owns wrapping
"MD013": false,
// Duplicate headings — allow same heading text in different sections
"MD024": { "siblings_only": true },
// Ordered list prefix — accept both `1. 1. 1.` and `1. 2. 3.`
"MD029": { "style": "one_or_ordered" },
// Inline HTML — needed for badges, <details>, <br>, <img>
"MD033": false,
// Bare URLs — fine in docs, badges, link lists
"MD034": false,
// Emphasis used as heading — stylistic call, allow
"MD036": false,
// Fenced code language hint — nice but not worth fixing across all docs
"MD040": false,
// First-line H1 — some files lead with badges or autogenerated content
"MD041": false
}
11 changes: 11 additions & 0 deletions .markdownlintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node_modules/**
dist/**
build/**
coverage/**
src-tauri/target/**
src-tauri/plugins/*/target/**
src-tauri/plugins/*/.tauri/**
src-tauri/gen/**
.serena/**
.playwright-mcp/**
.husky/**
29 changes: 29 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
node_modules
dist
build
coverage
src-tauri/target
src-tauri/plugins/*/target
src-tauri/plugins/*/.tauri
src-tauri/plugins/*/permissions/autogenerated
src-tauri/gen
.serena
.playwright-mcp
.husky

# Biome owns these
*.js
*.cjs
*.mjs
*.ts
*.tsx
*.jsx
*.json
*.jsonc
*.css
*.scss
*.html

# Lingui generated catalogs
src/locales/**/*.po
src/locales/**/*.mjs
15 changes: 15 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"endOfLine": "lf",
"proseWrap": "preserve",
"overrides": [
{
"files": ["*.yml", "*.yaml"],
"options": {
"singleQuote": false
}
}
]
}
32 changes: 19 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ npm run format; npm run fix:unsafe; npm run test; npm run build
- **`nix develop`** — full dev environment (Node 22 + Tauri Linux deps + rustup). Linux only (`x86_64`/`aarch64`).
- **`nix build .#obsidianirc`** — produces `result/bin/ObsidianIRC`. Bump `npmDeps` in [nix/obsidianirc.nix](nix/obsidianirc.nix) when `package-lock.json` changes.
- Details: [BUILD.md — Nix (flake)](BUILD.md#nix-flake)

---

## Project Layout
Expand Down Expand Up @@ -79,7 +80,10 @@ src-tauri/ # Tauri config, Rust backend, plugins (Swift share-s
which dispatches via `IRC_DISPATCH`:

```ts
const IRC_DISPATCH: Record<string, (ctx: IRCClientContext, serverId: string, msg: ParsedMessage) => void> = {
const IRC_DISPATCH: Record<
string,
(ctx: IRCClientContext, serverId: string, msg: ParsedMessage) => void
> = {
PRIVMSG: handlePrivmsg,
JOIN: handleJoin,
"332": handleRplTopic,
Expand All @@ -104,7 +108,9 @@ Each handler file subscribes to `ircClient` events and updates the Zustand store
// Pattern in every src/store/handlers/*.ts
export function registerXxxHandlers(store: StoreApi<AppState>) {
ircClient.on("EVENT", (payload) => {
store.setState((state) => ({ /* return Partial<AppState> — no mutation */ }));
store.setState((state) => ({
/* return Partial<AppState> — no mutation */
}));
});
}
```
Expand Down Expand Up @@ -237,27 +243,27 @@ All user-visible text **must** be wrapped with LinguiJS macros so it can be tran

### Which tool to use


| String location | Macro | Import |
| --------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------- |
| JSX text children (`<button>`, `<span>`, `<p>`, headings) | `<Trans>…</Trans>` | `import { Trans } from "@lingui/macro"` |
| JSX props: `placeholder=`, `aria-label=`, `title=` | `t`…`` via `useLingui` | `import { useLingui } from "@lingui/macro"` then `const { t } = useLingui()` inside the component |
| Simple `t` outside JSX (inside a render function) | `t`…`` | `import { t } from "@lingui/macro"` |
| Variables/interpolation | `t`Hello ${name}`` | same — placeholders become `{0}` in the PO file |
| Non-React `.ts` files (store handlers, event callbacks) | `t`…`` | `import { t } from "@lingui/macro"` — safe inside callbacks that fire after `i18n.activate()` |
| JSX props: `placeholder=`, `aria-label=`, `title=` | `` t`…` `` via `useLingui` | `import { useLingui } from "@lingui/macro"` then `const { t } = useLingui()` inside the component |
| Simple `t` outside JSX (inside a render function) | `` t`…` `` | `import { t } from "@lingui/macro"` |
| Variables/interpolation | `` t`Hello ${name}` `` | same — placeholders become `{0}` in the PO file |
| Non-React `.ts` files (store handlers, event callbacks) | `` t`…` `` | `import { t } from "@lingui/macro"` — safe inside callbacks that fire after `i18n.activate()` |
| Module-level constants | **Do not use** `t` at module scope | `t` evaluates before `i18n.activate()` runs. Move the string inside the function body. |


### Correct patterns

```tsx
// JSX children
<button><Trans>Save</Trans></button>
<button>
<Trans>Save</Trans>
</button>;

// Props with interpolation (requires useLingui inside the component)
import { useLingui } from "@lingui/macro";
const { t } = useLingui();
<input placeholder={t`Message #${channelName}`} />
<input placeholder={t`Message #${channelName}`} />;

// Simple t tag inside render
import { t } from "@lingui/macro";
Expand Down Expand Up @@ -304,8 +310,8 @@ Rules:
Write the complete translated file back.
```

3. Run `npm run i18n:compile` to regenerate `.mjs` files.
4. Commit `src/locales/`.
1. Run `npm run i18n:compile` to regenerate `.mjs` files.
2. Commit `src/locales/`.

### Adding a new locale

Expand All @@ -326,4 +332,4 @@ The `i18n` job in `.github/workflows/workflow.yaml`:

### Tests

`@lingui/react` is mocked in tests via the alias in `vite.config.ts` → `tests/mocks/lingui-react.ts`. The mock handles `I18nProvider`, `Trans` (including `values` and `components` interpolation), and `useLingui()`. When you add a new component with lingui macros, its tests work without any wrapper changes.
`@lingui/react` is mocked in tests via the alias in `vite.config.ts` → `tests/mocks/lingui-react.ts`. The mock handles `I18nProvider`, `Trans` (including `values` and `components` interpolation), and `useLingui()`. When you add a new component with lingui macros, its tests work without any wrapper changes.
Loading
Loading