Add Drag Out Obsidian plugin for native drag-and-drop (#1)

* docs: add README for Drag Out plugin with installation, usage, and notes

* build(esbuild): add esbuild config for bundling main.ts

Add configuration to bundle main.ts with environment‑aware settings,
minify in production, generate source maps in dev, and watch for changes.
Excludes external modules and builtins.

* build(main-orig.js): add CommonJS build of main.ts as plugin entry point

* build(main.js): add esbuild-generated bundle for Drag Out plugin

* feat(main.ts): add DragOut plugin for external file drag support

Add DragOutPlugin class with settings, dragstart handling, icon resolution,
and a setting tab. Enables dragging files/folders from Obsidian to external
applications using a configurable modifier key. Includes debug logging and
cross‑platform support.

* feat: add manifest.json for Drag Out plugin to define metadata and versioning

* chore(package): add package.json with project metadata and build scripts

* chore(release): add release.mjs to automate version sync and build

Adds a script that reads the package version, updates manifest.json and
runs the production build, and copies release artifacts into dist/<version>/.
for GitHub releases.

* build: add tsconfig.json to configure TypeScript compiler settings

* feat: add icon.png and versions.json for app branding and version tracking

* remove original handwritten main.js pre-esbuild

* docs(license): adds license boilerplate

* docs(changelog): add initial changelog file with 0.1.2 entries

* docs(readme): add Bun build note and correct formatting

Add a note that build scripts rely on Bun, update code block language to
text, fix indentation of bullet points, and adjust line breaks for
consistency.

* refactor(main.js): update minified bundle variable names and improve drag handling logic

* refactor(DragOutPlugin): improve drag handling and selection logic
- Move event suppression after startDrag to avoid early exit
- Simplify selected path extraction with Array.from/map/filter
- Ensure current item is added only if not already selected
- Reorder comments for clarity and maintainability

* refactor(release): separate optional and required files, abort on missing required files

This change updates the release script to treat `icon.png` as an optional asset.
Missing required files now trigger an error and halt the release, while missing optional files only emit a warning.

* docs(changelog): update change notes for platform defaults, drag behavior, README, and icon fallback

- Make default modifier key platform‑aware (Ctrl on Windows/Linux, Alt on macOS).
- Clarify multi‑select drag behavior: selected items drag all, unselected drag one.
- Add settings copy describing current platform defaults and drag interception.
- Rework README for public audience, including Bun build commands and debugging tips.
- Add fallback for missing drag icon, using a tiny embedded image on macOS.
- Remove original hand‑written main.js test script.

* docs(readme): rewrite and update installation, usage, and build instructions

- Clarify plugin purpose and usage
- Update build commands to use Bun
- Add platform notes and modifier key details
- Improve formatting and consistency

* refactor(main.js): add base64 icon fallback and update modifier key text

* feat(main.ts): add fallback drag icon for macOS and improve icon handling

* docs(changelog): add minAppVersion validation to release script abort condition

* refactor(release.mjs): validate minAppVersion and use it in versions.json
This commit is contained in:
xberry1231 2026-04-25 03:03:49 -04:00 committed by GitHub
parent 61bf0721f1
commit 9b76d70473
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1004 additions and 0 deletions

63
CHANGELOG.md Normal file
View file

@ -0,0 +1,63 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.1.2] - 2026-04-25
### Added
- This changelog and license.
### Changed
- Default modifier key is now platform-aware: Windows and Linux default to Ctrl,
while macOS defaults to Alt/Option.
- Multi-select drag behavior is now more precise. Dragging a selected item sends
all selected items; dragging an unselected item sends only that item.
- Updated settings copy to describe the current platform defaults and the effect
of always intercepting file-tree drags.
- Reworked the README for a public audience, including current usage defaults,
manual install steps, Bun-based build commands, platform notes, and debugging
guidance.
### Fixed
- Moved the default behavior suppression to take place after the drag capture
operations succeed to avoid null behavior.
- Release script now aborts if either required file (manifest.json, main.js) are missing,
or if the manifest doesn't have a valid minAppVersion.
- Drag icon handling no longer assumes `icon.png` is bundled. If no icon file is
present, the plugin falls back to a tiny embedded native image so external
drag still has a non-empty icon for macOS.
### Removed
- Original hand-written main.js test script before running esbuild.
## [0.1.1] - 2026-04-25
### Changed
- Default modifier key is now Ctrl (Cmd on macOS) instead of Alt. Holding Ctrl
during drag matches Windows Explorer's "force copy" semantic, so the file
drops cleanly without a "release modifier first" step. Alt remains
selectable in settings for users who prefer it.
### Fixed
- TypeScript build no longer fails on `Electron.*` type references — proper
type imports replaced bare global references.
- `buildDragItem` return type now satisfies Electron's `Item` interface
(single-file drags include the required `file` field).
## [0.1.0] - 2026-04-25
### Added
- Initial release.
- Drag any file from Obsidian's file explorer to external applications using
the OS's native drag-and-drop, instead of Obsidian's default
`obsidian://open?...` URI.
- Multi-select drag support — Ctrl/Cmd-click multiple files, then drag any
one to bring them all.
- Folder drag support (experimental).
- Configurable modifier key (Ctrl, Alt, Shift, or none).
- Optional debug logging to developer console.
- Bundled drag image using the Obsidian community brand black icon.

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Xander Berry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

115
README.md Normal file
View file

@ -0,0 +1,115 @@
# Drag Out
Drag files from Obsidian's file explorer to other apps using native OS
drag-and-drop instead of dropping an `obsidian://` URL.
This is useful for browser upload fields, email attachments, chat apps, file
managers, and other places that expect an actual file from your filesystem.
**Status:** experimental, desktop only. Tested primarily on Windows so far;
macOS and Linux feedback is welcome.
## What It Does
Obsidian's file tree is a virtual UI. By default, dragging a note out of the
file tree gives other apps a link like `obsidian://open?vault=Vault&file=Note.md`
instead of the file itself.
Drag Out intercepts file-tree drags when your configured modifier key is held
and uses Electron's `webContents.startDrag` API to expose the real on-disk file
path. Normal Obsidian drag behavior is left alone when the modifier is not held.
## Installation
Drag Out is not in the Obsidian community plugin marketplace yet. To install it
manually:
1. Download the release assets: `main.js` and `manifest.json`. If the release
includes `icon.png`, download that too.
2. Open your vault folder.
3. Create this folder if it does not already exist:
```text
.obsidian/plugins/drag-out/
```
4. Put the downloaded files in that folder.
5. In Obsidian, go to **Settings > Community plugins > Installed plugins**,
refresh the plugin list, and enable **Drag Out**.
If Restricted Mode is on, Obsidian will ask you to turn it off before enabling
community plugins.
## Usage
Hold the modifier key while dragging a file or folder from Obsidian's left
sidebar file explorer to another app.
The default modifier is:
- **Ctrl** on Windows and Linux
- **Option / Alt** on macOS
You can change the modifier in **Settings > Drag Out**. Available options are
Ctrl/Cmd, Alt/Option, Shift, or None.
Multi-select is supported: select multiple files in Obsidian's file explorer,
then modifier-drag one of the selected files. If you drag an unselected file
while other files are selected, only the file you dragged is sent.
## Why A Modifier Key?
Electron's external drag API takes over the current drag operation. That means
Obsidian's own file-tree drag behavior, such as moving notes between folders,
cannot run during the same drag.
Using a modifier keeps normal Obsidian drags unchanged. If you always want file
drags to go to external apps, set the modifier to **None** in the plugin
settings.
## Platform Notes
- **Windows:** tested most heavily. Holding the modifier while dragging starts
an external file drag, so Obsidian's internal drop targets will not respond
during that drag. This is related to Electron's external drag behavior
([Electron #7118](https://github.com/electron/electron/issues/7118)).
- **macOS:** expected to work, but needs more real-world testing. The default
modifier is Option/Alt to avoid conflicting with common Cmd interactions.
- **Linux:** expected to work where Obsidian's Electron drag API is available,
but currently less tested than Windows.
- **Mobile:** not supported. The plugin is marked `isDesktopOnly`.
- **Cloud placeholders:** files that are not hydrated locally yet, such as
OneDrive placeholders, may need to download before the target app can use
them.
## Building From Source
This repo uses Bun for its scripts.
```bash
bun install
bun run build
```
For development watch mode:
```bash
bun run dev
```
The build reads `main.ts` and writes the bundled plugin entrypoint to
`main.js`. `manifest.json` is maintained separately.
## Debugging
Enable **Debug logging** in the plugin settings, then open Obsidian's developer
console with `Ctrl+Shift+I` and try a drag. A successful external drag logs
the resolved filesystem paths.
If you see `Drag Out: could not access Electron's webContents`, please report
your Obsidian version, operating system, and whether you installed from a
release or built from source.
## License
MIT

247
bun.lock Normal file
View file

@ -0,0 +1,247 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "obsidian-drag-out",
"devDependencies": {
"@types/node": "^20.0.0",
"builtin-modules": "3.3.0",
"electron": "^39.0.0",
"esbuild": "0.24.0",
"obsidian": "^1.4.11",
"tslib": "2.7.0",
"typescript": "5.5.4",
},
},
},
"packages": {
"@codemirror/state": ["@codemirror/state@6.5.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw=="],
"@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="],
"@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.24.0", "", { "os": "android", "cpu": "arm" }, "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.24.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.24.0", "", { "os": "android", "cpu": "x64" }, "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.24.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.24.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.24.0", "", { "os": "linux", "cpu": "arm" }, "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.24.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.24.0", "", { "os": "linux", "cpu": "none" }, "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.24.0", "", { "os": "linux", "cpu": "none" }, "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.24.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.24.0", "", { "os": "linux", "cpu": "none" }, "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.24.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.24.0", "", { "os": "none", "cpu": "x64" }, "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.24.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.24.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.24.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.24.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.24.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.0", "", { "os": "win32", "cpu": "x64" }, "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA=="],
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
"@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
"@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="],
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
"@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="],
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
"@types/node": ["@types/node@20.19.39", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw=="],
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
"@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="],
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"builtin-modules": ["builtin-modules@3.3.0", "", {}, "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw=="],
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
"cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],
"clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
"electron": ["electron@39.8.9", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-0zgNu0MmWR/kzFm+B+LlvpLOX8l3wt1z+BJIYv2Uxc2bEqWBT3jNsX1IhPVP5OJijjZO5ewCOIy0n/bsfJfjdw=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
"esbuild": ["esbuild@0.24.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.0", "@esbuild/android-arm": "0.24.0", "@esbuild/android-arm64": "0.24.0", "@esbuild/android-x64": "0.24.0", "@esbuild/darwin-arm64": "0.24.0", "@esbuild/darwin-x64": "0.24.0", "@esbuild/freebsd-arm64": "0.24.0", "@esbuild/freebsd-x64": "0.24.0", "@esbuild/linux-arm": "0.24.0", "@esbuild/linux-arm64": "0.24.0", "@esbuild/linux-ia32": "0.24.0", "@esbuild/linux-loong64": "0.24.0", "@esbuild/linux-mips64el": "0.24.0", "@esbuild/linux-ppc64": "0.24.0", "@esbuild/linux-riscv64": "0.24.0", "@esbuild/linux-s390x": "0.24.0", "@esbuild/linux-x64": "0.24.0", "@esbuild/netbsd-x64": "0.24.0", "@esbuild/openbsd-arm64": "0.24.0", "@esbuild/openbsd-x64": "0.24.0", "@esbuild/sunos-x64": "0.24.0", "@esbuild/win32-arm64": "0.24.0", "@esbuild/win32-ia32": "0.24.0", "@esbuild/win32-x64": "0.24.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
"fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
"jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
"obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
"resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
"sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],
"tslib": ["tslib@2.7.0", "", {}, "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="],
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"typescript": ["typescript@5.5.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
"@types/cacheable-request/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
"@types/keyv/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
"@types/responselike/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
"@types/yauzl/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
"clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
"electron/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
"global-agent/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
}
}

48
esbuild.config.mjs Normal file
View file

@ -0,0 +1,48 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const context = await esbuild.context({
banner: { js: banner },
entryPoints: ["main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@electron/remote",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins
],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

6
main.js Normal file
View file

@ -0,0 +1,6 @@
/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
var y=Object.create;var u=Object.defineProperty;var b=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var C=Object.getPrototypeOf,w=Object.prototype.hasOwnProperty;var S=(s,n)=>{for(var t in n)u(s,t,{get:n[t],enumerable:!0})},m=(s,n,t,e)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of D(n))!w.call(s,i)&&i!==t&&u(s,i,{get:()=>n[i],enumerable:!(e=b(n,i))||e.enumerable});return s};var E=(s,n,t)=>(t=s!=null?y(C(s)):{},m(n||!s||!s.__esModule?u(t,"default",{value:s,enumerable:!0}):t,s)),P=s=>m(u({},"__esModule",{value:!0}),s);var L={};S(L,{default:()=>f});module.exports=P(L);var a=require("obsidian"),v=require("fs"),h=E(require("path")),A={modifierKey:process.platform==="darwin"?"alt":"ctrl",debugLogging:!1},I=".nav-file-title, .nav-folder-title",O="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=",f=class extends a.Plugin{constructor(){super(...arguments);this.settings=A;this.iconPath=null}async onload(){await this.loadSettings(),this.resolveIconPath(),this.registerDomEvent(document,"dragstart",t=>this.handleDragStart(t),{capture:!0}),this.addSettingTab(new p(this.app,this)),this.log("Drag Out loaded")}onunload(){this.log("Drag Out unloaded")}resolveIconPath(){let t=this.app.vault.adapter;if(!(t instanceof a.FileSystemAdapter)){this.iconPath=null;return}let e=h.join(t.getBasePath(),this.app.vault.configDir,"plugins",this.manifest.id),i=h.join(e,"icon.png");this.iconPath=(0,v.existsSync)(i)?i:null}isModifierPressed(t){switch(this.settings.modifierKey){case"alt":return t.altKey;case"shift":return t.shiftKey;case"ctrl":return t.ctrlKey||t.metaKey;case"none":return!0}}handleDragStart(t){if(!this.isModifierPressed(t))return;let e=t.target;if(!e)return;let i=e.closest(I);if(!i)return;let c=i.getAttribute("data-path");if(!c){this.log("file tree item missing data-path attribute");return}let o=this.app.vault.adapter;if(!(o instanceof a.FileSystemAdapter))return;let d=o.getBasePath(),g=this.collectSelectedAbsolutePaths(c,d);if(g.length===0){this.log("no paths resolved, falling through to default drag");return}let r=this.getWebContents();if(!r){new a.Notice("Drag Out: could not access Electron's webContents. Check the developer console (Ctrl+Shift+I) for details.");return}try{let l=this.buildDragItem(g);r.startDrag(l),t.preventDefault(),t.stopPropagation(),this.log("startDrag invoked",g)}catch(l){console.error("[DragOut] startDrag failed:",l),new a.Notice("Drag Out: drag failed. See console for details.")}}buildDragItem(t){let e=this.getDragIcon();return t.length===1?{file:t[0],icon:e}:{file:t[0],files:t,icon:e}}getDragIcon(){if(this.iconPath)return this.iconPath;let t=this.requireElectron(),e=t.nativeImage.createFromDataURL(O);return e.isEmpty()?t.nativeImage.createEmpty():e}collectSelectedAbsolutePaths(t,e){let i=document.querySelectorAll(".nav-file-title.is-selected, .nav-folder-title.is-selected"),c=new Set,o=[],d=r=>{let l=h.join(e,r);c.has(l)||(c.add(l),o.push(l))};if(i.length===0)return d(t),o;let g=Array.from(i).map(r=>r.getAttribute("data-path")).filter(r=>!!r);return g.includes(t)?(g.forEach(d),o):(d(t),o)}getWebContents(){var t,e;try{let i=(t=window.require)==null?void 0:t.call(window,"@electron/remote");if(i!=null&&i.getCurrentWebContents)return i.getCurrentWebContents()}catch(i){}try{let i=this.requireElectron();if((e=i.remote)!=null&&e.getCurrentWebContents)return i.remote.getCurrentWebContents()}catch(i){}return null}requireElectron(){var e;return((e=window.require)!=null?e:require)("electron")}log(...t){this.settings.debugLogging&&console.log("[DragOut]",...t)}async loadSettings(){this.settings=Object.assign({},A,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}},p=class extends a.PluginSettingTab{constructor(n,t){super(n,t),this.plugin=t}display(){let{containerEl:n}=this;n.empty(),new a.Setting(n).setName("Modifier key").setDesc("Hold this key while dragging a file from the file explorer to drag the actual file (instead of an obsidian:// link). Pick 'None' to always use external drag \u2014 but be aware this disables Obsidian's built-in file-tree drag behavior.").addDropdown(t=>t.addOption("ctrl","Ctrl / Cmd (default Win/Linux)").addOption("alt","Alt / Option (default Mac)").addOption("shift","Shift").addOption("none","None \u2014 always intercept").setValue(this.plugin.settings.modifierKey).onChange(async e=>{this.plugin.settings.modifierKey=e,await this.plugin.saveSettings()})),new a.Setting(n).setName("Debug logging").setDesc("Log drag events to the developer console (Ctrl+Shift+I). Useful for troubleshooting; leave off otherwise.").addToggle(t=>t.setValue(this.plugin.settings.debugLogging).onChange(async e=>{this.plugin.settings.debugLogging=e,await this.plugin.saveSettings()}))}};

338
main.ts Normal file
View file

@ -0,0 +1,338 @@
import {
App,
FileSystemAdapter,
Notice,
Plugin,
PluginSettingTab,
Setting,
} from "obsidian";
import type { WebContents, NativeImage } from "electron";
import { existsSync } from "fs";
import * as path from "path";
type ModifierKey = "alt" | "shift" | "ctrl" | "none";
interface DragOutSettings {
/** Modifier key required to trigger external drag. "none" = always on. */
modifierKey: ModifierKey;
/** Show a brief notice on first successful drag (helpful for verifying setup). */
debugLogging: boolean;
}
const DEFAULT_SETTINGS: DragOutSettings = {
modifierKey: process.platform === "darwin" ? "alt" : "ctrl",
debugLogging: false,
};
/**
* Selectors for items in Obsidian's file tree that we want to intercept
* drags from. .nav-file-title is a file, .nav-folder-title is a folder.
*/
const FILE_TREE_SELECTOR = ".nav-file-title, .nav-folder-title";
// Electron requires a non-empty drag icon on macOS. Keep a tiny fallback so
// releases still work even when no icon.png is bundled.
const FALLBACK_ICON_DATA_URL =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=";
export default class DragOutPlugin extends Plugin {
settings: DragOutSettings = DEFAULT_SETTINGS;
private iconPath: string | null = null;
async onload() {
await this.loadSettings();
// Resolve absolute path to our bundled icon (used as drag image).
this.resolveIconPath();
// Register dragstart at capture phase so we run before Obsidian's
// own handler that sets the obsidian:// URI on the dataTransfer.
this.registerDomEvent(
document,
"dragstart",
(evt) => this.handleDragStart(evt),
{ capture: true }
);
this.addSettingTab(new DragOutSettingTab(this.app, this));
this.log("Drag Out loaded");
}
onunload() {
this.log("Drag Out unloaded");
}
private resolveIconPath(): void {
const adapter = this.app.vault.adapter;
if (!(adapter instanceof FileSystemAdapter)) {
this.iconPath = null;
return;
}
const pluginDir = path.join(
adapter.getBasePath(),
this.app.vault.configDir,
"plugins",
this.manifest.id
);
const candidateIconPath = path.join(pluginDir, "icon.png");
this.iconPath = existsSync(candidateIconPath) ? candidateIconPath : null;
}
private isModifierPressed(evt: DragEvent): boolean {
switch (this.settings.modifierKey) {
case "alt":
return evt.altKey;
case "shift":
return evt.shiftKey;
case "ctrl":
// Ctrl on Windows/Linux, Cmd on Mac
return evt.ctrlKey || evt.metaKey;
case "none":
return true;
}
}
private handleDragStart(evt: DragEvent): void {
if (!this.isModifierPressed(evt)) return;
const target = evt.target as HTMLElement | null;
if (!target) return;
// Only intercept drags that originate inside the file tree.
const fileItem = target.closest(FILE_TREE_SELECTOR) as HTMLElement | null;
if (!fileItem) return;
const relativePath = fileItem.getAttribute("data-path");
if (!relativePath) {
this.log("file tree item missing data-path attribute");
return;
}
const adapter = this.app.vault.adapter;
if (!(adapter instanceof FileSystemAdapter)) {
// Not a desktop / FS-backed vault. Nothing we can do.
return;
}
const basePath = adapter.getBasePath();
// Collect all selected paths in case of multi-select drag.
const absolutePaths = this.collectSelectedAbsolutePaths(
relativePath,
basePath
);
if (absolutePaths.length === 0) {
this.log("no paths resolved, falling through to default drag");
return;
}
// Reach into Electron and call webContents.startDrag.
const webContents = this.getWebContents();
if (!webContents) {
new Notice(
"Drag Out: could not access Electron's webContents. " +
"Check the developer console (Ctrl+Shift+I) for details."
);
return;
}
try {
const dragItem = this.buildDragItem(absolutePaths);
webContents.startDrag(dragItem);
// Suppress Obsidian's default behavior (which writes an obsidian:// URI
// onto the clipboard / dataTransfer). We're taking over the drag.
evt.preventDefault();
evt.stopPropagation();
this.log("startDrag invoked", absolutePaths);
} catch (err) {
console.error("[DragOut] startDrag failed:", err);
new Notice("Drag Out: drag failed. See console for details.");
}
}
/**
* Build the argument object passed to webContents.startDrag.
* - Single path: pass `file`.
* - Multiple paths: pass `files` (overrides `file`).
* - `icon` is required and must be non-empty on macOS.
*/
private buildDragItem(absolutePaths: string[]): {
file: string;
files?: string[];
icon: string | NativeImage;
} {
const icon = this.getDragIcon();
if (absolutePaths.length === 1) {
return { file: absolutePaths[0], icon };
}
return { file: absolutePaths[0], files: absolutePaths, icon };
}
private getDragIcon(): string | NativeImage {
if (this.iconPath) {
return this.iconPath;
}
const electron = this.requireElectron();
const fallbackIcon =
electron.nativeImage.createFromDataURL(FALLBACK_ICON_DATA_URL);
return fallbackIcon.isEmpty()
? electron.nativeImage.createEmpty()
: fallbackIcon;
}
/**
* Walk the visible file tree to find every currently-selected item.
* If nothing is "selected" (single-click drag), we just return the
* item the drag started on. Folders pass through too Electron's
* startDrag accepts folder paths on Windows and macOS.
*/
private collectSelectedAbsolutePaths(
currentRelative: string,
basePath: string
): string[] {
const selectedNodes = document.querySelectorAll(
".nav-file-title.is-selected, .nav-folder-title.is-selected"
);
const seen = new Set<string>();
const out: string[] = [];
const push = (rel: string) => {
const abs = path.join(basePath, rel);
if (!seen.has(abs)) {
seen.add(abs);
out.push(abs);
}
};
if (selectedNodes.length === 0) {
push(currentRelative);
return out;
}
const selectedRelativePaths = Array.from(selectedNodes)
.map((el) => el.getAttribute("data-path"))
.filter((rel): rel is string => !!rel);
if (!selectedRelativePaths.includes(currentRelative)) {
push(currentRelative);
return out;
}
selectedRelativePaths.forEach(push);
return out;
}
/**
* Resolve Electron's webContents from the renderer. Tries the modern
* @electron/remote first, then falls back to the legacy electron.remote.
* Returns null if neither is reachable.
*/
private getWebContents(): WebContents | null {
// Path 1: @electron/remote (modern, requires opt-in by host)
try {
const electronRemote = (window as any).require?.(
"@electron/remote"
);
if (electronRemote?.getCurrentWebContents) {
return electronRemote.getCurrentWebContents();
}
} catch (_) {
/* fall through */
}
// Path 2: legacy electron.remote (pre-Electron 14 — Obsidian still
// exposes this on at least some versions).
try {
const electron = this.requireElectron();
if ((electron as any).remote?.getCurrentWebContents) {
return (electron as any).remote.getCurrentWebContents();
}
} catch (_) {
/* fall through */
}
return null;
}
private requireElectron(): typeof import("electron") {
// Use window.require to bypass any bundler that might have rewritten
// a top-level require call into something else.
const req = (window as any).require ?? require;
return req("electron");
}
private log(...args: unknown[]): void {
if (this.settings.debugLogging) {
console.log("[DragOut]", ...args);
}
}
async loadSettings(): Promise<void> {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
}
async saveSettings(): Promise<void> {
await this.saveData(this.settings);
}
}
class DragOutSettingTab extends PluginSettingTab {
plugin: DragOutPlugin;
constructor(app: App, plugin: DragOutPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName("Modifier key")
.setDesc(
"Hold this key while dragging a file from the file explorer " +
"to drag the actual file (instead of an obsidian:// link). " +
"Pick 'None' to always use external drag — but be aware " +
"this disables Obsidian's built-in file-tree drag behavior."
)
.addDropdown((dd) =>
dd
.addOption("ctrl", "Ctrl / Cmd (default Win/Linux)")
.addOption("alt", "Alt / Option (default Mac)")
.addOption("shift", "Shift")
.addOption("none", "None — always intercept")
.setValue(this.plugin.settings.modifierKey)
.onChange(async (value) => {
this.plugin.settings.modifierKey =
value as ModifierKey;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Debug logging")
.setDesc(
"Log drag events to the developer console (Ctrl+Shift+I). " +
"Useful for troubleshooting; leave off otherwise."
)
.addToggle((tg) =>
tg
.setValue(this.plugin.settings.debugLogging)
.onChange(async (value) => {
this.plugin.settings.debugLogging = value;
await this.plugin.saveSettings();
})
);
}
}

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "drag-out",
"name": "Drag Out",
"version": "0.1.1",
"minAppVersion": "1.5.0",
"description": "Drag files from Obsidian's file explorer to other applications (local filesystem, browser uploads, email attachments, etc.) with native OS drag handles instead of obsidian:// URIs.",
"author": "Xander Berry",
"authorUrl": "https://github.com/xwberry",
"isDesktopOnly": true
}

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "obsidian-drag-out",
"version": "0.1.1",
"description": "Drag files from Obsidian's file explorer to other applications (local filesystem, browser uploads, email attachments, etc.) with native OS drag handles instead of obsidian:// URIs.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && bun esbuild.config.mjs production",
"release": "bun release.mjs"
},
"keywords": ["obsidian", "plugin", "drag-and-drop"],
"author": "Xander Berry",
"authorUrl": "https://github.com/xwberry",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.0.0",
"builtin-modules": "3.3.0",
"electron": "^39.0.0",
"esbuild": "0.24.0",
"obsidian": "^1.4.11",
"tslib": "2.7.0",
"typescript": "5.5.4"
}
}

103
release.mjs Normal file
View file

@ -0,0 +1,103 @@
/*
* release.mjs
*
* Run after bumping the version in package.json (e.g. via `bun pm version patch`).
*
* Steps:
* 1. Read version from package.json
* 2. Sync into manifest.json
* 3. Add entry to versions.json (mapping version -> manifest.minAppVersion)
* 4. Run production build
* 5. Copy release artifacts into dist/<version>/
*
* Usage: bun run release
*/
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { execSync } from "node:child_process";
const RELEASE_FILES = ["main.js", "manifest.json", "icon.png"];
// --- 1. Read target version from package.json ------------------------------
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
const minAppVersion = manifest.minAppVersion;
if (!minAppVersion || typeof minAppVersion !== "string") {
console.error("manifest.json has no valid 'minAppVersion'. Aborting.");
process.exit(1);
}
const version = pkg.version;
if (!version) {
console.error("package.json has no 'version' field. Aborting.");
process.exit(1);
}
console.log(`Releasing version ${version}\n`);
// --- 2. Sync manifest.json -------------------------------------------------
const manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const previousManifestVersion = manifest.version;
manifest.version = version;
writeFileSync("manifest.json", JSON.stringify(manifest, null, 2) + "\n");
console.log(` manifest.json: ${previousManifestVersion} -> ${version}`);
// --- 3. Update versions.json -----------------------------------------------
const versions = JSON.parse(readFileSync("versions.json", "utf8"));
if (versions[version]) {
console.log(` versions.json: entry for ${version} already exists (${versions[version]})`);
} else {
versions[version] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, 2) + "\n");
console.log(` versions.json: added ${version} -> ${minAppVersion}`);
}
// --- 4. Build production ---------------------------------------------------
console.log("\nBuilding production bundle...");
try {
execSync("bun run build", { stdio: "inherit" });
} catch (err) {
console.error("\nBuild failed. Aborting release.");
process.exit(1);
}
// --- 5. Copy artifacts to dist/<version>/ ----------------------------------
const distDir = join("dist", version);
mkdirSync(distDir, { recursive: true });
console.log(`\nStaging release files in ${distDir}/`);
const missingRequired = [];
const missingOptional = [];
for (const file of RELEASE_FILES) {
if (!existsSync(file)) {
if (file === "icon.png") {
missingOptional.push(file);
} else {
missingRequired.push(file);
}
continue;
}
copyFileSync(file, join(distDir, file));
console.log(` ${file}`);
}
if (missingOptional.length > 0) {
console.warn(`\nWARNING: missing optional files (skipped): ${missingOptional.join(", ")}`);
}
if (missingRequired.length > 0) {
console.error(`\nERROR: missing required release files (aborting): ${missingRequired.join(", ")}`);
process.exit(1);
}
// --- Done ------------------------------------------------------------------
console.log(`
Release ${version} prepared.
Next steps:
git add -A
git commit -m "Release ${version}"
git push && git push --tags
Then on GitHub:
- Draft a new release at tag v${version}
- Attach release files from ${distDir}/ as LOOSE assets (main.js, manifest.json, and icon.png if present; do not zip)
- Publish
`);

26
tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES2018",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"esModuleInterop": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7",
"ES2018"
]
},
"include": [
"**/*.ts"
]
}

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"0.1.1": "1.5.0"
}