Compare commits
No commits in common. "main" and "v0.1.0-alpha.1" have entirely different histories.
main
...
v0.1.0-alp
|
|
@ -2,7 +2,6 @@
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
build/
|
build/
|
||||||
verstak-desktop-res.syso
|
|
||||||
smoke-platform
|
smoke-platform
|
||||||
plugins/
|
plugins/
|
||||||
vendor/
|
vendor/
|
||||||
|
|
|
||||||
433
README.md
433
README.md
|
|
@ -1,341 +1,208 @@
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
<img src="packaging/linux/verstak.svg" width="112" alt="Verstak logo">
|
|
||||||
|
|
||||||
# Verstak
|
# Verstak
|
||||||
|
|
||||||
### Keep the context of your work together — locally.
|
Verstak is a local-first workspace for files, notes, browser captures,
|
||||||
|
activity and work-journal entries. This repository is the desktop application:
|
||||||
|
the Go/Wails host and UI shell that loads plugins from a `plugins/` directory
|
||||||
|
next to its executable.
|
||||||
|
|
||||||
Files, notes, links, browser captures, activity and work history
|
> **Alpha software.** Use a disposable vault while evaluating it. APIs, storage
|
||||||
in one extensible local-first workspace.
|
> formats and packaging can change before the first stable release.
|
||||||
|
|
||||||
**English** · [Русский](README.ru.md)
|
## Components
|
||||||
|
|
||||||
[](https://github.com/mirivlad/verstak/releases)
|
The public alpha is split into small repositories. Keep their `main` branches
|
||||||

|
in the same release line when building from source.
|
||||||

|
|
||||||
[](LICENSE)
|
|
||||||
|
|
||||||
[Download](https://github.com/mirivlad/verstak/releases/latest) ·
|
| Component | Repository | What it is for |
|
||||||
[Documentation](https://github.com/mirivlad/verstak-docs) ·
|
| --- | --- | --- |
|
||||||
[Report an issue](https://github.com/mirivlad/verstak/issues)
|
| Desktop | [mirivlad/verstak](https://github.com/mirivlad/verstak) | This application: vault UI, local host and plugin runtime. |
|
||||||
|
| Official plugins | [mirivlad/verstak-official-plugins](https://github.com/mirivlad/verstak-official-plugins) | Files, Notes, Browser Inbox, Activity, Journal, Sync, Todo and other first-party plugins. |
|
||||||
|
| Browser extension | [mirivlad/verstak-browser-extension](https://github.com/mirivlad/verstak-browser-extension) | Manual browser captures and opt-in domain-time activity. |
|
||||||
|
| Sync server | [mirivlad/verstak-sync-server](https://github.com/mirivlad/verstak-sync-server) | Optional self-hosted synchronization between devices. |
|
||||||
|
| Plugin SDK | [mirivlad/verstak-sdk](https://github.com/mirivlad/verstak-sdk) | TypeScript API, JSON schemas and contract tests for plugin authors. |
|
||||||
|
| Architecture documentation | [mirivlad/verstak-docs](https://github.com/mirivlad/verstak-docs) | Product, platform and plugin-system design documents. |
|
||||||
|
|
||||||
</div>
|
No server account or browser extension is needed for a local desktop vault.
|
||||||
|
|
||||||
> [!WARNING]
|
## Build the desktop and official plugins
|
||||||
> Verstak is currently **alpha software**. APIs, storage formats and packaging may change before the first stable release. Use a test vault and keep backups of important data.
|
|
||||||
|
|
||||||
## What is Verstak?
|
To build from source, install Go 1.24+, Node.js 20+ with npm, Python 3, the [Wails v2 build
|
||||||
|
prerequisites](https://wails.io/docs/gettingstarted/installation/), and your
|
||||||
|
distribution's WebKitGTK development package.
|
||||||
|
|
||||||
Verstak is a local-first workbench that keeps the context around your work in one place.
|
Clone the repositories as siblings. The directory names below are intentional:
|
||||||
|
the desktop helper finds the official-plugin checkout at
|
||||||
A **workspace** can represent almost anything:
|
`../verstak-official-plugins`.
|
||||||
|
|
||||||
* a software project;
|
|
||||||
* a client or customer;
|
|
||||||
* a server or infrastructure site;
|
|
||||||
* a device under repair;
|
|
||||||
* an article or research topic;
|
|
||||||
* a training course;
|
|
||||||
* a personal long-term project.
|
|
||||||
|
|
||||||
Normally, information about such work is scattered across directories, notes, browser tabs, task managers, password managers, terminal history and memory.
|
|
||||||
|
|
||||||
Verstak brings it together in a local **vault** that remains under your control.
|
|
||||||
|
|
||||||
No account, cloud service or sync server is required for local use.
|
|
||||||
|
|
||||||
## Verstak in use
|
|
||||||
|
|
||||||
| Overview: return to a workspace and see its recent work, captures and items that need attention. | Files: manage ordinary folders and documents inside a workspace. |
|
|
||||||
| --- | --- |
|
|
||||||
|  |  |
|
|
||||||
|
|
||||||
| Notes: keep Markdown notes next to the workspace they describe. | Journal: record a completed session and retain its context. |
|
|
||||||
| --- | --- |
|
|
||||||
|  |  |
|
|
||||||
|
|
||||||
## Main features
|
|
||||||
|
|
||||||
| Feature | Description |
|
|
||||||
| ------------------------ | -------------------------------------------------------------------------------------------- |
|
|
||||||
| **Workspaces** | Organize files, notes and activity around durable projects, clients and other areas of work. |
|
|
||||||
| **Files** | Browse and manage ordinary files stored inside your vault. |
|
|
||||||
| **Notes** | Create Markdown notes, overview pages and links between related information. |
|
|
||||||
| **Overview** | Quickly return to recent work and see what may need attention. |
|
|
||||||
| **Browser Inbox** | Send pages, links, selections and files from a browser into Verstak. |
|
|
||||||
| **Activity and Journal** | Reconstruct work sessions and turn selected activity into journal entries. |
|
|
||||||
| **Todo** | Keep optional task lists inside individual workspaces and across the whole vault. |
|
|
||||||
| **Search** | Search across notes, files and supported plugin data. |
|
|
||||||
| **Trash** | Restore deleted items or remove them permanently from one central location. |
|
|
||||||
| **Secrets** | Keep credentials and access information connected to the relevant workspace. |
|
|
||||||
| **Templates** | Create repeatable workspace structures for common types of work. |
|
|
||||||
| **Plugins** | Add, replace or disable tools without turning Verstak into one rigid monolithic application. |
|
|
||||||
| **Optional sync** | Synchronize vaults between devices using a self-hosted Verstak Sync Server. |
|
|
||||||
|
|
||||||
## Download and install
|
|
||||||
|
|
||||||
Download the latest build from the
|
|
||||||
[GitHub Releases page](https://github.com/mirivlad/verstak/releases/latest).
|
|
||||||
|
|
||||||
Release packages already include the matching official plugins.
|
|
||||||
|
|
||||||
| System | Download | Installation |
|
|
||||||
| --------------------------------- | ----------------------------------------- | --------------------------------------------- |
|
|
||||||
| Debian 13 / Ubuntu 24.04 or newer | `verstak_<version>_amd64.deb` | Install through APT. |
|
|
||||||
| Other x86_64 Linux distributions | `verstak-linux-x86_64-<version>.AppImage` | Make the file executable and run it. |
|
|
||||||
| Windows 10/11 x64 | `verstak-windows-amd64-<version>.zip` | Extract the archive and launch `Verstak.cmd`. |
|
|
||||||
|
|
||||||
### Debian and Ubuntu
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install ./verstak_*_amd64.deb
|
|
||||||
```
|
|
||||||
|
|
||||||
Launch Verstak from the application menu or run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
verstak
|
|
||||||
```
|
|
||||||
|
|
||||||
### AppImage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x verstak-linux-x86_64-*.AppImage
|
|
||||||
./verstak-linux-x86_64-*.AppImage
|
|
||||||
```
|
|
||||||
|
|
||||||
On a system without FUSE support:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
APPIMAGE_EXTRACT_AND_RUN=1 ./verstak-linux-x86_64-*.AppImage
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windows portable version
|
|
||||||
|
|
||||||
1. Download `verstak-windows-amd64-<version>.zip`.
|
|
||||||
2. Extract it to a local directory.
|
|
||||||
3. Run `Verstak.cmd`.
|
|
||||||
|
|
||||||
Do not run Verstak directly from inside the ZIP archive or from a network share.
|
|
||||||
|
|
||||||
Verstak uses the Microsoft WebView2 Runtime. It is already included with Windows 11 and most current Windows 10 installations. If Verstak does not start, install the
|
|
||||||
[Microsoft WebView2 Runtime x64](https://go.microsoft.com/fwlink/p/?LinkId=2124701).
|
|
||||||
|
|
||||||
## Background and tray mode
|
|
||||||
|
|
||||||
Closing the main window keeps Verstak running in the system tray. Use **Show Verstak** to bring it back and **Quit** to exit the application completely. This lets scheduled Todo reminders continue while the window is hidden.
|
|
||||||
|
|
||||||
### Verify a download
|
|
||||||
|
|
||||||
Each release includes a `SHA256SUMS` file.
|
|
||||||
|
|
||||||
On Linux:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sha256sum -c SHA256SUMS --ignore-missing
|
|
||||||
```
|
|
||||||
|
|
||||||
## First start
|
|
||||||
|
|
||||||
### 1. Create or open a vault
|
|
||||||
|
|
||||||
A **vault** is the root directory where Verstak stores your work.
|
|
||||||
|
|
||||||
Choose a writable local directory. For example:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Documents/
|
|
||||||
└── Verstak/
|
|
||||||
```
|
|
||||||
|
|
||||||
Your files and notes remain ordinary files that can be accessed without Verstak.
|
|
||||||
|
|
||||||
### 2. Create your first workspace
|
|
||||||
|
|
||||||
Inside the vault, create a workspace for a project, client, server, device or another area of work.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Home server
|
|
||||||
Customer Alpha
|
|
||||||
Verstak development
|
|
||||||
3D printer
|
|
||||||
Training course
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Add context
|
|
||||||
|
|
||||||
Open the workspace and add the information needed to return to it later:
|
|
||||||
|
|
||||||
* notes and decisions;
|
|
||||||
* documents and source files;
|
|
||||||
* useful links;
|
|
||||||
* access information;
|
|
||||||
* journal entries;
|
|
||||||
* tasks;
|
|
||||||
* materials captured from the browser.
|
|
||||||
|
|
||||||
### 4. Return through Overview
|
|
||||||
|
|
||||||
The Overview screen shows recent changes, unfinished captures, activity and other useful entry points back into your work.
|
|
||||||
|
|
||||||
## How Verstak stores data
|
|
||||||
|
|
||||||
Verstak follows several principles:
|
|
||||||
|
|
||||||
* your vault is local and works without an account or internet connection;
|
|
||||||
* files and notes remain readable outside the application;
|
|
||||||
* synchronization is optional and is not the source of truth;
|
|
||||||
* the user should understand where data is stored and what happens to it;
|
|
||||||
* application tools are provided by plugins rather than being permanently embedded into the core.
|
|
||||||
|
|
||||||
The desktop application loads plugins from a `plugins/` directory located beside the executable.
|
|
||||||
|
|
||||||
## Browser integration
|
|
||||||
|
|
||||||
The optional browser extension can send the following into Browser Inbox:
|
|
||||||
|
|
||||||
* the current page;
|
|
||||||
* selected text;
|
|
||||||
* a link;
|
|
||||||
* a downloaded or selected file.
|
|
||||||
|
|
||||||
Repository:
|
|
||||||
|
|
||||||
[mirivlad/verstak-browser-extension](https://github.com/mirivlad/verstak-browser-extension)
|
|
||||||
|
|
||||||
To connect the extension:
|
|
||||||
|
|
||||||
1. Open Browser Inbox settings in Verstak.
|
|
||||||
2. Copy the Receiver URL and Pairing Token.
|
|
||||||
3. Paste them into the extension settings.
|
|
||||||
4. Save the settings and send a test page.
|
|
||||||
|
|
||||||
Passive domain activity tracking is disabled by default. When enabled, it sends bounded time totals by normalized domain. It does not send page contents, keystrokes or full browsing history.
|
|
||||||
|
|
||||||
## Optional synchronization
|
|
||||||
|
|
||||||
Local use does not require a server.
|
|
||||||
|
|
||||||
For synchronization between devices, deploy the optional self-hosted
|
|
||||||
[Verstak Sync Server](https://github.com/mirivlad/verstak-sync-server).
|
|
||||||
|
|
||||||
Each vault is connected separately. The local vault remains the primary copy of your data.
|
|
||||||
|
|
||||||
## Build from source
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
* Go 1.24 or newer;
|
|
||||||
* Node.js 20 or newer with npm;
|
|
||||||
* Python 3;
|
|
||||||
* Git;
|
|
||||||
* Wails v2 build dependencies;
|
|
||||||
* WebKitGTK development packages on Linux.
|
|
||||||
* Ayatana AppIndicator development files on Linux (`sudo apt install libayatana-appindicator3-dev`).
|
|
||||||
|
|
||||||
See the
|
|
||||||
[Wails installation documentation](https://wails.io/docs/gettingstarted/installation/)
|
|
||||||
for distribution-specific dependencies.
|
|
||||||
|
|
||||||
### Clone the repositories
|
|
||||||
|
|
||||||
The desktop, SDK and official plugin repositories must be cloned as sibling directories:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
verstak-workspace/
|
verstak-workspace/
|
||||||
├── verstak/
|
├── verstak/
|
||||||
├── verstak-sdk/
|
├── verstak-sdk/
|
||||||
└── verstak-official-plugins/
|
├── verstak-official-plugins/
|
||||||
|
└── verstak-browser-extension/ # optional for browser integration
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir verstak-workspace
|
git clone https://github.com/mirivlad/verstak.git verstak
|
||||||
cd verstak-workspace
|
git clone https://github.com/mirivlad/verstak-sdk.git verstak-sdk
|
||||||
|
git clone https://github.com/mirivlad/verstak-official-plugins.git verstak-official-plugins
|
||||||
git clone https://github.com/mirivlad/verstak.git
|
git clone https://github.com/mirivlad/verstak-browser-extension.git verstak-browser-extension
|
||||||
git clone https://github.com/mirivlad/verstak-sdk.git
|
|
||||||
git clone https://github.com/mirivlad/verstak-official-plugins.git
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd verstak-sdk
|
|
||||||
./scripts/build.sh
|
|
||||||
|
|
||||||
cd ../verstak-official-plugins
|
|
||||||
./scripts/build.sh
|
|
||||||
|
|
||||||
|
cd verstak-sdk && ./scripts/build.sh
|
||||||
|
cd ../verstak-official-plugins && ./scripts/build.sh
|
||||||
cd ../verstak
|
cd ../verstak
|
||||||
./scripts/install-dev-plugins.sh
|
./scripts/install-dev-plugins.sh
|
||||||
./scripts/build.sh
|
./scripts/build.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting application will be located at:
|
`install-dev-plugins.sh` copies the packages from
|
||||||
|
`../verstak-official-plugins/dist/` into this repository's `plugins/`
|
||||||
|
directory. `build.sh` then copies them to `build/bin/plugins/`, beside the
|
||||||
|
desktop executable:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build/bin/verstak-desktop
|
./build/bin/verstak-desktop
|
||||||
```
|
```
|
||||||
|
|
||||||
Run it with additional diagnostics:
|
For a manually assembled installation, place each unpacked plugin directory
|
||||||
|
directly in `plugins/` beside `verstak-desktop`; for example,
|
||||||
|
`plugins/browser-inbox/plugin.json`. Do not put the release archive itself in
|
||||||
|
that directory. The desktop release archive already includes its matching
|
||||||
|
`plugins/` directory.
|
||||||
|
|
||||||
|
Start with `--debug` to show internal plugin-provider identifiers and write
|
||||||
|
diagnostic logs:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build/bin/verstak-desktop --debug
|
./build/bin/verstak-desktop --debug
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build release packages locally
|
## Portable test artifacts
|
||||||
|
|
||||||
These commands create local artifacts in `release/`. They do not publish a GitHub Release.
|
These commands make local alpha artifacts in `release/`. They do not create a
|
||||||
|
GitHub Release.
|
||||||
### Debian package
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Debian 13 / Ubuntu 24.04 or later. APT installs WebKitGTK dependencies.
|
||||||
./scripts/package-deb.sh v0.1.0-alpha.1
|
./scripts/package-deb.sh v0.1.0-alpha.1
|
||||||
```
|
|
||||||
|
|
||||||
### AppImage
|
# Portable Linux x86_64 AppImage with bundled WebKitGTK runtime and plugins.
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/package-appimage.sh v0.1.0-alpha.1
|
./scripts/package-appimage.sh v0.1.0-alpha.1
|
||||||
```
|
```
|
||||||
|
|
||||||
### Windows portable ZIP
|
Install the Debian package with `sudo apt install ./release/verstak_*.deb`,
|
||||||
|
then launch `verstak`. Run the AppImage directly after making it executable:
|
||||||
|
|
||||||
The Windows archive can be cross-compiled on Linux using MinGW:
|
```bash
|
||||||
|
chmod +x release/verstak-linux-x86_64-v0.1.0-alpha.1.AppImage
|
||||||
|
./release/verstak-linux-x86_64-v0.1.0-alpha.1.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
On distributions without FUSE support, use
|
||||||
|
`APPIMAGE_EXTRACT_AND_RUN=1 ./release/verstak-linux-x86_64-v0.1.0-alpha.1.AppImage`.
|
||||||
|
|
||||||
|
Build the Windows portable ZIP on Linux with MinGW:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install gcc-mingw-w64-x86-64 zip
|
sudo apt install gcc-mingw-w64-x86-64 zip
|
||||||
./scripts/package-windows-portable.sh v0.1.0-alpha.1
|
./scripts/package-windows-portable.sh v0.1.0-alpha.1
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project repositories
|
The ZIP uses the x64 Evergreen Microsoft WebView2 Runtime installed in Windows
|
||||||
|
and writes `release/verstak-windows-amd64-<version>.zip`. Extract it to a local
|
||||||
|
disk (not a network share) and launch `Verstak.cmd`. Windows 11 and most
|
||||||
|
Windows 10 installations already include this runtime. If Verstak does not
|
||||||
|
start, install the official [Microsoft WebView2 Runtime x64 standalone
|
||||||
|
installer](https://go.microsoft.com/fwlink/p/?LinkId=2124701), then launch it
|
||||||
|
again.
|
||||||
|
|
||||||
| Repository | Purpose |
|
Each format is listed in `release/SHA256SUMS` after packaging.
|
||||||
| ---------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
||||||
| [verstak](https://github.com/mirivlad/verstak) | Desktop application, core platform and UI shell |
|
|
||||||
| [verstak-official-plugins](https://github.com/mirivlad/verstak-official-plugins) | Official Files, Notes, Journal, Todo and other plugins |
|
|
||||||
| [verstak-sdk](https://github.com/mirivlad/verstak-sdk) | TypeScript SDK, schemas and plugin contracts |
|
|
||||||
| [verstak-browser-extension](https://github.com/mirivlad/verstak-browser-extension) | Browser capture and optional activity integration |
|
|
||||||
| [verstak-sync-server](https://github.com/mirivlad/verstak-sync-server) | Optional self-hosted synchronization server |
|
|
||||||
| [verstak-docs](https://github.com/mirivlad/verstak-docs) | Product and platform architecture documentation |
|
|
||||||
|
|
||||||
Compatible components should be built from the same release line.
|
## First local vault
|
||||||
|
|
||||||
## Development status
|
1. Launch the desktop application and choose or create a writable vault folder.
|
||||||
|
Verstak stores its local metadata in that vault; do not point it at a
|
||||||
|
read-only directory.
|
||||||
|
2. Create a Дело (workspace) in the Files plugin before assigning captures or
|
||||||
|
activity to it.
|
||||||
|
3. Open Notes, Files, Activity and Journal as needed. Activity candidates are
|
||||||
|
only suggestions: a Journal entry and a new Дело are always created by the
|
||||||
|
user.
|
||||||
|
|
||||||
Verstak is under active development.
|
## Browser extension
|
||||||
|
|
||||||
The current alpha is intended for testing, feedback and experimentation. Backward compatibility is not guaranteed until the first stable release.
|
The extension is optional. Build it locally with `npm ci && npm test && npm run
|
||||||
|
build` in `verstak-browser-extension/`, then load `dist/chromium` as an
|
||||||
|
unpacked Chromium extension or `dist/firefox` temporarily in Firefox. A signed
|
||||||
|
Firefox XPI is available from the [extension's GitHub
|
||||||
|
Releases](https://github.com/mirivlad/verstak-browser-extension/releases).
|
||||||
|
|
||||||
Bug reports and feature discussions are welcome in
|
To connect it to the desktop application:
|
||||||
[GitHub Issues](https://github.com/mirivlad/verstak/issues).
|
|
||||||
|
1. Ensure the `browser-inbox` plugin is installed and open its settings in
|
||||||
|
Verstak.
|
||||||
|
2. Copy the displayed Receiver URL and Pairing Token.
|
||||||
|
3. Paste both values into the extension's settings and save.
|
||||||
|
4. Use a manual Send Page, selection, link or file action to create a Browser
|
||||||
|
Inbox capture.
|
||||||
|
|
||||||
|
Passive domain activity is disabled by default. When the user explicitly turns
|
||||||
|
it on, the extension sends only bounded time totals by normalized domain. It
|
||||||
|
does not send URLs, page titles, page contents, keystrokes, navigation history
|
||||||
|
or inactive-tab time. The extension settings provide a domain exclusion list.
|
||||||
|
|
||||||
|
## Optional sync server
|
||||||
|
|
||||||
|
The sync server is self-hosted and is not required for local use. Build and
|
||||||
|
start a development instance from a sibling checkout:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/mirivlad/verstak-sync-server.git verstak-sync-server
|
||||||
|
cd verstak-sync-server
|
||||||
|
./scripts/build.sh
|
||||||
|
./build/bin/verstak-sync-server --port 47732 --data ./server-data \
|
||||||
|
--admin-user admin --admin-pass 'choose-a-strong-password'
|
||||||
|
```
|
||||||
|
|
||||||
|
For a second device or a production host, follow the deployment, HTTPS and
|
||||||
|
backup guidance in the [sync server
|
||||||
|
README](https://github.com/mirivlad/verstak-sync-server#readme). In the desktop
|
||||||
|
application, open the Sync plugin, enter the server URL and user credentials,
|
||||||
|
test the connection, then select **Connect**. Each vault is paired separately.
|
||||||
|
|
||||||
|
## Separate plugin archives
|
||||||
|
|
||||||
|
The desktop artifacts already include matching official plugins. To distribute
|
||||||
|
plugins separately, build OS-specific archives in the sibling repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ../verstak-official-plugins
|
||||||
|
sudo apt install gcc-mingw-w64-x86-64 zip
|
||||||
|
./scripts/package-portable.sh v0.1.0-alpha.1
|
||||||
|
```
|
||||||
|
|
||||||
|
It writes a Linux `tar.gz` and a Windows ZIP. Both archives expand directly
|
||||||
|
into the `plugins/` directory beside the corresponding desktop executable.
|
||||||
|
Separate archives are required because `platform-test` includes a native
|
||||||
|
sidecar for its target OS.
|
||||||
|
|
||||||
|
## Publish a GitHub Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/publish-github-release.sh v0.1.0-alpha.1
|
||||||
|
```
|
||||||
|
|
||||||
|
The command requires an authenticated [`gh`](https://cli.github.com/) CLI, a
|
||||||
|
clean local `main` equal to `origin/main`, and the sibling official-plugins
|
||||||
|
checkout. It builds the Debian package, Linux AppImage and Windows portable
|
||||||
|
ZIP, then creates or updates the GitHub Release with those three artifacts and
|
||||||
|
`SHA256SUMS`. Alpha, beta and release-candidate tags are marked as GitHub
|
||||||
|
prereleases; stable tags are marked latest. The command creates an annotated
|
||||||
|
tag when needed and pushes it through `origin`, which also mirrors tags in this
|
||||||
|
checkout. Publish the compatible official-plugins release before the desktop
|
||||||
|
release that embeds those plugins.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright © 2026 Verstak contributors.
|
Copyright © 2026 Verstak contributors. Licensed under
|
||||||
|
[GNU AGPLv3 or later](LICENSE).
|
||||||
Verstak is licensed under the
|
|
||||||
[GNU Affero General Public License v3.0 or later](LICENSE).
|
|
||||||
|
|
|
||||||
340
README.ru.md
340
README.ru.md
|
|
@ -1,340 +0,0 @@
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
<img src="packaging/linux/verstak.svg" width="112" alt="Логотип Верстака">
|
|
||||||
|
|
||||||
# Верстак
|
|
||||||
|
|
||||||
### Рабочий контекст остаётся рядом — и хранится локально.
|
|
||||||
|
|
||||||
Файлы, заметки, ссылки, материалы из браузера, активность и история работы
|
|
||||||
в одном расширяемом рабочем пространстве.
|
|
||||||
|
|
||||||
[English](README.md) · **Русский**
|
|
||||||
|
|
||||||
[](https://github.com/mirivlad/verstak/releases)
|
|
||||||

|
|
||||||

|
|
||||||
[](LICENSE)
|
|
||||||
|
|
||||||
[Скачать](https://github.com/mirivlad/verstak/releases/latest) ·
|
|
||||||
[Документация](https://github.com/mirivlad/verstak-docs) ·
|
|
||||||
[Сообщить о проблеме](https://github.com/mirivlad/verstak/issues)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> Верстак пока находится на стадии **alpha**. До первого стабильного выпуска могут меняться API, форматы хранения и состав пакетов. Для знакомства используйте тестовый vault и сохраняйте резервные копии важных данных.
|
|
||||||
|
|
||||||
## Что такое Верстак
|
|
||||||
|
|
||||||
Верстак — это local-first рабочая среда, которая удерживает в одном месте контекст вокруг ваших дел.
|
|
||||||
|
|
||||||
**Делом** может быть что угодно:
|
|
||||||
|
|
||||||
* программный проект;
|
|
||||||
* клиент или заказчик;
|
|
||||||
* сервер или площадка;
|
|
||||||
* ремонтируемое устройство;
|
|
||||||
* статья или исследование;
|
|
||||||
* учебный курс;
|
|
||||||
* личный долгосрочный проект.
|
|
||||||
|
|
||||||
Обычно информация о таком деле расползается по папкам, заметкам, браузерным вкладкам, менеджерам задач, хранилищам паролей, истории терминала и памяти.
|
|
||||||
|
|
||||||
Верстак собирает её в один локальный **vault**, который остаётся под вашим контролем.
|
|
||||||
|
|
||||||
Для локальной работы не нужны учётная запись, облачный сервис или собственный сервер.
|
|
||||||
|
|
||||||
## Верстак в работе
|
|
||||||
|
|
||||||
| Обзор: возвращает к делу и показывает недавнюю работу, входящие и то, что требует внимания. | Файлы: обычные папки и документы прямо внутри дела. |
|
|
||||||
| --- | --- |
|
|
||||||
|  |  |
|
|
||||||
|
|
||||||
| Заметки: Markdown-заметки остаются рядом с делом, которое они описывают. | Журнал: фиксирует завершённую работу и её контекст. |
|
|
||||||
| --- | --- |
|
|
||||||
|  |  |
|
|
||||||
|
|
||||||
## Основные возможности
|
|
||||||
|
|
||||||
| Возможность | Что она даёт |
|
|
||||||
| ------------------------ | ---------------------------------------------------------------------------------------------- |
|
|
||||||
| **Дела** | Объединяют файлы, заметки и историю работы вокруг проектов, клиентов и других направлений. |
|
|
||||||
| **Файлы** | Позволяют работать с обычными файлами, расположенными внутри vault. |
|
|
||||||
| **Заметки** | Markdown-заметки, обзорные страницы и связи между материалами. |
|
|
||||||
| **Обзор** | Помогает быстро вернуться к недавней работе и увидеть то, что требует внимания. |
|
|
||||||
| **Входящие из браузера** | Принимают страницы, ссылки, выделенный текст и файлы из браузера. |
|
|
||||||
| **Активность и журнал** | Помогают восстановить рабочие сессии и превратить выбранную активность в записи журнала. |
|
|
||||||
| **Задачи** | Необязательные списки задач внутри отдельных дел и во всём vault. |
|
|
||||||
| **Поиск** | Ищет по заметкам, файлам и данным поддерживаемых плагинов. |
|
|
||||||
| **Корзина** | Позволяет восстановить удалённое или окончательно удалить его из одного общего раздела. |
|
|
||||||
| **Секреты** | Хранит доступы и учётные данные рядом с делом, к которому они относятся. |
|
|
||||||
| **Шаблоны** | Создают повторяемую структуру для однотипных дел. |
|
|
||||||
| **Плагины** | Позволяют добавлять, заменять и отключать инструменты, не превращая Верстак в жёсткий монолит. |
|
|
||||||
| **Синхронизация** | Необязательно синхронизирует vault между устройствами через собственный сервер. |
|
|
||||||
|
|
||||||
## Скачивание и установка
|
|
||||||
|
|
||||||
Готовые сборки находятся на странице
|
|
||||||
[GitHub Releases](https://github.com/mirivlad/verstak/releases/latest).
|
|
||||||
|
|
||||||
В релизные пакеты уже входят совместимые версии официальных плагинов. Устанавливать их отдельно не требуется.
|
|
||||||
|
|
||||||
| Система | Какой файл скачать | Как установить |
|
|
||||||
| -------------------------------- | ---------------------------------------- | -------------------------------------------- |
|
|
||||||
| Debian 13 / Ubuntu 24.04 и новее | `verstak_<версия>_amd64.deb` | Установить через APT. |
|
|
||||||
| Другие дистрибутивы Linux x86_64 | `verstak-linux-x86_64-<версия>.AppImage` | Сделать файл исполняемым и запустить. |
|
|
||||||
| Windows 10/11 x64 | `verstak-windows-amd64-<версия>.zip` | Распаковать архив и запустить `Verstak.cmd`. |
|
|
||||||
|
|
||||||
### Debian и Ubuntu
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install ./verstak_*_amd64.deb
|
|
||||||
```
|
|
||||||
|
|
||||||
После установки запустите Верстак из меню приложений или командой:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
verstak
|
|
||||||
```
|
|
||||||
|
|
||||||
### AppImage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x verstak-linux-x86_64-*.AppImage
|
|
||||||
./verstak-linux-x86_64-*.AppImage
|
|
||||||
```
|
|
||||||
|
|
||||||
Если в системе нет поддержки FUSE:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
APPIMAGE_EXTRACT_AND_RUN=1 ./verstak-linux-x86_64-*.AppImage
|
|
||||||
```
|
|
||||||
|
|
||||||
### Портативная версия для Windows
|
|
||||||
|
|
||||||
1. Скачайте `verstak-windows-amd64-<версия>.zip`.
|
|
||||||
2. Распакуйте архив в локальную папку.
|
|
||||||
3. Запустите `Verstak.cmd`.
|
|
||||||
|
|
||||||
Не запускайте Верстак прямо из ZIP-архива или с сетевого диска.
|
|
||||||
|
|
||||||
Верстак использует Microsoft WebView2 Runtime. Он уже присутствует в Windows 11 и большинстве актуальных установок Windows 10. Если приложение не запускается, установите
|
|
||||||
[Microsoft WebView2 Runtime x64](https://go.microsoft.com/fwlink/p/?LinkId=2124701).
|
|
||||||
|
|
||||||
## Фоновая работа и трей
|
|
||||||
|
|
||||||
Закрытие главного окна не завершает Верстак: приложение остаётся в системном трее. Выберите **Show Verstak**, чтобы вернуть окно, или **Quit**, чтобы полностью завершить приложение. Благодаря этому напоминания Todo продолжают работать, пока окно скрыто.
|
|
||||||
|
|
||||||
### Проверка скачанного файла
|
|
||||||
|
|
||||||
В каждый релиз входит файл `SHA256SUMS`.
|
|
||||||
|
|
||||||
Проверка в Linux:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sha256sum -c SHA256SUMS --ignore-missing
|
|
||||||
```
|
|
||||||
|
|
||||||
## Первый запуск
|
|
||||||
|
|
||||||
### 1. Создайте или откройте vault
|
|
||||||
|
|
||||||
**Vault** — это корневая папка, в которой Верстак хранит ваши дела.
|
|
||||||
|
|
||||||
Выберите доступный для записи локальный каталог. Например:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Документы/
|
|
||||||
└── Верстак/
|
|
||||||
```
|
|
||||||
|
|
||||||
Файлы и заметки остаются обычными файлами. Их можно открыть и без Верстака.
|
|
||||||
|
|
||||||
### 2. Создайте первое дело
|
|
||||||
|
|
||||||
Внутри vault создайте дело для проекта, клиента, сервера, устройства или другого направления работы.
|
|
||||||
|
|
||||||
Например:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Домашний сервер
|
|
||||||
Заказчик Альфа
|
|
||||||
Разработка Верстака
|
|
||||||
3D-принтер
|
|
||||||
Учебный курс
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Соберите контекст
|
|
||||||
|
|
||||||
Добавьте в дело всё, что поможет вернуться к нему через неделю, месяц или год:
|
|
||||||
|
|
||||||
* заметки и принятые решения;
|
|
||||||
* документы и исходные файлы;
|
|
||||||
* полезные ссылки;
|
|
||||||
* данные для доступа;
|
|
||||||
* записи журнала;
|
|
||||||
* задачи;
|
|
||||||
* материалы, отправленные из браузера.
|
|
||||||
|
|
||||||
### 4. Возвращайтесь через Обзор
|
|
||||||
|
|
||||||
Экран «Обзор» показывает недавние изменения, необработанные входящие, активность и другие подходящие точки для продолжения работы.
|
|
||||||
|
|
||||||
## Как Верстак хранит данные
|
|
||||||
|
|
||||||
Верстак придерживается нескольких принципов:
|
|
||||||
|
|
||||||
* vault хранится локально и доступен без учётной записи и интернета;
|
|
||||||
* файлы и заметки остаются читаемыми вне приложения;
|
|
||||||
* синхронизация является дополнением, а не источником истины;
|
|
||||||
* пользователь должен понимать, где находятся его данные и что с ними происходит;
|
|
||||||
* инструменты реализуются плагинами, а не навсегда встраиваются в ядро.
|
|
||||||
|
|
||||||
Настольное приложение загружает плагины из каталога `plugins/`, расположенного рядом с исполняемым файлом.
|
|
||||||
|
|
||||||
## Интеграция с браузером
|
|
||||||
|
|
||||||
Необязательное расширение позволяет отправлять во «Входящие из браузера»:
|
|
||||||
|
|
||||||
* текущую страницу;
|
|
||||||
* выделенный текст;
|
|
||||||
* ссылку;
|
|
||||||
* выбранный или скачанный файл.
|
|
||||||
|
|
||||||
Репозиторий расширения:
|
|
||||||
|
|
||||||
[mirivlad/verstak-browser-extension](https://github.com/mirivlad/verstak-browser-extension)
|
|
||||||
|
|
||||||
Чтобы подключить расширение:
|
|
||||||
|
|
||||||
1. Откройте настройки «Входящих из браузера» в Верстаке.
|
|
||||||
2. Скопируйте адрес приёмника и токен сопряжения.
|
|
||||||
3. Вставьте их в настройки расширения.
|
|
||||||
4. Сохраните настройки и отправьте тестовую страницу.
|
|
||||||
|
|
||||||
Пассивный учёт активности по доменам по умолчанию отключён. После включения расширение передаёт только ограниченные интервалы времени для нормализованных доменов. Оно не отправляет содержимое страниц, нажатия клавиш или полную историю посещений.
|
|
||||||
|
|
||||||
## Необязательная синхронизация
|
|
||||||
|
|
||||||
Для локального использования сервер не нужен.
|
|
||||||
|
|
||||||
Для синхронизации между устройствами можно развернуть собственный
|
|
||||||
[Verstak Sync Server](https://github.com/mirivlad/verstak-sync-server).
|
|
||||||
|
|
||||||
Каждый vault подключается отдельно. Основной копией данных остаётся локальный vault.
|
|
||||||
|
|
||||||
## Сборка из исходного кода
|
|
||||||
|
|
||||||
### Требования
|
|
||||||
|
|
||||||
* Go 1.24 или новее;
|
|
||||||
* Node.js 20 или новее и npm;
|
|
||||||
* Python 3;
|
|
||||||
* Git;
|
|
||||||
* зависимости для сборки Wails v2;
|
|
||||||
* пакеты разработки WebKitGTK в Linux.
|
|
||||||
* файлы разработки Ayatana AppIndicator в Linux (`sudo apt install libayatana-appindicator3-dev`).
|
|
||||||
|
|
||||||
Зависимости для конкретного дистрибутива перечислены в
|
|
||||||
[документации Wails](https://wails.io/docs/gettingstarted/installation/).
|
|
||||||
|
|
||||||
### Клонирование репозиториев
|
|
||||||
|
|
||||||
Desktop, SDK и официальные плагины должны находиться в соседних каталогах:
|
|
||||||
|
|
||||||
```text
|
|
||||||
verstak-workspace/
|
|
||||||
├── verstak/
|
|
||||||
├── verstak-sdk/
|
|
||||||
└── verstak-official-plugins/
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir verstak-workspace
|
|
||||||
cd verstak-workspace
|
|
||||||
|
|
||||||
git clone https://github.com/mirivlad/verstak.git
|
|
||||||
git clone https://github.com/mirivlad/verstak-sdk.git
|
|
||||||
git clone https://github.com/mirivlad/verstak-official-plugins.git
|
|
||||||
```
|
|
||||||
|
|
||||||
### Сборка
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd verstak-sdk
|
|
||||||
./scripts/build.sh
|
|
||||||
|
|
||||||
cd ../verstak-official-plugins
|
|
||||||
./scripts/build.sh
|
|
||||||
|
|
||||||
cd ../verstak
|
|
||||||
./scripts/install-dev-plugins.sh
|
|
||||||
./scripts/build.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Готовое приложение появится по адресу:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build/bin/verstak-desktop
|
|
||||||
```
|
|
||||||
|
|
||||||
Запуск с дополнительной диагностикой:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build/bin/verstak-desktop --debug
|
|
||||||
```
|
|
||||||
|
|
||||||
## Локальная сборка релизных пакетов
|
|
||||||
|
|
||||||
Следующие команды создают пакеты в каталоге `release/`, но не публикуют GitHub Release.
|
|
||||||
|
|
||||||
### Пакет Debian
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/package-deb.sh v0.1.0-alpha.1
|
|
||||||
```
|
|
||||||
|
|
||||||
### AppImage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/package-appimage.sh v0.1.0-alpha.1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Портативный ZIP для Windows
|
|
||||||
|
|
||||||
Windows-версию можно собрать в Linux при помощи MinGW:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install gcc-mingw-w64-x86-64 zip
|
|
||||||
./scripts/package-windows-portable.sh v0.1.0-alpha.1
|
|
||||||
```
|
|
||||||
|
|
||||||
## Репозитории проекта
|
|
||||||
|
|
||||||
| Репозиторий | Назначение |
|
|
||||||
| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
||||||
| [verstak](https://github.com/mirivlad/verstak) | Настольное приложение, ядро платформы и оболочка интерфейса |
|
|
||||||
| [verstak-official-plugins](https://github.com/mirivlad/verstak-official-plugins) | Официальные плагины файлов, заметок, журнала, задач и других инструментов |
|
|
||||||
| [verstak-sdk](https://github.com/mirivlad/verstak-sdk) | TypeScript SDK, схемы и контракты плагинов |
|
|
||||||
| [verstak-browser-extension](https://github.com/mirivlad/verstak-browser-extension) | Отправка материалов из браузера и необязательный учёт активности |
|
|
||||||
| [verstak-sync-server](https://github.com/mirivlad/verstak-sync-server) | Необязательный собственный сервер синхронизации |
|
|
||||||
| [verstak-docs](https://github.com/mirivlad/verstak-docs) | Продуктовая и архитектурная документация |
|
|
||||||
|
|
||||||
Совместимые компоненты следует собирать из одной релизной линии.
|
|
||||||
|
|
||||||
## Состояние разработки
|
|
||||||
|
|
||||||
Верстак активно развивается.
|
|
||||||
|
|
||||||
Текущая alpha предназначена для тестирования, обратной связи и экспериментов. До первого стабильного выпуска обратная совместимость не гарантируется.
|
|
||||||
|
|
||||||
Сообщения об ошибках и предложения принимаются в
|
|
||||||
[GitHub Issues](https://github.com/mirivlad/verstak/issues).
|
|
||||||
|
|
||||||
## Лицензия
|
|
||||||
|
|
||||||
Copyright © 2026 Verstak contributors.
|
|
||||||
|
|
||||||
Верстак распространяется на условиях
|
|
||||||
[GNU Affero General Public License v3.0 или новее](LICENSE).
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 118 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
|
|
@ -1,341 +0,0 @@
|
||||||
# Native Notifications, Tray, and Public README Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Deliver native Todo reminders and Windows/Linux tray operation, then publish updated alpha artifacts and public README documentation with real application screenshots.
|
|
||||||
|
|
||||||
**Architecture:** Desktop core stores and delivers plugin-owned notification schedules and controls the tray lifecycle. Todo uses the public plugin API to replace its desired reminders. A small core tray adapter keeps the Wails process alive after window close. Documentation uses the supplied English and Russian README sources plus screenshots from an actual test vault.
|
|
||||||
|
|
||||||
**Tech Stack:** Go 1.24, Wails v2.12 runtime notifications, `getlantern/systray`, plain JavaScript plugin API, Node smoke tests, Playwright, bash packaging.
|
|
||||||
|
|
||||||
## Global constraints
|
|
||||||
|
|
||||||
- Support Windows and Linux only; no background daemon after an explicit Quit.
|
|
||||||
- Closing a window hides it; only tray **Quit** ends the process.
|
|
||||||
- `verstak.todo` requires `verstak/core/notifications/v1` and `notifications.schedule`.
|
|
||||||
- Plugins cannot receive Wails runtime access.
|
|
||||||
- Keep Wails generated bindings out of commits.
|
|
||||||
- Commit each independent change and immediately push it to GitHub and `mirror`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Persisted notification scheduler
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `internal/core/notifications/manager.go`
|
|
||||||
- Create: `internal/core/notifications/store.go`
|
|
||||||
- Create: `internal/core/notifications/manager_test.go`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- `type Request struct { ID, DueAt, Title, Body string }`
|
|
||||||
- `type Item struct { PluginID, ID, DueAt, Title, Body, SentForDueAt string }`
|
|
||||||
- `type Sender interface { Send(context.Context, Item) error }`
|
|
||||||
- `Manager.Replace(pluginID string, requests []Request) error`, `Clear(pluginID string) error`, `Start(context.Context)`, and `Stop()`.
|
|
||||||
- Persistent state: `<vault>/.verstak/notifications/schedules.json`.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add failing scheduler tests**
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestTickRetriesFailedSendAndAcknowledgesOneDelivery(t *testing.T) {
|
|
||||||
sender := &fakeSender{err: errors.New("unavailable")}
|
|
||||||
m := newTestManager(t, sender, time.Date(2026, 7, 14, 12, 0, 0, 0, time.UTC))
|
|
||||||
requireNoError(t, m.Replace("verstak.todo", []Request{{ID: "todo-1", DueAt: "2026-07-14T11:00:00Z", Title: "Reminder"}}))
|
|
||||||
m.Tick(context.Background())
|
|
||||||
if sender.calls != 1 || m.Items()[0].SentForDueAt != "" { t.Fatal("failed send was acknowledged") }
|
|
||||||
sender.err = nil
|
|
||||||
m.Tick(context.Background())
|
|
||||||
m.Tick(context.Background())
|
|
||||||
if sender.calls != 2 || m.Items()[0].SentForDueAt != "2026-07-14T11:00:00Z" { t.Fatal("delivery was not exactly once") }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Also test stale records are removed by `Replace`, changing `DueAt` rearms a record, state reloads from disk, and a past-due unsent item is sent once.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Confirm red**
|
|
||||||
|
|
||||||
Run: `GOCACHE=/tmp/verstak-go-cache go test ./internal/core/notifications -count=1`
|
|
||||||
|
|
||||||
Expected: fails because the package does not exist.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implement the minimal manager**
|
|
||||||
|
|
||||||
Use one mutex, injected clock/sender, an atomic same-directory temp-file rename, and a 30-second ticker. Preserve `SentForDueAt` only if the requested due time is unchanged. Validate non-empty unique IDs, RFC3339 UTC dates, at most 500 requests, and bounded text. Mark sent only after `Sender.Send` succeeds.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Verify and commit**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
gofmt -w internal/core/notifications/*.go
|
|
||||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/notifications ./internal/core/... -count=1
|
|
||||||
git diff --check
|
|
||||||
```
|
|
||||||
|
|
||||||
Commit/push:
|
|
||||||
```bash
|
|
||||||
git add internal/core/notifications
|
|
||||||
git commit -m "feat: add persisted notification scheduler"
|
|
||||||
git push https://github.com/mirivlad/verstak.git main
|
|
||||||
git push mirror main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 2: Permission-gated core/plugin notification API
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `main.go`
|
|
||||||
- Modify: `internal/api/app.go`
|
|
||||||
- Modify: `internal/api/app_test.go`
|
|
||||||
- Modify: `internal/core/permissions/registry.go`
|
|
||||||
- Modify: `frontend/src/lib/plugin-host/VerstakPluginAPI.js`
|
|
||||||
- Modify: `frontend/tests/plugin-api-contributions-test.mjs`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Core capability: `verstak/core/notifications/v1`.
|
|
||||||
- Permission: `notifications.schedule`, non-dangerous.
|
|
||||||
- Bound methods: `ReplacePluginNotifications(pluginID string, items []notifications.Request) string` and `ClearPluginNotifications(pluginID string) string`.
|
|
||||||
- Plugin methods: `api.notifications.replace(items)` and `api.notifications.clear()`.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add failing backend and host-API tests**
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestReplacePluginNotificationsRequiresCapabilityAndPermission(t *testing.T) {
|
|
||||||
app, scheduler := newNotificationTestApp(t, manifestWithoutNotificationPermission)
|
|
||||||
if got := app.ReplacePluginNotifications("example", []notifications.Request{{ID: "r", DueAt: "2026-07-14T10:00:00Z", Title: "R"}}); got == "" {
|
|
||||||
t.Fatal("missing permission was accepted")
|
|
||||||
}
|
|
||||||
if scheduler.replaceCalls != 0 { t.Fatalf("replace calls = %d", scheduler.replaceCalls) }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The JS test mocks `App.ReplacePluginNotifications`, calls `api.notifications.replace`, and asserts both plugin ID forwarding and a namespaced rejection for a non-empty backend error.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Confirm red**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run TestReplacePluginNotificationsRequiresCapabilityAndPermission -count=1
|
|
||||||
node frontend/tests/plugin-api-contributions-test.mjs
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: missing API methods.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implement guarded bridge**
|
|
||||||
|
|
||||||
Register the capability in `main.go`, construct the scheduler, and guard bound calls with both existing access helpers before forwarding. Add `notifications.schedule` to the registry. Add the public host methods using `callBackendErrorString`; do not expose `window.runtime` to plugins.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Verify and commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gofmt -w main.go internal/api/app.go internal/api/app_test.go internal/core/permissions/registry.go
|
|
||||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/api -count=1
|
|
||||||
node frontend/tests/plugin-api-contributions-test.mjs
|
|
||||||
git diff --check
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add main.go internal/api/app.go internal/api/app_test.go internal/core/permissions/registry.go frontend/src/lib/plugin-host/VerstakPluginAPI.js frontend/tests/plugin-api-contributions-test.mjs
|
|
||||||
git commit -m "feat: expose scheduled notifications to plugins"
|
|
||||||
git push https://github.com/mirivlad/verstak.git main
|
|
||||||
git push mirror main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 3: Todo reminder synchronization and Wails lifecycle
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `main.go`
|
|
||||||
- Modify: `internal/api/app.go`
|
|
||||||
- Modify: `internal/api/app_test.go`
|
|
||||||
- Modify: `../verstak-official-plugins/plugins/todo/plugin.json`
|
|
||||||
- Modify: `../verstak-official-plugins/plugins/todo/frontend/src/index.js`
|
|
||||||
- Modify: `../verstak-official-plugins/plugins/todo/locales/en.json`
|
|
||||||
- Modify: `../verstak-official-plugins/plugins/todo/locales/ru.json`
|
|
||||||
- Modify: `../verstak-official-plugins/scripts/smoke-todo-plugin.js`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- `App.DomReady(ctx)` initializes Wails notifications before starting the scheduler.
|
|
||||||
- `App.Shutdown(ctx)` stops it before Wails cleanup.
|
|
||||||
- Todo replaces the complete reminder list only after Todo persistence succeeds and after Todo data loads.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add failing lifecycle and Todo smoke tests**
|
|
||||||
|
|
||||||
The App test uses function variables for Wails calls and proves initialize → start and stop → cleanup order. The Todo smoke mock records `notifications.replace`; it asserts one open reminder request with the Todo ID and a later empty replacement after completion or deletion.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Confirm red**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run 'TestDomReadyInitializesNotificationsBeforeScheduler|TestShutdownStopsNotifications' -count=1
|
|
||||||
cd ../verstak-official-plugins && node scripts/smoke-todo-plugin.js
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: lifecycle methods and schedule calls are absent.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implement lifecycle and Todo desired state**
|
|
||||||
|
|
||||||
Use `runtime.InitializeNotifications`, `runtime.SendNotification`, and `runtime.CleanupNotifications` only in core. Register `OnDomReady`/ `OnShutdown` in Wails options. Todo maps open valid `reminderAt` values to ISO UTC using `new Date(todo.reminderAt).toISOString()`, localizes title/body, and calls `api.notifications.replace`. A scheduling failure reports status but never rolls back already-saved Todo data.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Verify and commit both repositories**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/api -count=1
|
|
||||||
cd ../verstak-official-plugins && node scripts/smoke-todo-plugin.js && ./scripts/check.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ../verstak-desktop
|
|
||||||
git add main.go internal/api/app.go internal/api/app_test.go
|
|
||||||
git commit -m "feat: start native notifications with desktop app"
|
|
||||||
git push https://github.com/mirivlad/verstak.git main
|
|
||||||
git push mirror main
|
|
||||||
cd ../verstak-official-plugins
|
|
||||||
git add plugins/todo scripts/smoke-todo-plugin.js
|
|
||||||
git commit -m "feat: schedule native Todo reminders"
|
|
||||||
git push https://github.com/mirivlad/verstak-official-plugins.git main
|
|
||||||
git push mirror main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 4: Windows/Linux tray, close policy, and single instance
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `internal/shell/tray/controller.go`
|
|
||||||
- Create: `internal/shell/tray/controller_test.go`
|
|
||||||
- Modify: `main.go`
|
|
||||||
- Modify: `internal/api/app.go`
|
|
||||||
- Modify: `internal/api/app_test.go`
|
|
||||||
- Modify: `go.mod`
|
|
||||||
- Modify: `go.sum`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- `tray.Start(icon []byte, actions Actions) error`, with `Actions.Show` and `Actions.Quit`.
|
|
||||||
- `App.BeforeClose(ctx) bool` hides and returns true unless `App.Quit()` made shutdown explicit.
|
|
||||||
- `App.ShowWindow()` reveals the existing window.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add failing close-policy and tray action tests**
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestBeforeCloseHidesWindowUntilExplicitQuit(t *testing.T) {
|
|
||||||
app, window := newWindowTestApp(t)
|
|
||||||
if prevent := app.BeforeClose(context.Background()); !prevent || window.hideCalls != 1 { t.Fatal("ordinary close must hide") }
|
|
||||||
app.Quit()
|
|
||||||
if prevent := app.BeforeClose(context.Background()); prevent { t.Fatal("explicit quit was prevented") }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Use a fake tray adapter to assert exactly two labels, **Show Verstak** and **Quit**, and one callback invocation per click.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Confirm red**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run TestBeforeCloseHidesWindowUntilExplicitQuit -count=1
|
|
||||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/shell/tray -count=1
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: absent packages/methods.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implement adapter and Wails wiring**
|
|
||||||
|
|
||||||
Add `github.com/getlantern/systray@v1.2.2`. The production adapter calls nonblocking `systray.Register`, sets compact source-controlled PNG bytes derived from the existing tracked logo, and starts goroutines for the two click channels. `main.go` registers `OnBeforeClose` and uses `options.SingleInstanceLock` whose second launch calls `app.ShowWindow`. Do not embed ignored Wails-generated files from `build/`.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Verify and commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gofmt -w main.go internal/api/app.go internal/api/app_test.go internal/shell/tray/*.go
|
|
||||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/shell/tray ./internal/api -count=1
|
|
||||||
./scripts/build.sh
|
|
||||||
ldd build/bin/verstak-desktop | grep -E 'ayatana-appindicator|appindicator'
|
|
||||||
git diff --check
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add main.go internal/api/app.go internal/api/app_test.go internal/shell/tray go.mod go.sum
|
|
||||||
git commit -m "feat: keep desktop app in system tray"
|
|
||||||
git push https://github.com/mirivlad/verstak.git main
|
|
||||||
git push mirror main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 5: Packaging and public README screenshots
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `packaging/deb/control`
|
|
||||||
- Modify: `scripts/build.sh`
|
|
||||||
- Modify: `scripts/package-appimage.sh`
|
|
||||||
- Modify: `scripts/test-package-formats.sh`
|
|
||||||
- Modify: `README.md`
|
|
||||||
- Create: `README.ru.md`
|
|
||||||
- Create: `docs/screenshots/overview.png`
|
|
||||||
- Create: `docs/screenshots/workspace-files-notes.png`
|
|
||||||
- Create: `docs/screenshots/activity-journal.png`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add failing package-contract checks**
|
|
||||||
|
|
||||||
Add assertions for `libayatana-appindicator3-dev` in the Linux build guidance, `libayatana-appindicator3-1` in Debian dependencies, and `ayatana-appindicator` in the AppImage packing verification.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Confirm red**
|
|
||||||
|
|
||||||
Run: `./scripts/test-package-formats.sh`
|
|
||||||
|
|
||||||
Expected: the first tray dependency assertion fails.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implement portable package support**
|
|
||||||
|
|
||||||
Declare the Debian runtime dependency. Make the Linux build error name the appindicator development header. Require the dynamically discovered appindicator library to appear in AppDir after the existing `ldd` traversal. Keep the Windows system-WebView2 policy unchanged.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Install the supplied public README sources**
|
|
||||||
|
|
||||||
Replace this repository's `README.md` with the supplied English source at the workspace root and add the supplied Russian source as `README.ru.md`. Add an image strip after “What is Verstak?” / “Что такое Верстак?” that links to three tracked PNG screenshots and uses matching localized alt text.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Produce real screenshots**
|
|
||||||
|
|
||||||
Build and run the desktop app against `/home/mirivlad/Nextcloud/Verstak/VerstakVault` on the active X display. Capture:
|
|
||||||
1. Overview showing recent work and entry points;
|
|
||||||
2. a populated workspace Files/Notes view;
|
|
||||||
3. Activity with a suggested session and its Journal review.
|
|
||||||
|
|
||||||
Use `scrot` or ImageMagick `import`, crop only surrounding desktop chrome, inspect every PNG visually, and do not add generated mockups. Remove all non-user-safe text from the test vault before capture.
|
|
||||||
|
|
||||||
- [ ] **Step 6: Verify documentation and package contracts, commit, push**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/test-package-formats.sh
|
|
||||||
git diff --check
|
|
||||||
test -s docs/screenshots/overview.png
|
|
||||||
test -s docs/screenshots/workspace-files-notes.png
|
|
||||||
test -s docs/screenshots/activity-journal.png
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add packaging/deb/control scripts/build.sh scripts/package-appimage.sh scripts/test-package-formats.sh README.md README.ru.md docs/screenshots
|
|
||||||
git commit -m "docs: add public README and product screenshots"
|
|
||||||
git push https://github.com/mirivlad/verstak.git main
|
|
||||||
git push mirror main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 6: Complete verification and GitHub alpha releases
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- No source changes expected unless verification exposes a defect.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Run all checks**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
GOCACHE=/tmp/verstak-go-cache ./scripts/test.sh
|
|
||||||
./scripts/check.sh
|
|
||||||
./scripts/test-package-formats.sh
|
|
||||||
./scripts/test-build-windows.sh
|
|
||||||
cd ../verstak-official-plugins && ./scripts/check.sh && ./scripts/test-package-portable.sh && ./scripts/test-publish-github-release.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Build and checksum both alpha releases**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ../verstak-official-plugins && ./scripts/release.sh v0.1.0-alpha.2 && (cd release && sha256sum --check SHA256SUMS)
|
|
||||||
cd ../verstak-desktop && ./scripts/release.sh v0.1.0-alpha.2 && (cd release && sha256sum --check SHA256SUMS)
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Publish and inspect releases**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ../verstak-official-plugins && ./scripts/publish-github-release.sh v0.1.0-alpha.2
|
|
||||||
cd ../verstak-desktop && ./scripts/publish-github-release.sh v0.1.0-alpha.2
|
|
||||||
gh release view v0.1.0-alpha.2 -R mirivlad/verstak-official-plugins
|
|
||||||
gh release view v0.1.0-alpha.2 -R mirivlad/verstak
|
|
||||||
```
|
|
||||||
|
|
||||||
The final report distinguishes automated verification from the two required real desktop manual checks: tray close/show/quit and a near-future Todo notification while the window is hidden.
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
# Native Notifications and System Tray Design
|
|
||||||
|
|
||||||
**Status:** approved for implementation on 2026-07-14
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Deliver Todo reminders as native notifications on Windows and Linux, and keep
|
|
||||||
Verstak running in the system tray when its main window is closed. The feature
|
|
||||||
must work in the portable Windows archive, Debian package, and AppImage.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
- The desktop core owns notification delivery, scheduling, and tray lifetime.
|
|
||||||
- `verstak.todo` owns the Todo-specific reminder policy and text.
|
|
||||||
- No new official notifications plugin is introduced. A dynamic plugin cannot
|
|
||||||
call Wails directly, so it cannot be the native-notification transport.
|
|
||||||
- macOS is out of scope for this alpha. The interfaces remain platform-neutral
|
|
||||||
where that costs nothing.
|
|
||||||
|
|
||||||
## Tray behavior
|
|
||||||
|
|
||||||
On Windows and Linux, a tray icon is registered before the Wails event loop
|
|
||||||
starts. Its menu contains exactly two actions:
|
|
||||||
|
|
||||||
1. **Show Verstak** — shows and focuses the existing main window.
|
|
||||||
2. **Quit** — exits the process deliberately.
|
|
||||||
|
|
||||||
Closing the main window with its window-manager close control hides the window
|
|
||||||
and keeps the process, plugins, local browser receiver, and reminder scheduler
|
|
||||||
alive. It does not terminate the application. The quit action temporarily
|
|
||||||
allows the close lifecycle to finish and then exits normally.
|
|
||||||
|
|
||||||
The app has a single-instance lock. If a user launches the executable while an
|
|
||||||
instance is hidden in the tray, the existing instance shows its window instead
|
|
||||||
of creating a second process.
|
|
||||||
|
|
||||||
The implementation uses `github.com/getlantern/systray` through a small
|
|
||||||
`internal/shell/tray` adapter. It uses `Register`, rather than its blocking
|
|
||||||
`Run`, so Wails remains the owner of the GUI event loop. A compact PNG derived
|
|
||||||
from the tracked project logo is encoded in the tray package, so a clean build
|
|
||||||
does not depend on ignored Wails-generated icon files. The library accepts PNG
|
|
||||||
icon bytes on both target platforms.
|
|
||||||
|
|
||||||
## Notification capability and permission
|
|
||||||
|
|
||||||
The core registers the capability:
|
|
||||||
|
|
||||||
```
|
|
||||||
verstak/core/notifications/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
Plugins that use it must both require that capability and declare the
|
|
||||||
`notifications.schedule` permission. The plugin-host API exposes only two
|
|
||||||
operations within the calling plugin namespace:
|
|
||||||
|
|
||||||
```
|
|
||||||
api.notifications.replace(items)
|
|
||||||
api.notifications.clear()
|
|
||||||
```
|
|
||||||
|
|
||||||
`replace` is an atomic desired-state replacement, not an append operation. An
|
|
||||||
item contains a plugin-local stable `id`, an ISO-8601 UTC `dueAt`, a title, and
|
|
||||||
a body. The core supplies the plugin ID and rejects calls from disabled,
|
|
||||||
missing-permission, or undeclared-capability plugins. It also validates empty
|
|
||||||
IDs, duplicate IDs, invalid timestamps, and unsafe oversized text.
|
|
||||||
|
|
||||||
No plugin can send arbitrary immediate native notifications or address another
|
|
||||||
plugin's schedules in this alpha.
|
|
||||||
|
|
||||||
## Scheduler and persistence
|
|
||||||
|
|
||||||
`internal/core/notifications` persists one canonical schedule file at:
|
|
||||||
|
|
||||||
```
|
|
||||||
<vault>/.verstak/notifications/schedules.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Each record contains `{pluginId, id, dueAt, title, body, sentForDueAt}`.
|
|
||||||
The composite `(pluginId, id)` is unique. Replacing an item with the same due
|
|
||||||
time preserves `sentForDueAt`; changing `dueAt` clears it. Replacing a plugin's
|
|
||||||
list removes its stale records. This provides deterministic cancellation for
|
|
||||||
completed, deleted, and rescheduled Todos.
|
|
||||||
|
|
||||||
The manager starts after Wails reaches `OnDomReady`, initializes Wails native
|
|
||||||
notifications, and evaluates the persisted schedule immediately and then at
|
|
||||||
least every 30 seconds. A sender is injected behind an interface for unit
|
|
||||||
tests. After a successful delivery, the manager atomically records
|
|
||||||
`sentForDueAt`. A delivery error leaves the schedule pending and is logged for
|
|
||||||
a later retry.
|
|
||||||
|
|
||||||
An expired record that has not been sent is delivered once after the next app
|
|
||||||
start. A record already sent for its current due time is never sent again.
|
|
||||||
Completely quitting Verstak stops the scheduler: no separate daemon or OS
|
|
||||||
background service is added. Hiding the window in the tray does **not** stop it.
|
|
||||||
|
|
||||||
The core calls `CleanupNotifications` during shutdown, including on Linux where
|
|
||||||
it releases the D-Bus connection.
|
|
||||||
|
|
||||||
## Todo behavior
|
|
||||||
|
|
||||||
`verstak.todo` adds the core notifications capability to `requires` and adds
|
|
||||||
the `notifications.schedule` permission. After every successful Todo storage
|
|
||||||
write, it derives the complete desired reminder list:
|
|
||||||
|
|
||||||
- include only open Todos with a valid `reminderAt`;
|
|
||||||
- use the Todo ID as the stable notification ID;
|
|
||||||
- convert local `datetime-local` input to an ISO-8601 UTC instant;
|
|
||||||
- use the Todo title in the notification body and locale-aware reminder text;
|
|
||||||
- call `api.notifications.replace` with the full list.
|
|
||||||
|
|
||||||
The same replacement runs after loading persisted Todos, so a transient
|
|
||||||
schedule-write failure repairs itself next time the Todo view is opened. A
|
|
||||||
schedule API failure does not roll back Todo data; the UI reports the failure
|
|
||||||
instead. The existing in-view overdue/reminder badge remains useful context and
|
|
||||||
is not removed.
|
|
||||||
|
|
||||||
## Packaging
|
|
||||||
|
|
||||||
`getlantern/systray` requires CGO. The Windows release build already uses
|
|
||||||
`x86_64-w64-mingw32-gcc`; the Windows packaging tests must compile it with the
|
|
||||||
tray dependency included. Linux build instructions add
|
|
||||||
`libayatana-appindicator3-dev`. The Debian package declares the corresponding
|
|
||||||
runtime dependency `libayatana-appindicator3-1`.
|
|
||||||
|
|
||||||
The existing AppImage packager traverses `ldd` for the desktop executable and
|
|
||||||
copies non-glibc runtime libraries. Its verification is extended to prove that
|
|
||||||
the appindicator library is present in the AppDir when the tray implementation
|
|
||||||
is compiled in.
|
|
||||||
|
|
||||||
## Public README and product screenshots
|
|
||||||
|
|
||||||
The workspace-root `README.md` and `README.ru.md` supplied by the maintainer
|
|
||||||
become the public repository documents: the English source replaces this
|
|
||||||
repository's `README.md`, and the Russian source is committed as
|
|
||||||
`README.ru.md`. Their existing language links and public-release instructions
|
|
||||||
are retained.
|
|
||||||
|
|
||||||
Three screenshots are captured from the real desktop application using the
|
|
||||||
test vault, then committed under `docs/screenshots/`:
|
|
||||||
|
|
||||||
1. `overview.png` — returning to recent work and useful next actions;
|
|
||||||
2. `workspace-files-notes.png` — ordinary vault files and Markdown notes in a
|
|
||||||
workspace;
|
|
||||||
3. `activity-journal.png` — review of a factual activity session as a Journal
|
|
||||||
entry.
|
|
||||||
|
|
||||||
Both README variants include the same three images with localized alt text.
|
|
||||||
They are factual UI captures, not generated illustrative mockups. Capture data
|
|
||||||
must be limited to the disposable test vault and inspected before commit so no
|
|
||||||
credentials or personal content are published.
|
|
||||||
|
|
||||||
## Test and manual verification
|
|
||||||
|
|
||||||
Automated tests cover:
|
|
||||||
|
|
||||||
- schedule replacement, cancellation, rescheduling, persistence, one-time
|
|
||||||
overdue delivery, failed-send retry, and permission/capability rejection;
|
|
||||||
- Todo desired-list derivation and calls after create/edit/status/delete;
|
|
||||||
- close policy: ordinary close hides, explicit quit permits shutdown;
|
|
||||||
- tray controller action wiring and second-instance window reveal;
|
|
||||||
- Linux/Windows build scripts and package dependency expectations.
|
|
||||||
|
|
||||||
Manual smoke tests are required because neither unit tests nor Playwright can
|
|
||||||
assert a real desktop notification area or OS toast:
|
|
||||||
|
|
||||||
1. On Linux and Windows, start Verstak, close its window, use the tray menu to
|
|
||||||
reveal it, and use **Quit** to terminate it.
|
|
||||||
2. Set a Todo reminder for a near future time, hide the window in the tray, and
|
|
||||||
observe one native notification.
|
|
||||||
3. Quit before a future reminder, relaunch after it expires, and observe one
|
|
||||||
overdue notification with no duplicate on the next scheduler scan.
|
|
||||||
4. Inspect all three README screenshots at their committed size and confirm
|
|
||||||
that they show only intended test-vault data and explain the feature named
|
|
||||||
in their localized captions.
|
|
||||||
|
|
@ -219,7 +219,7 @@
|
||||||
const settings = await App.GetAppSettings();
|
const settings = await App.GetAppSettings();
|
||||||
debug.log('[App] checkVault: GetAppSettings returned', settings);
|
debug.log('[App] checkVault: GetAppSettings returned', settings);
|
||||||
flog('checkVault: GetAppSettings returned');
|
flog('checkVault: GetAppSettings returned');
|
||||||
if (settings?.debug) debug.enable({ persist: false });
|
if (settings?.debug) debug.enable();
|
||||||
|
|
||||||
debug.log('[App] checkVault: calling GetVaultStatus...');
|
debug.log('[App] checkVault: calling GetVaultStatus...');
|
||||||
vaultStatus = await App.GetVaultStatus() || { status: 'unknown', path: '', vaultId: '' };
|
vaultStatus = await App.GetVaultStatus() || { status: 'unknown', path: '', vaultId: '' };
|
||||||
|
|
|
||||||
|
|
@ -127,11 +127,9 @@ export var debug = {
|
||||||
log: log,
|
log: log,
|
||||||
logf: logf,
|
logf: logf,
|
||||||
isEnabled: function () { return ENABLED; },
|
isEnabled: function () { return ENABLED; },
|
||||||
enable: function (options) {
|
enable: function () {
|
||||||
ENABLED = true;
|
ENABLED = true;
|
||||||
if (!options || options.persist !== false) {
|
try { localStorage.setItem('verstak-debug', 'true'); } catch (e) {}
|
||||||
try { localStorage.setItem('verstak-debug', 'true'); } catch (e) {}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
disable: function () {
|
disable: function () {
|
||||||
ENABLED = false;
|
ENABLED = false;
|
||||||
|
|
|
||||||
|
|
@ -220,24 +220,6 @@ export function createPluginAPI(pluginId) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
notifications: {
|
|
||||||
replace: function(items) {
|
|
||||||
assertActive('notifications.replace');
|
|
||||||
if (!Array.isArray(items)) {
|
|
||||||
throw new Error('notifications.replace requires an array');
|
|
||||||
}
|
|
||||||
return callBackendErrorString(pluginId, 'notifications.replace', function() {
|
|
||||||
return App.ReplacePluginNotifications(pluginId, items);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
clear: function() {
|
|
||||||
assertActive('notifications.clear');
|
|
||||||
return callBackendErrorString(pluginId, 'notifications.clear', function() {
|
|
||||||
return App.ClearPluginNotifications(pluginId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
settings: {
|
settings: {
|
||||||
read: async function(key) {
|
read: async function(key) {
|
||||||
assertActive('settings.read');
|
assertActive('settings.read');
|
||||||
|
|
|
||||||
|
|
@ -378,8 +378,6 @@
|
||||||
.workspace-create-header h2 { margin: 0; font-size: 1rem; }
|
.workspace-create-header h2 { margin: 0; font-size: 1rem; }
|
||||||
.workspace-create-field { display: grid; gap: 0.35rem; color: var(--vt-color-text-muted); font-size: 0.75rem; }
|
.workspace-create-field { display: grid; gap: 0.35rem; color: var(--vt-color-text-muted); font-size: 0.75rem; }
|
||||||
.workspace-create-field input, .workspace-create-field select { width: 100%; min-height: 2rem; box-sizing: border-box; border: 1px solid var(--vt-color-border-strong); border-radius: var(--vt-radius-sm); background: #0f1424; color: var(--vt-color-text-primary); padding: 0.35rem 0.5rem; font: inherit; font-size: 0.84rem; }
|
.workspace-create-field input, .workspace-create-field select { width: 100%; min-height: 2rem; box-sizing: border-box; border: 1px solid var(--vt-color-border-strong); border-radius: var(--vt-radius-sm); background: #0f1424; color: var(--vt-color-text-primary); padding: 0.35rem 0.5rem; font: inherit; font-size: 0.84rem; }
|
||||||
.workspace-create-field select { appearance: none; background-color: #0f1424; background-image: linear-gradient(45deg, transparent 50%, var(--vt-color-text-muted) 50%), linear-gradient(135deg, var(--vt-color-text-muted) 50%, transparent 50%); background-position: calc(100% - 14px) 50%, calc(100% - 9px) 50%; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; padding-right: 1.7rem; }
|
|
||||||
.workspace-create-field select option { background: #0f1424; color: var(--vt-color-text-primary); }
|
|
||||||
.workspace-create-field input:focus, .workspace-create-field select:focus { outline: none; border-color: var(--vt-color-accent); box-shadow: var(--vt-focus-ring); }
|
.workspace-create-field input:focus, .workspace-create-field select:focus { outline: none; border-color: var(--vt-color-accent); box-shadow: var(--vt-focus-ring); }
|
||||||
.workspace-template-summary { display: grid; gap: 0.55rem; padding: 0.75rem; border: 1px solid var(--vt-color-border); border-radius: var(--vt-radius-md); background: var(--vt-color-surface-muted); }
|
.workspace-template-summary { display: grid; gap: 0.55rem; padding: 0.75rem; border: 1px solid var(--vt-color-border); border-radius: var(--vt-radius-md); background: var(--vt-color-surface-muted); }
|
||||||
.workspace-template-summary p { margin: 0; color: var(--vt-color-text-secondary); font-size: 0.8rem; line-height: 1.45; }
|
.workspace-template-summary p { margin: 0; color: var(--vt-color-text-secondary); font-size: 0.8rem; line-height: 1.45; }
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import vm from 'node:vm';
|
|
||||||
|
|
||||||
const source = fs.readFileSync(path.resolve('frontend/src/lib/log/debug.js'), 'utf8');
|
|
||||||
const values = new Map();
|
|
||||||
const localStorage = {
|
|
||||||
getItem(key) { return values.has(key) ? values.get(key) : null; },
|
|
||||||
setItem(key, value) { values.set(key, String(value)); },
|
|
||||||
removeItem(key) { values.delete(key); },
|
|
||||||
};
|
|
||||||
const context = vm.createContext({
|
|
||||||
console: { log() {} },
|
|
||||||
localStorage,
|
|
||||||
window: { location: { search: '' } },
|
|
||||||
});
|
|
||||||
const module = new vm.SourceTextModule(source, { context, identifier: 'debug.js' });
|
|
||||||
await module.link(() => { throw new Error('debug.js must not import modules'); });
|
|
||||||
await module.evaluate();
|
|
||||||
|
|
||||||
const { debug } = module.namespace;
|
|
||||||
debug.enable({ persist: false });
|
|
||||||
|
|
||||||
assert.equal(debug.isEnabled(), true, 'session debug should enable diagnostics');
|
|
||||||
assert.equal(
|
|
||||||
localStorage.getItem('verstak-debug'),
|
|
||||||
null,
|
|
||||||
'a --debug session must not make diagnostics persist into ordinary launches',
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('debug mode smoke passed');
|
|
||||||
|
|
@ -3,7 +3,6 @@ import path from 'node:path';
|
||||||
import { pathToFileURL } from 'node:url';
|
import { pathToFileURL } from 'node:url';
|
||||||
|
|
||||||
const pluginData = {};
|
const pluginData = {};
|
||||||
const scheduledNotifications = [];
|
|
||||||
|
|
||||||
globalThis.window = {
|
globalThis.window = {
|
||||||
__VERSTAK_PLUGIN_REGISTRY__: {},
|
__VERSTAK_PLUGIN_REGISTRY__: {},
|
||||||
|
|
@ -39,14 +38,6 @@ globalThis.window = {
|
||||||
pluginData[pluginId][name] = Object.assign({}, data || {});
|
pluginData[pluginId][name] = Object.assign({}, data || {});
|
||||||
return Promise.resolve('');
|
return Promise.resolve('');
|
||||||
},
|
},
|
||||||
ReplacePluginNotifications: (pluginId, items) => {
|
|
||||||
scheduledNotifications.push({ pluginId, items });
|
|
||||||
return Promise.resolve(items[0]?.id === 'rejected' ? 'notification permission denied' : '');
|
|
||||||
},
|
|
||||||
ClearPluginNotifications: (pluginId) => {
|
|
||||||
scheduledNotifications.push({ pluginId, clear: true });
|
|
||||||
return Promise.resolve('');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -125,28 +116,6 @@ if (stored.version !== 1 || stored.workspaceRootPath !== 'Project') {
|
||||||
throw new Error(`unexpected storage data: ${JSON.stringify(stored)}`);
|
throw new Error(`unexpected storage data: ${JSON.stringify(stored)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!api.notifications || typeof api.notifications.replace !== 'function' || typeof api.notifications.clear !== 'function') {
|
|
||||||
throw new Error('api.notifications replace/clear contract is missing');
|
|
||||||
}
|
|
||||||
await api.notifications.replace([{ id: 'reminder-1', dueAt: '2026-07-14T10:00:00Z', title: 'Reminder' }]);
|
|
||||||
await api.notifications.clear();
|
|
||||||
if (scheduledNotifications.length !== 2
|
|
||||||
|| scheduledNotifications[0].pluginId !== 'verstak.files'
|
|
||||||
|| scheduledNotifications[0].items[0].id !== 'reminder-1'
|
|
||||||
|| scheduledNotifications[1].pluginId !== 'verstak.files'
|
|
||||||
|| !scheduledNotifications[1].clear) {
|
|
||||||
throw new Error(`unexpected notification calls: ${JSON.stringify(scheduledNotifications)}`);
|
|
||||||
}
|
|
||||||
let rejected = false;
|
|
||||||
try {
|
|
||||||
await api.notifications.replace([{ id: 'rejected', dueAt: '2026-07-14T10:00:00Z', title: 'Reminder' }]);
|
|
||||||
} catch (err) {
|
|
||||||
rejected = String(err.message || err).includes('[plugin:verstak.files] notifications.replace failed: notification permission denied');
|
|
||||||
}
|
|
||||||
if (!rejected) {
|
|
||||||
throw new Error('notification backend errors must be plugin-scoped rejections');
|
|
||||||
}
|
|
||||||
|
|
||||||
api.dispose();
|
api.dispose();
|
||||||
if (localeListeners.size !== 0) {
|
if (localeListeners.size !== 0) {
|
||||||
throw new Error('api.i18n locale subscription was not disposed');
|
throw new Error('api.i18n locale subscription was not disposed');
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
import { readFile } from 'node:fs/promises';
|
|
||||||
|
|
||||||
const source = await readFile(new URL('../src/lib/shell/WorkspaceTree.svelte', import.meta.url), 'utf8');
|
|
||||||
|
|
||||||
assert.match(source, /\.workspace-create-field select\s*\{[^}]*appearance:\s*none/, 'workspace template select must hide the native arrow');
|
|
||||||
assert.match(source, /\.workspace-create-field select option\s*\{[^}]*background/, 'workspace template options must use the application surface');
|
|
||||||
|
|
||||||
console.log('workspace select style contract passed');
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
|
|
||||||
const bindingsPath = path.resolve('frontend/wailsjs/go/api/App.js');
|
|
||||||
const bindings = fs.readFileSync(bindingsPath, 'utf8');
|
|
||||||
|
|
||||||
for (const method of ['ReplacePluginNotifications', 'ClearPluginNotifications']) {
|
|
||||||
if (!bindings.includes(`export function ${method}(`)) {
|
|
||||||
throw new Error(`Wails binding does not export ${method}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Wails notification bindings are present');
|
|
||||||
|
|
@ -1,19 +1,15 @@
|
||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
import {workspace} from '../models';
|
import {workspace} from '../models';
|
||||||
import {context} from '../models';
|
|
||||||
import {workbench} from '../models';
|
import {workbench} from '../models';
|
||||||
import {capability} from '../models';
|
import {capability} from '../models';
|
||||||
import {api} from '../models';
|
import {api} from '../models';
|
||||||
import {permissions} from '../models';
|
import {permissions} from '../models';
|
||||||
import {plugin} from '../models';
|
import {plugin} from '../models';
|
||||||
import {files} from '../models';
|
import {files} from '../models';
|
||||||
import {notifications} from '../models';
|
|
||||||
|
|
||||||
export function ArchiveWorkspaceNode(arg1:string):Promise<string>;
|
export function ArchiveWorkspaceNode(arg1:string):Promise<string>;
|
||||||
|
|
||||||
export function ClearPluginNotifications(arg1:string):Promise<string>;
|
|
||||||
|
|
||||||
export function CloseVault():Promise<void>;
|
export function CloseVault():Promise<void>;
|
||||||
|
|
||||||
export function CreateVault(arg1:string):Promise<void>;
|
export function CreateVault(arg1:string):Promise<void>;
|
||||||
|
|
@ -28,8 +24,6 @@ export function DeleteVaultTrash(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
export function DisablePlugin(arg1:string):Promise<string>;
|
export function DisablePlugin(arg1:string):Promise<string>;
|
||||||
|
|
||||||
export function DomReady(arg1:context.Context):Promise<void>;
|
|
||||||
|
|
||||||
export function EditWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>;
|
export function EditWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>;
|
||||||
|
|
||||||
export function EnablePlugin(arg1:string):Promise<string>;
|
export function EnablePlugin(arg1:string):Promise<string>;
|
||||||
|
|
@ -78,22 +72,22 @@ export function ListVaultFiles(arg1:string,arg2:string):Promise<Array<files.File
|
||||||
|
|
||||||
export function ListVaultTrash(arg1:string):Promise<Array<files.TrashEntry>|string>;
|
export function ListVaultTrash(arg1:string):Promise<Array<files.TrashEntry>|string>;
|
||||||
|
|
||||||
|
export function ListWorkspaces():Promise<Array<workspace.Workspace>|string>;
|
||||||
|
|
||||||
export function ListWorkspaceIdentities():Promise<Array<workspace.WorkspaceIdentity>|string>;
|
export function ListWorkspaceIdentities():Promise<Array<workspace.WorkspaceIdentity>|string>;
|
||||||
|
|
||||||
export function ListWorkspaceTemplates():Promise<Array<workspace.WorkspaceTemplate>|string>;
|
export function ListWorkspaceTemplates():Promise<Array<workspace.WorkspaceTemplate>|string>;
|
||||||
|
|
||||||
export function ListWorkspaces():Promise<Array<workspace.Workspace>|string>;
|
|
||||||
|
|
||||||
export function MoveVaultPath(arg1:string,arg2:string,arg3:string,arg4:files.MoveOptions):Promise<string>;
|
export function MoveVaultPath(arg1:string,arg2:string,arg3:string,arg4:files.MoveOptions):Promise<string>;
|
||||||
|
|
||||||
export function MoveWorkspaceNode(arg1:string,arg2:string):Promise<string>;
|
export function MoveWorkspaceNode(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
export function OpenExternalURL(arg1:string,arg2:string):Promise<string>;
|
|
||||||
|
|
||||||
export function OpenVault(arg1:string):Promise<void>;
|
export function OpenVault(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function OpenVaultPathExternal(arg1:string,arg2:string):Promise<string>;
|
export function OpenVaultPathExternal(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
|
export function OpenExternalURL(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
export function OpenWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>;
|
export function OpenWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>;
|
||||||
|
|
||||||
export function PluginBrowserReceiverPairing(arg1:string):Promise<Record<string, string>|string>;
|
export function PluginBrowserReceiverPairing(arg1:string):Promise<Record<string, string>|string>;
|
||||||
|
|
@ -130,10 +124,6 @@ export function PluginSyncTestConnection(arg1:string,arg2:string,arg3:string,arg
|
||||||
|
|
||||||
export function PublishPluginEvent(arg1:string,arg2:string,arg3:Record<string, any>):Promise<string>;
|
export function PublishPluginEvent(arg1:string,arg2:string,arg3:Record<string, any>):Promise<string>;
|
||||||
|
|
||||||
export function PurgeWorkspaceTrash(arg1:string):Promise<string>;
|
|
||||||
|
|
||||||
export function Quit():Promise<void>;
|
|
||||||
|
|
||||||
export function ReadPluginDataJSON(arg1:string,arg2:string):Promise<Record<string, any>>;
|
export function ReadPluginDataJSON(arg1:string,arg2:string):Promise<Record<string, any>>;
|
||||||
|
|
||||||
export function ReadPluginDataNDJSON(arg1:string,arg2:string):Promise<Array<Record<string, any>>>;
|
export function ReadPluginDataNDJSON(arg1:string,arg2:string):Promise<Array<Record<string, any>>>;
|
||||||
|
|
@ -152,11 +142,9 @@ export function ReloadPlugins():Promise<number|string>;
|
||||||
|
|
||||||
export function RenameWorkspace(arg1:string,arg2:string):Promise<string>;
|
export function RenameWorkspace(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
export function RenameWorkspaceNode(arg1:string,arg2:string):Promise<string>;
|
|
||||||
|
|
||||||
export function RepairWorkspaceIdentity(arg1:string,arg2:string):Promise<string>;
|
export function RepairWorkspaceIdentity(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
export function ReplacePluginNotifications(arg1:string,arg2:Array<notifications.Request>):Promise<string>;
|
export function RenameWorkspaceNode(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
export function RestoreVaultTrash(arg1:string,arg2:string,arg3:files.RestoreOptions):Promise<string|string>;
|
export function RestoreVaultTrash(arg1:string,arg2:string,arg3:files.RestoreOptions):Promise<string|string>;
|
||||||
|
|
||||||
|
|
@ -164,6 +152,8 @@ export function RestoreWorkspaceTrash(arg1:string,arg2:string):Promise<workspace
|
||||||
|
|
||||||
export function SelectDirectory():Promise<string>;
|
export function SelectDirectory():Promise<string>;
|
||||||
|
|
||||||
|
export function PurgeWorkspaceTrash(arg1:string):Promise<string>;
|
||||||
|
|
||||||
export function SelectVaultForOpen():Promise<string>;
|
export function SelectVaultForOpen():Promise<string>;
|
||||||
|
|
||||||
export function SetCurrentVault(arg1:string):Promise<string>;
|
export function SetCurrentVault(arg1:string):Promise<string>;
|
||||||
|
|
@ -172,14 +162,8 @@ export function SetCurrentWorkspace(arg1:string):Promise<string>;
|
||||||
|
|
||||||
export function SetCurrentWorkspaceNode(arg1:string):Promise<string>;
|
export function SetCurrentWorkspaceNode(arg1:string):Promise<string>;
|
||||||
|
|
||||||
export function SetNotificationService(arg1:api.notificationService):Promise<void>;
|
|
||||||
|
|
||||||
export function ShowVaultPathInFolder(arg1:string,arg2:string):Promise<string>;
|
export function ShowVaultPathInFolder(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
export function ShowWindow():Promise<void>;
|
|
||||||
|
|
||||||
export function Shutdown(arg1:context.Context):Promise<void>;
|
|
||||||
|
|
||||||
export function SubscribePluginEvent(arg1:string,arg2:string):Promise<string>;
|
export function SubscribePluginEvent(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
export function TrashVaultPath(arg1:string,arg2:string):Promise<files.TrashResult|string>;
|
export function TrashVaultPath(arg1:string,arg2:string):Promise<files.TrashResult|string>;
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,6 @@ export function ArchiveWorkspaceNode(arg1) {
|
||||||
return window['go']['api']['App']['ArchiveWorkspaceNode'](arg1);
|
return window['go']['api']['App']['ArchiveWorkspaceNode'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ClearPluginNotifications(arg1) {
|
|
||||||
return window['go']['api']['App']['ClearPluginNotifications'](arg1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CloseVault() {
|
export function CloseVault() {
|
||||||
return window['go']['api']['App']['CloseVault']();
|
return window['go']['api']['App']['CloseVault']();
|
||||||
}
|
}
|
||||||
|
|
@ -38,10 +34,6 @@ export function DisablePlugin(arg1) {
|
||||||
return window['go']['api']['App']['DisablePlugin'](arg1);
|
return window['go']['api']['App']['DisablePlugin'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DomReady(arg1) {
|
|
||||||
return window['go']['api']['App']['DomReady'](arg1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EditWorkbenchResource(arg1, arg2) {
|
export function EditWorkbenchResource(arg1, arg2) {
|
||||||
return window['go']['api']['App']['EditWorkbenchResource'](arg1, arg2);
|
return window['go']['api']['App']['EditWorkbenchResource'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
@ -138,6 +130,10 @@ export function ListVaultTrash(arg1) {
|
||||||
return window['go']['api']['App']['ListVaultTrash'](arg1);
|
return window['go']['api']['App']['ListVaultTrash'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ListWorkspaces() {
|
||||||
|
return window['go']['api']['App']['ListWorkspaces']();
|
||||||
|
}
|
||||||
|
|
||||||
export function ListWorkspaceIdentities() {
|
export function ListWorkspaceIdentities() {
|
||||||
return window['go']['api']['App']['ListWorkspaceIdentities']();
|
return window['go']['api']['App']['ListWorkspaceIdentities']();
|
||||||
}
|
}
|
||||||
|
|
@ -146,10 +142,6 @@ export function ListWorkspaceTemplates() {
|
||||||
return window['go']['api']['App']['ListWorkspaceTemplates']();
|
return window['go']['api']['App']['ListWorkspaceTemplates']();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ListWorkspaces() {
|
|
||||||
return window['go']['api']['App']['ListWorkspaces']();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MoveVaultPath(arg1, arg2, arg3, arg4) {
|
export function MoveVaultPath(arg1, arg2, arg3, arg4) {
|
||||||
return window['go']['api']['App']['MoveVaultPath'](arg1, arg2, arg3, arg4);
|
return window['go']['api']['App']['MoveVaultPath'](arg1, arg2, arg3, arg4);
|
||||||
}
|
}
|
||||||
|
|
@ -158,10 +150,6 @@ export function MoveWorkspaceNode(arg1, arg2) {
|
||||||
return window['go']['api']['App']['MoveWorkspaceNode'](arg1, arg2);
|
return window['go']['api']['App']['MoveWorkspaceNode'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OpenExternalURL(arg1, arg2) {
|
|
||||||
return window['go']['api']['App']['OpenExternalURL'](arg1, arg2);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OpenVault(arg1) {
|
export function OpenVault(arg1) {
|
||||||
return window['go']['api']['App']['OpenVault'](arg1);
|
return window['go']['api']['App']['OpenVault'](arg1);
|
||||||
}
|
}
|
||||||
|
|
@ -170,6 +158,10 @@ export function OpenVaultPathExternal(arg1, arg2) {
|
||||||
return window['go']['api']['App']['OpenVaultPathExternal'](arg1, arg2);
|
return window['go']['api']['App']['OpenVaultPathExternal'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function OpenExternalURL(arg1, arg2) {
|
||||||
|
return window['go']['api']['App']['OpenExternalURL'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
export function OpenWorkbenchResource(arg1, arg2) {
|
export function OpenWorkbenchResource(arg1, arg2) {
|
||||||
return window['go']['api']['App']['OpenWorkbenchResource'](arg1, arg2);
|
return window['go']['api']['App']['OpenWorkbenchResource'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
@ -242,14 +234,6 @@ export function PublishPluginEvent(arg1, arg2, arg3) {
|
||||||
return window['go']['api']['App']['PublishPluginEvent'](arg1, arg2, arg3);
|
return window['go']['api']['App']['PublishPluginEvent'](arg1, arg2, arg3);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PurgeWorkspaceTrash(arg1) {
|
|
||||||
return window['go']['api']['App']['PurgeWorkspaceTrash'](arg1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Quit() {
|
|
||||||
return window['go']['api']['App']['Quit']();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ReadPluginDataJSON(arg1, arg2) {
|
export function ReadPluginDataJSON(arg1, arg2) {
|
||||||
return window['go']['api']['App']['ReadPluginDataJSON'](arg1, arg2);
|
return window['go']['api']['App']['ReadPluginDataJSON'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
@ -286,16 +270,12 @@ export function RenameWorkspace(arg1, arg2) {
|
||||||
return window['go']['api']['App']['RenameWorkspace'](arg1, arg2);
|
return window['go']['api']['App']['RenameWorkspace'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RenameWorkspaceNode(arg1, arg2) {
|
|
||||||
return window['go']['api']['App']['RenameWorkspaceNode'](arg1, arg2);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RepairWorkspaceIdentity(arg1, arg2) {
|
export function RepairWorkspaceIdentity(arg1, arg2) {
|
||||||
return window['go']['api']['App']['RepairWorkspaceIdentity'](arg1, arg2);
|
return window['go']['api']['App']['RepairWorkspaceIdentity'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReplacePluginNotifications(arg1, arg2) {
|
export function RenameWorkspaceNode(arg1, arg2) {
|
||||||
return window['go']['api']['App']['ReplacePluginNotifications'](arg1, arg2);
|
return window['go']['api']['App']['RenameWorkspaceNode'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RestoreVaultTrash(arg1, arg2, arg3) {
|
export function RestoreVaultTrash(arg1, arg2, arg3) {
|
||||||
|
|
@ -310,6 +290,10 @@ export function SelectDirectory() {
|
||||||
return window['go']['api']['App']['SelectDirectory']();
|
return window['go']['api']['App']['SelectDirectory']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PurgeWorkspaceTrash(arg1) {
|
||||||
|
return window['go']['api']['App']['PurgeWorkspaceTrash'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function SelectVaultForOpen() {
|
export function SelectVaultForOpen() {
|
||||||
return window['go']['api']['App']['SelectVaultForOpen']();
|
return window['go']['api']['App']['SelectVaultForOpen']();
|
||||||
}
|
}
|
||||||
|
|
@ -326,22 +310,10 @@ export function SetCurrentWorkspaceNode(arg1) {
|
||||||
return window['go']['api']['App']['SetCurrentWorkspaceNode'](arg1);
|
return window['go']['api']['App']['SetCurrentWorkspaceNode'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SetNotificationService(arg1) {
|
|
||||||
return window['go']['api']['App']['SetNotificationService'](arg1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShowVaultPathInFolder(arg1, arg2) {
|
export function ShowVaultPathInFolder(arg1, arg2) {
|
||||||
return window['go']['api']['App']['ShowVaultPathInFolder'](arg1, arg2);
|
return window['go']['api']['App']['ShowVaultPathInFolder'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ShowWindow() {
|
|
||||||
return window['go']['api']['App']['ShowWindow']();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Shutdown(arg1) {
|
|
||||||
return window['go']['api']['App']['Shutdown'](arg1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SubscribePluginEvent(arg1, arg2) {
|
export function SubscribePluginEvent(arg1, arg2) {
|
||||||
return window['go']['api']['App']['SubscribePluginEvent'](arg1, arg2);
|
return window['go']['api']['App']['SubscribePluginEvent'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
export namespace api {
|
export namespace api {
|
||||||
|
|
||||||
export class FlatContextMenuEntry {
|
export class FlatContextMenuEntry {
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -8,11 +8,11 @@ export namespace api {
|
||||||
group?: string;
|
group?: string;
|
||||||
capability?: string;
|
capability?: string;
|
||||||
handler?: string;
|
handler?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FlatContextMenuEntry(source);
|
return new FlatContextMenuEntry(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.pluginId = source["pluginId"];
|
this.pluginId = source["pluginId"];
|
||||||
|
|
@ -31,11 +31,11 @@ export namespace api {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
capability?: string;
|
capability?: string;
|
||||||
handler?: string;
|
handler?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FlatAction(source);
|
return new FlatAction(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.pluginId = source["pluginId"];
|
this.pluginId = source["pluginId"];
|
||||||
|
|
@ -52,11 +52,11 @@ export namespace api {
|
||||||
title: string;
|
title: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
component: string;
|
component: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FlatWorkspaceItem(source);
|
return new FlatWorkspaceItem(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.pluginId = source["pluginId"];
|
this.pluginId = source["pluginId"];
|
||||||
|
|
@ -72,11 +72,11 @@ export namespace api {
|
||||||
extensions?: string[];
|
extensions?: string[];
|
||||||
contexts?: string[];
|
contexts?: string[];
|
||||||
modes?: string[];
|
modes?: string[];
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FlatOpenProviderSupport(source);
|
return new FlatOpenProviderSupport(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.kind = source["kind"];
|
this.kind = source["kind"];
|
||||||
|
|
@ -93,11 +93,11 @@ export namespace api {
|
||||||
priority?: number;
|
priority?: number;
|
||||||
component: string;
|
component: string;
|
||||||
supports: FlatOpenProviderSupport[];
|
supports: FlatOpenProviderSupport[];
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FlatOpenProvider(source);
|
return new FlatOpenProvider(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.pluginId = source["pluginId"];
|
this.pluginId = source["pluginId"];
|
||||||
|
|
@ -107,7 +107,7 @@ export namespace api {
|
||||||
this.component = source["component"];
|
this.component = source["component"];
|
||||||
this.supports = this.convertValues(source["supports"], FlatOpenProviderSupport);
|
this.supports = this.convertValues(source["supports"], FlatOpenProviderSupport);
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
|
|
@ -132,11 +132,11 @@ export namespace api {
|
||||||
label: string;
|
label: string;
|
||||||
position?: string;
|
position?: string;
|
||||||
handler?: string;
|
handler?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FlatStatusBarItem(source);
|
return new FlatStatusBarItem(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.pluginId = source["pluginId"];
|
this.pluginId = source["pluginId"];
|
||||||
|
|
@ -153,11 +153,11 @@ export namespace api {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
view: string;
|
view: string;
|
||||||
position?: number;
|
position?: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FlatSidebarItem(source);
|
return new FlatSidebarItem(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.pluginId = source["pluginId"];
|
this.pluginId = source["pluginId"];
|
||||||
|
|
@ -174,11 +174,11 @@ export namespace api {
|
||||||
title: string;
|
title: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
component: string;
|
component: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FlatSettingsPanel(source);
|
return new FlatSettingsPanel(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.pluginId = source["pluginId"];
|
this.pluginId = source["pluginId"];
|
||||||
|
|
@ -193,11 +193,11 @@ export namespace api {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
handler: string;
|
handler: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FlatSearchProvider(source);
|
return new FlatSearchProvider(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.pluginId = source["pluginId"];
|
this.pluginId = source["pluginId"];
|
||||||
|
|
@ -212,11 +212,11 @@ export namespace api {
|
||||||
title: string;
|
title: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
handler?: string;
|
handler?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FlatCommand(source);
|
return new FlatCommand(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.pluginId = source["pluginId"];
|
this.pluginId = source["pluginId"];
|
||||||
|
|
@ -232,11 +232,11 @@ export namespace api {
|
||||||
title: string;
|
title: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
component: string;
|
component: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FlatView(source);
|
return new FlatView(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.pluginId = source["pluginId"];
|
this.pluginId = source["pluginId"];
|
||||||
|
|
@ -258,11 +258,11 @@ export namespace api {
|
||||||
fileActions: FlatAction[];
|
fileActions: FlatAction[];
|
||||||
noteActions: FlatAction[];
|
noteActions: FlatAction[];
|
||||||
contextMenuEntries: FlatContextMenuEntry[];
|
contextMenuEntries: FlatContextMenuEntry[];
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ContributionSummary(source);
|
return new ContributionSummary(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.views = this.convertValues(source["views"], FlatView);
|
this.views = this.convertValues(source["views"], FlatView);
|
||||||
|
|
@ -277,7 +277,7 @@ export namespace api {
|
||||||
this.noteActions = this.convertValues(source["noteActions"], FlatAction);
|
this.noteActions = this.convertValues(source["noteActions"], FlatAction);
|
||||||
this.contextMenuEntries = this.convertValues(source["contextMenuEntries"], FlatContextMenuEntry);
|
this.contextMenuEntries = this.convertValues(source["contextMenuEntries"], FlatContextMenuEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
|
|
@ -296,17 +296,17 @@ export namespace api {
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class SyncStatusDTO {
|
export class SyncStatusDTO {
|
||||||
configured: boolean;
|
configured: boolean;
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
|
|
@ -320,11 +320,11 @@ export namespace api {
|
||||||
syncInterval: number;
|
syncInterval: number;
|
||||||
lastError: string;
|
lastError: string;
|
||||||
statusLabel: string;
|
statusLabel: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new SyncStatusDTO(source);
|
return new SyncStatusDTO(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.configured = source["configured"];
|
this.configured = source["configured"];
|
||||||
|
|
@ -345,17 +345,17 @@ export namespace api {
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace capability {
|
export namespace capability {
|
||||||
|
|
||||||
export class Entry {
|
export class Entry {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Entry(source);
|
return new Entry(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.name = source["name"];
|
this.name = source["name"];
|
||||||
|
|
@ -368,17 +368,17 @@ export namespace capability {
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace files {
|
export namespace files {
|
||||||
|
|
||||||
export class FileBytes {
|
export class FileBytes {
|
||||||
relativePath: string;
|
relativePath: string;
|
||||||
size: number;
|
size: number;
|
||||||
mimeHint: string;
|
mimeHint: string;
|
||||||
dataBase64: string;
|
dataBase64: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FileBytes(source);
|
return new FileBytes(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.relativePath = source["relativePath"];
|
this.relativePath = source["relativePath"];
|
||||||
|
|
@ -398,11 +398,11 @@ export namespace files {
|
||||||
isReserved: boolean;
|
isReserved: boolean;
|
||||||
canRead: boolean;
|
canRead: boolean;
|
||||||
canWrite: boolean;
|
canWrite: boolean;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FileEntry(source);
|
return new FileEntry(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.name = source["name"];
|
this.name = source["name"];
|
||||||
|
|
@ -430,11 +430,11 @@ export namespace files {
|
||||||
isReserved: boolean;
|
isReserved: boolean;
|
||||||
canRead: boolean;
|
canRead: boolean;
|
||||||
canWrite: boolean;
|
canWrite: boolean;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FileMetadata(source);
|
return new FileMetadata(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.relativePath = source["relativePath"];
|
this.relativePath = source["relativePath"];
|
||||||
|
|
@ -453,11 +453,11 @@ export namespace files {
|
||||||
}
|
}
|
||||||
export class MoveOptions {
|
export class MoveOptions {
|
||||||
overwrite: boolean;
|
overwrite: boolean;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new MoveOptions(source);
|
return new MoveOptions(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.overwrite = source["overwrite"];
|
this.overwrite = source["overwrite"];
|
||||||
|
|
@ -466,11 +466,11 @@ export namespace files {
|
||||||
export class RestoreOptions {
|
export class RestoreOptions {
|
||||||
targetPath?: string;
|
targetPath?: string;
|
||||||
overwrite: boolean;
|
overwrite: boolean;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new RestoreOptions(source);
|
return new RestoreOptions(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.targetPath = source["targetPath"];
|
this.targetPath = source["targetPath"];
|
||||||
|
|
@ -485,11 +485,11 @@ export namespace files {
|
||||||
originalType: string;
|
originalType: string;
|
||||||
basename: string;
|
basename: string;
|
||||||
size: number;
|
size: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new TrashEntry(source);
|
return new TrashEntry(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.originalPath = source["originalPath"];
|
this.originalPath = source["originalPath"];
|
||||||
|
|
@ -507,11 +507,11 @@ export namespace files {
|
||||||
trashId: string;
|
trashId: string;
|
||||||
deletedAt: string;
|
deletedAt: string;
|
||||||
size: number;
|
size: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new TrashResult(source);
|
return new TrashResult(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.originalPath = source["originalPath"];
|
this.originalPath = source["originalPath"];
|
||||||
|
|
@ -524,11 +524,11 @@ export namespace files {
|
||||||
export class WriteOptions {
|
export class WriteOptions {
|
||||||
createIfMissing: boolean;
|
createIfMissing: boolean;
|
||||||
overwrite: boolean;
|
overwrite: boolean;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new WriteOptions(source);
|
return new WriteOptions(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.createIfMissing = source["createIfMissing"];
|
this.createIfMissing = source["createIfMissing"];
|
||||||
|
|
@ -538,40 +538,17 @@ export namespace files {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace notifications {
|
|
||||||
|
|
||||||
export class Request {
|
|
||||||
id: string;
|
|
||||||
dueAt: string;
|
|
||||||
title: string;
|
|
||||||
body?: string;
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new Request(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.id = source["id"];
|
|
||||||
this.dueAt = source["dueAt"];
|
|
||||||
this.title = source["title"];
|
|
||||||
this.body = source["body"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace permissions {
|
export namespace permissions {
|
||||||
|
|
||||||
export class Entry {
|
export class Entry {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
dangerous: boolean;
|
dangerous: boolean;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Entry(source);
|
return new Entry(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.name = source["name"];
|
this.name = source["name"];
|
||||||
|
|
@ -583,15 +560,15 @@ export namespace permissions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace plugin {
|
export namespace plugin {
|
||||||
|
|
||||||
export class HealthCheckConfig {
|
export class HealthCheckConfig {
|
||||||
type?: string;
|
type?: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new HealthCheckConfig(source);
|
return new HealthCheckConfig(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.type = source["type"];
|
this.type = source["type"];
|
||||||
|
|
@ -602,18 +579,18 @@ export namespace plugin {
|
||||||
type: string;
|
type: string;
|
||||||
entry: Record<string, string>;
|
entry: Record<string, string>;
|
||||||
healthCheck?: HealthCheckConfig;
|
healthCheck?: HealthCheckConfig;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new BackendConfig(source);
|
return new BackendConfig(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.type = source["type"];
|
this.type = source["type"];
|
||||||
this.entry = source["entry"];
|
this.entry = source["entry"];
|
||||||
this.healthCheck = this.convertValues(source["healthCheck"], HealthCheckConfig);
|
this.healthCheck = this.convertValues(source["healthCheck"], HealthCheckConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
|
|
@ -638,11 +615,11 @@ export namespace plugin {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
capability?: string;
|
capability?: string;
|
||||||
handler?: string;
|
handler?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ContributionAction(source);
|
return new ContributionAction(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
|
|
@ -656,11 +633,11 @@ export namespace plugin {
|
||||||
id: string;
|
id: string;
|
||||||
events?: string[];
|
events?: string[];
|
||||||
handler: string;
|
handler: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ContributionActivityProvider(source);
|
return new ContributionActivityProvider(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
|
|
@ -674,11 +651,11 @@ export namespace plugin {
|
||||||
keybinding?: string;
|
keybinding?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
handler?: string;
|
handler?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ContributionCommand(source);
|
return new ContributionCommand(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
|
|
@ -695,11 +672,11 @@ export namespace plugin {
|
||||||
group?: string;
|
group?: string;
|
||||||
capability?: string;
|
capability?: string;
|
||||||
handler?: string;
|
handler?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ContributionContextMenuEntry(source);
|
return new ContributionContextMenuEntry(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
|
|
@ -716,11 +693,11 @@ export namespace plugin {
|
||||||
extensions?: string[];
|
extensions?: string[];
|
||||||
contexts?: string[];
|
contexts?: string[];
|
||||||
modes?: string[];
|
modes?: string[];
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new OpenProviderSupport(source);
|
return new OpenProviderSupport(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.kind = source["kind"];
|
this.kind = source["kind"];
|
||||||
|
|
@ -736,11 +713,11 @@ export namespace plugin {
|
||||||
priority?: number;
|
priority?: number;
|
||||||
component: string;
|
component: string;
|
||||||
supports: OpenProviderSupport[];
|
supports: OpenProviderSupport[];
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ContributionOpenProvider(source);
|
return new ContributionOpenProvider(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
|
|
@ -749,7 +726,7 @@ export namespace plugin {
|
||||||
this.component = source["component"];
|
this.component = source["component"];
|
||||||
this.supports = this.convertValues(source["supports"], OpenProviderSupport);
|
this.supports = this.convertValues(source["supports"], OpenProviderSupport);
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
|
|
@ -772,11 +749,11 @@ export namespace plugin {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
handler: string;
|
handler: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ContributionSearchProvider(source);
|
return new ContributionSearchProvider(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
|
|
@ -789,11 +766,11 @@ export namespace plugin {
|
||||||
title: string;
|
title: string;
|
||||||
component: string;
|
component: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ContributionSettingsPanel(source);
|
return new ContributionSettingsPanel(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
|
|
@ -808,11 +785,11 @@ export namespace plugin {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
view: string;
|
view: string;
|
||||||
position?: number;
|
position?: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ContributionSidebarItem(source);
|
return new ContributionSidebarItem(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
|
|
@ -827,11 +804,11 @@ export namespace plugin {
|
||||||
label: string;
|
label: string;
|
||||||
position?: string;
|
position?: string;
|
||||||
handler?: string;
|
handler?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ContributionStatusBarItem(source);
|
return new ContributionStatusBarItem(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
|
|
@ -845,11 +822,11 @@ export namespace plugin {
|
||||||
title: string;
|
title: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
component: string;
|
component: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ContributionView(source);
|
return new ContributionView(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
|
|
@ -863,11 +840,11 @@ export namespace plugin {
|
||||||
title: string;
|
title: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
component: string;
|
component: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ContributionWorkspaceItem(source);
|
return new ContributionWorkspaceItem(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
|
|
@ -889,11 +866,11 @@ export namespace plugin {
|
||||||
statusBarItems?: ContributionStatusBarItem[];
|
statusBarItems?: ContributionStatusBarItem[];
|
||||||
openProviders?: ContributionOpenProvider[];
|
openProviders?: ContributionOpenProvider[];
|
||||||
workspaceItems?: ContributionWorkspaceItem[];
|
workspaceItems?: ContributionWorkspaceItem[];
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Contributions(source);
|
return new Contributions(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.views = this.convertValues(source["views"], ContributionView);
|
this.views = this.convertValues(source["views"], ContributionView);
|
||||||
|
|
@ -909,7 +886,7 @@ export namespace plugin {
|
||||||
this.openProviders = this.convertValues(source["openProviders"], ContributionOpenProvider);
|
this.openProviders = this.convertValues(source["openProviders"], ContributionOpenProvider);
|
||||||
this.workspaceItems = this.convertValues(source["workspaceItems"], ContributionWorkspaceItem);
|
this.workspaceItems = this.convertValues(source["workspaceItems"], ContributionWorkspaceItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
|
|
@ -931,18 +908,18 @@ export namespace plugin {
|
||||||
export class FrontendConfig {
|
export class FrontendConfig {
|
||||||
entry: string;
|
entry: string;
|
||||||
style?: string;
|
style?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new FrontendConfig(source);
|
return new FrontendConfig(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.entry = source["entry"];
|
this.entry = source["entry"];
|
||||||
this.style = source["style"];
|
this.style = source["style"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LocalizationConfig {
|
export class LocalizationConfig {
|
||||||
defaultLocale: string;
|
defaultLocale: string;
|
||||||
locales: Record<string, string>;
|
locales: Record<string, string>;
|
||||||
|
|
@ -960,11 +937,11 @@ export namespace plugin {
|
||||||
export class SyncConfig {
|
export class SyncConfig {
|
||||||
namespaces?: string[];
|
namespaces?: string[];
|
||||||
participate?: boolean;
|
participate?: boolean;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new SyncConfig(source);
|
return new SyncConfig(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.namespaces = source["namespaces"];
|
this.namespaces = source["namespaces"];
|
||||||
|
|
@ -973,11 +950,11 @@ export namespace plugin {
|
||||||
}
|
}
|
||||||
export class MigrationConfig {
|
export class MigrationConfig {
|
||||||
path?: string;
|
path?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new MigrationConfig(source);
|
return new MigrationConfig(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.path = source["path"];
|
this.path = source["path"];
|
||||||
|
|
@ -1002,11 +979,11 @@ export namespace plugin {
|
||||||
migrations?: MigrationConfig;
|
migrations?: MigrationConfig;
|
||||||
contributes?: Contributions;
|
contributes?: Contributions;
|
||||||
sync?: SyncConfig;
|
sync?: SyncConfig;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Manifest(source);
|
return new Manifest(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.schemaVersion = source["schemaVersion"];
|
this.schemaVersion = source["schemaVersion"];
|
||||||
|
|
@ -1028,7 +1005,7 @@ export namespace plugin {
|
||||||
this.contributes = this.convertValues(source["contributes"], Contributions);
|
this.contributes = this.convertValues(source["contributes"], Contributions);
|
||||||
this.sync = this.convertValues(source["sync"], SyncConfig);
|
this.sync = this.convertValues(source["sync"], SyncConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
|
|
@ -1047,19 +1024,19 @@ export namespace plugin {
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class Plugin {
|
export class Plugin {
|
||||||
manifest: Manifest;
|
manifest: Manifest;
|
||||||
status: string;
|
status: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
rootPath: string;
|
rootPath: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Plugin(source);
|
return new Plugin(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.manifest = this.convertValues(source["manifest"], Manifest);
|
this.manifest = this.convertValues(source["manifest"], Manifest);
|
||||||
|
|
@ -1068,7 +1045,7 @@ export namespace plugin {
|
||||||
this.enabled = source["enabled"];
|
this.enabled = source["enabled"];
|
||||||
this.rootPath = source["rootPath"];
|
this.rootPath = source["rootPath"];
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
|
|
@ -1091,18 +1068,18 @@ export namespace plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace workbench {
|
export namespace workbench {
|
||||||
|
|
||||||
export class OpenResourceContext {
|
export class OpenResourceContext {
|
||||||
sourcePluginId?: string;
|
sourcePluginId?: string;
|
||||||
sourceView?: string;
|
sourceView?: string;
|
||||||
isInsideNotesFolder?: boolean;
|
isInsideNotesFolder?: boolean;
|
||||||
notesScopePath?: string;
|
notesScopePath?: string;
|
||||||
notesMode?: boolean;
|
notesMode?: boolean;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new OpenResourceContext(source);
|
return new OpenResourceContext(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.sourcePluginId = source["sourcePluginId"];
|
this.sourcePluginId = source["sourcePluginId"];
|
||||||
|
|
@ -1119,11 +1096,11 @@ export namespace workbench {
|
||||||
mime?: string;
|
mime?: string;
|
||||||
extension?: string;
|
extension?: string;
|
||||||
context?: OpenResourceContext;
|
context?: OpenResourceContext;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new OpenResourceRequest(source);
|
return new OpenResourceRequest(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.kind = source["kind"];
|
this.kind = source["kind"];
|
||||||
|
|
@ -1133,7 +1110,7 @@ export namespace workbench {
|
||||||
this.extension = source["extension"];
|
this.extension = source["extension"];
|
||||||
this.context = this.convertValues(source["context"], OpenResourceContext);
|
this.context = this.convertValues(source["context"], OpenResourceContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
|
|
@ -1159,11 +1136,11 @@ export namespace workbench {
|
||||||
providerComponent?: string;
|
providerComponent?: string;
|
||||||
request: OpenResourceRequest;
|
request: OpenResourceRequest;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new OpenResourceResult(source);
|
return new OpenResourceResult(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.status = source["status"];
|
this.status = source["status"];
|
||||||
|
|
@ -1173,7 +1150,7 @@ export namespace workbench {
|
||||||
this.request = this.convertValues(source["request"], OpenResourceRequest);
|
this.request = this.convertValues(source["request"], OpenResourceRequest);
|
||||||
this.message = source["message"];
|
this.message = source["message"];
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
|
|
@ -1199,11 +1176,11 @@ export namespace workbench {
|
||||||
providerComponent: string;
|
providerComponent: string;
|
||||||
request: OpenResourceRequest;
|
request: OpenResourceRequest;
|
||||||
openedAt: string;
|
openedAt: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new OpenedResource(source);
|
return new OpenedResource(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
|
|
@ -1213,7 +1190,7 @@ export namespace workbench {
|
||||||
this.request = this.convertValues(source["request"], OpenResourceRequest);
|
this.request = this.convertValues(source["request"], OpenResourceRequest);
|
||||||
this.openedAt = source["openedAt"];
|
this.openedAt = source["openedAt"];
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
|
|
@ -1236,11 +1213,11 @@ export namespace workbench {
|
||||||
defaultTextEditorProvider?: string;
|
defaultTextEditorProvider?: string;
|
||||||
defaultMarkdownEditorProvider?: string;
|
defaultMarkdownEditorProvider?: string;
|
||||||
defaultNotesMarkdownEditorProvider?: string;
|
defaultNotesMarkdownEditorProvider?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Preferences(source);
|
return new Preferences(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.defaultTextEditorProvider = source["defaultTextEditorProvider"];
|
this.defaultTextEditorProvider = source["defaultTextEditorProvider"];
|
||||||
|
|
@ -1253,17 +1230,38 @@ export namespace workbench {
|
||||||
|
|
||||||
export namespace workspace {
|
export namespace workspace {
|
||||||
|
|
||||||
|
export class WorkspaceTemplate {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
version: number;
|
||||||
|
workspaceTools: string[];
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new WorkspaceTemplate(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.id = source["id"];
|
||||||
|
this.name = source["name"];
|
||||||
|
this.description = source["description"];
|
||||||
|
this.version = source["version"];
|
||||||
|
this.workspaceTools = source["workspaceTools"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class TemplateSnapshot {
|
export class TemplateSnapshot {
|
||||||
templateId: string;
|
templateId: string;
|
||||||
templateName: string;
|
templateName: string;
|
||||||
templateVersion: number;
|
templateVersion: number;
|
||||||
appliedAt: string;
|
appliedAt: string;
|
||||||
workspaceTools?: string[];
|
workspaceTools?: string[];
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new TemplateSnapshot(source);
|
return new TemplateSnapshot(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.templateId = source["templateId"];
|
this.templateId = source["templateId"];
|
||||||
|
|
@ -1281,11 +1279,11 @@ export namespace workspace {
|
||||||
folders?: Record<string, string>;
|
folders?: Record<string, string>;
|
||||||
workspaceTools?: string[];
|
workspaceTools?: string[];
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Metadata(source);
|
return new Metadata(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.workspaceId = source["workspaceId"];
|
this.workspaceId = source["workspaceId"];
|
||||||
|
|
@ -1296,7 +1294,7 @@ export namespace workspace {
|
||||||
this.workspaceTools = source["workspaceTools"];
|
this.workspaceTools = source["workspaceTools"];
|
||||||
this.updatedAt = source["updatedAt"];
|
this.updatedAt = source["updatedAt"];
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
|
|
@ -1318,29 +1316,29 @@ export namespace workspace {
|
||||||
export class MetadataPatch {
|
export class MetadataPatch {
|
||||||
features?: Record<string, boolean>;
|
features?: Record<string, boolean>;
|
||||||
folders?: Record<string, string>;
|
folders?: Record<string, string>;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new MetadataPatch(source);
|
return new MetadataPatch(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.features = source["features"];
|
this.features = source["features"];
|
||||||
this.folders = source["folders"];
|
this.folders = source["folders"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TrashResult {
|
export class TrashResult {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
originalPath: string;
|
originalPath: string;
|
||||||
trashPath: string;
|
trashPath: string;
|
||||||
trashId: string;
|
trashId: string;
|
||||||
deletedAt: string;
|
deletedAt: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new TrashResult(source);
|
return new TrashResult(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.workspaceId = source["workspaceId"];
|
this.workspaceId = source["workspaceId"];
|
||||||
|
|
@ -1354,11 +1352,11 @@ export namespace workspace {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
rootPath: string;
|
rootPath: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Workspace(source);
|
return new Workspace(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
|
|
@ -1366,6 +1364,7 @@ export namespace workspace {
|
||||||
this.rootPath = source["rootPath"];
|
this.rootPath = source["rootPath"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WorkspaceIdentity {
|
export class WorkspaceIdentity {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
rootPath: string;
|
rootPath: string;
|
||||||
|
|
@ -1382,25 +1381,5 @@ export namespace workspace {
|
||||||
this.state = source["state"];
|
this.state = source["state"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class WorkspaceTemplate {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
version: number;
|
|
||||||
workspaceTools: string[];
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new WorkspaceTemplate(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.id = source["id"];
|
|
||||||
this.name = source["name"];
|
|
||||||
this.description = source["description"];
|
|
||||||
this.version = source["version"];
|
|
||||||
this.workspaceTools = source["workspaceTools"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
go.mod
9
go.mod
|
|
@ -3,7 +3,6 @@ module github.com/verstak/verstak-desktop
|
||||||
go 1.24.4
|
go 1.24.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/getlantern/systray v1.2.2
|
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/wailsapp/wails/v2 v2.12.0
|
github.com/wailsapp/wails/v2 v2.12.0
|
||||||
golang.org/x/net v0.35.0
|
golang.org/x/net v0.35.0
|
||||||
|
|
@ -12,14 +11,7 @@ require (
|
||||||
require (
|
require (
|
||||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||||
github.com/bep/debounce v1.2.1 // indirect
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
|
||||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
|
||||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
|
||||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
|
||||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
|
||||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-stack/stack v1.8.0 // indirect
|
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||||
|
|
@ -31,7 +23,6 @@ require (
|
||||||
github.com/leaanthony/u v1.1.1 // indirect
|
github.com/leaanthony/u v1.1.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
|
|
||||||
26
go.sum
26
go.sum
|
|
@ -2,27 +2,10 @@ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf
|
||||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
|
||||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
|
||||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
|
||||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
|
||||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
|
||||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
|
||||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
|
||||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
|
||||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
|
||||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
|
||||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
|
||||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
|
||||||
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
|
||||||
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
|
@ -45,8 +28,6 @@ github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/
|
||||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
|
||||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
|
||||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
|
@ -55,8 +36,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
|
@ -68,9 +47,6 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||||
|
|
@ -91,7 +67,6 @@ golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|
@ -104,6 +79,5 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import (
|
||||||
"github.com/verstak/verstak-desktop/internal/core/externalopen"
|
"github.com/verstak/verstak-desktop/internal/core/externalopen"
|
||||||
corefiles "github.com/verstak/verstak-desktop/internal/core/files"
|
corefiles "github.com/verstak/verstak-desktop/internal/core/files"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/filewatcher"
|
"github.com/verstak/verstak-desktop/internal/core/filewatcher"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/notifications"
|
|
||||||
"github.com/verstak/verstak-desktop/internal/core/permissions"
|
"github.com/verstak/verstak-desktop/internal/core/permissions"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/plugin"
|
"github.com/verstak/verstak-desktop/internal/core/plugin"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/pluginstate"
|
"github.com/verstak/verstak-desktop/internal/core/pluginstate"
|
||||||
|
|
@ -39,19 +38,6 @@ import (
|
||||||
|
|
||||||
var newSyncClient = syncsvc.NewClient
|
var newSyncClient = syncsvc.NewClient
|
||||||
var emitFrontendEvent = runtime.EventsEmit
|
var emitFrontendEvent = runtime.EventsEmit
|
||||||
var initializeNativeNotifications = runtime.InitializeNotifications
|
|
||||||
var cleanupNativeNotifications = runtime.CleanupNotifications
|
|
||||||
var sendNativeNotification = runtime.SendNotification
|
|
||||||
var hideNativeWindow = runtime.WindowHide
|
|
||||||
var showNativeWindow = runtime.WindowShow
|
|
||||||
var quitNativeApplication = runtime.Quit
|
|
||||||
|
|
||||||
type notificationService interface {
|
|
||||||
Replace(pluginID string, requests []notifications.Request) error
|
|
||||||
Clear(pluginID string) error
|
|
||||||
Start(ctx context.Context)
|
|
||||||
Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
const pluginEventRuntimeName = "verstak:plugin-event"
|
const pluginEventRuntimeName = "verstak:plugin-event"
|
||||||
const activityPluginID = "verstak.activity"
|
const activityPluginID = "verstak.activity"
|
||||||
|
|
@ -94,20 +80,10 @@ type App struct {
|
||||||
browserReceiver *browserreceiver.Receiver
|
browserReceiver *browserreceiver.Receiver
|
||||||
secretsSession *coresecrets.VaultSession
|
secretsSession *coresecrets.VaultSession
|
||||||
fileWatcher *filewatcher.Service
|
fileWatcher *filewatcher.Service
|
||||||
notifications notificationService
|
|
||||||
debug bool
|
debug bool
|
||||||
activityEvents map[string]bool
|
activityEvents map[string]bool
|
||||||
browserInboxEvents map[string]bool
|
browserInboxEvents map[string]bool
|
||||||
browserInboxEnabled atomic.Bool
|
browserInboxEnabled atomic.Bool
|
||||||
allowQuit atomic.Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNotificationService attaches the core-owned plugin notification scheduler.
|
|
||||||
func (a *App) SetNotificationService(service notificationService) {
|
|
||||||
if a == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
a.notifications = service
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type externalOpenService interface {
|
type externalOpenService interface {
|
||||||
|
|
@ -201,71 +177,6 @@ func (a *App) Startup(ctx context.Context) {
|
||||||
log.Printf("[api] App.Startup: initialized with %d plugins", len(a.plugins))
|
log.Printf("[api] App.Startup: initialized with %d plugins", len(a.plugins))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DomReady initializes the native notification runtime before starting schedules.
|
|
||||||
func (a *App) DomReady(ctx context.Context) {
|
|
||||||
if a.notifications == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := initializeNativeNotifications(ctx); err != nil {
|
|
||||||
log.Printf("[api] native notifications unavailable: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
a.notifications.Start(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown stops scheduled delivery before releasing native notification resources.
|
|
||||||
func (a *App) Shutdown(ctx context.Context) {
|
|
||||||
if a.notifications != nil {
|
|
||||||
a.notifications.Stop()
|
|
||||||
}
|
|
||||||
cleanupNativeNotifications(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeClose hides the primary window until the user explicitly quits from the tray.
|
|
||||||
func (a *App) BeforeClose(ctx context.Context) bool {
|
|
||||||
if a.allowQuit.Load() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
hideNativeWindow(ctx)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShowWindow brings the primary window back from the tray.
|
|
||||||
func (a *App) ShowWindow() {
|
|
||||||
if a == nil || a.ctx == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
showNativeWindow(a.ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quit allows the close event and ends the application process.
|
|
||||||
func (a *App) Quit() {
|
|
||||||
if a == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
a.allowQuit.Store(true)
|
|
||||||
if a.ctx != nil {
|
|
||||||
quitNativeApplication(a.ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NativeNotificationSender delivers scheduler items through the Wails runtime.
|
|
||||||
type NativeNotificationSender struct{}
|
|
||||||
|
|
||||||
// NewNativeNotificationSender creates the adapter used by the core scheduler.
|
|
||||||
func NewNativeNotificationSender() notifications.Sender {
|
|
||||||
return NativeNotificationSender{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send shows a native system notification for a scheduled plugin reminder.
|
|
||||||
func (NativeNotificationSender) Send(ctx context.Context, item notifications.Item) error {
|
|
||||||
return sendNativeNotification(ctx, runtime.NotificationOptions{
|
|
||||||
ID: "verstak:" + item.PluginID + ":" + item.ID,
|
|
||||||
Title: item.Title,
|
|
||||||
Body: item.Body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) ensureBrowserInboxSubscriptions() {
|
func (a *App) ensureBrowserInboxSubscriptions() {
|
||||||
if a.eventBus == nil || a.storage == nil {
|
if a.eventBus == nil || a.storage == nil {
|
||||||
a.browserInboxEnabled.Store(false)
|
a.browserInboxEnabled.Store(false)
|
||||||
|
|
@ -1355,44 +1266,6 @@ func (a *App) WritePluginDataJSON(pluginID, name string, data map[string]interfa
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplacePluginNotifications replaces one plugin's desired native notification
|
|
||||||
// schedules. Plugins must declare both the capability and permission.
|
|
||||||
func (a *App) ReplacePluginNotifications(pluginID string, requests []notifications.Request) string {
|
|
||||||
if _, err := a.requirePluginAccess(pluginID, "notifications.schedule"); err != nil {
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
if _, err := a.requirePluginCapabilityAccess(pluginID, "verstak/core/notifications/v1"); err != nil {
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
if a.notifications == nil {
|
|
||||||
return "notification scheduler not initialized"
|
|
||||||
}
|
|
||||||
if err := a.notifications.Replace(pluginID, requests); err != nil {
|
|
||||||
log.Printf("[api] ReplacePluginNotifications(%s): %v", pluginID, err)
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearPluginNotifications removes every native notification schedule owned by
|
|
||||||
// one plugin.
|
|
||||||
func (a *App) ClearPluginNotifications(pluginID string) string {
|
|
||||||
if _, err := a.requirePluginAccess(pluginID, "notifications.schedule"); err != nil {
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
if _, err := a.requirePluginCapabilityAccess(pluginID, "verstak/core/notifications/v1"); err != nil {
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
if a.notifications == nil {
|
|
||||||
return "notification scheduler not initialized"
|
|
||||||
}
|
|
||||||
if err := a.notifications.Clear(pluginID); err != nil {
|
|
||||||
log.Printf("[api] ClearPluginNotifications(%s): %v", pluginID, err)
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListVaultFiles lists a vault-relative directory for a plugin with files.read.
|
// ListVaultFiles lists a vault-relative directory for a plugin with files.read.
|
||||||
func (a *App) ListVaultFiles(pluginID, relativeDir string) ([]corefiles.FileEntry, string) {
|
func (a *App) ListVaultFiles(pluginID, relativeDir string) ([]corefiles.FileEntry, string) {
|
||||||
if _, err := a.requirePluginAccess(pluginID, "files.read"); err != nil {
|
if _, err := a.requirePluginAccess(pluginID, "files.read"); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,12 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
||||||
|
|
||||||
"github.com/verstak/verstak-desktop/internal/core/appsettings"
|
"github.com/verstak/verstak-desktop/internal/core/appsettings"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/browserreceiver"
|
"github.com/verstak/verstak-desktop/internal/core/browserreceiver"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/capability"
|
"github.com/verstak/verstak-desktop/internal/core/capability"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/contribution"
|
"github.com/verstak/verstak-desktop/internal/core/contribution"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/events"
|
"github.com/verstak/verstak-desktop/internal/core/events"
|
||||||
corefiles "github.com/verstak/verstak-desktop/internal/core/files"
|
corefiles "github.com/verstak/verstak-desktop/internal/core/files"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/notifications"
|
|
||||||
"github.com/verstak/verstak-desktop/internal/core/plugin"
|
"github.com/verstak/verstak-desktop/internal/core/plugin"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/storage"
|
"github.com/verstak/verstak-desktop/internal/core/storage"
|
||||||
syncsvc "github.com/verstak/verstak-desktop/internal/core/sync"
|
syncsvc "github.com/verstak/verstak-desktop/internal/core/sync"
|
||||||
|
|
@ -30,44 +27,6 @@ import (
|
||||||
"github.com/verstak/verstak-desktop/internal/core/workspace"
|
"github.com/verstak/verstak-desktop/internal/core/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeNotificationScheduler struct {
|
|
||||||
replaceCalls int
|
|
||||||
clearCalls int
|
|
||||||
startCalls int
|
|
||||||
stopCalls int
|
|
||||||
pluginID string
|
|
||||||
requests []notifications.Request
|
|
||||||
onStart func()
|
|
||||||
onStop func()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fakeNotificationScheduler) Replace(pluginID string, requests []notifications.Request) error {
|
|
||||||
s.replaceCalls++
|
|
||||||
s.pluginID = pluginID
|
|
||||||
s.requests = append([]notifications.Request(nil), requests...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fakeNotificationScheduler) Clear(pluginID string) error {
|
|
||||||
s.clearCalls++
|
|
||||||
s.pluginID = pluginID
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fakeNotificationScheduler) Start(context.Context) {
|
|
||||||
s.startCalls++
|
|
||||||
if s.onStart != nil {
|
|
||||||
s.onStart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fakeNotificationScheduler) Stop() {
|
|
||||||
s.stopCalls++
|
|
||||||
if s.onStop != nil {
|
|
||||||
s.onStop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLocalHTTPTestServer(t *testing.T, handler http.Handler) *httptest.Server {
|
func newLocalHTTPTestServer(t *testing.T, handler http.Handler) *httptest.Server {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|
@ -118,18 +77,6 @@ func newTestApp(tmpRoot string) *App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNotificationsTestApp(manifest plugin.Manifest) (*App, *fakeNotificationScheduler) {
|
|
||||||
scheduler := &fakeNotificationScheduler{}
|
|
||||||
return &App{
|
|
||||||
plugins: []plugin.Plugin{{
|
|
||||||
Manifest: manifest,
|
|
||||||
Status: plugin.StatusLoaded,
|
|
||||||
Enabled: true,
|
|
||||||
}},
|
|
||||||
notifications: scheduler,
|
|
||||||
}, scheduler
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFilesTestApp(t *testing.T, perms []string) (*App, string) {
|
func newFilesTestApp(t *testing.T, perms []string) (*App, string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
v := vault.NewVault(nil)
|
v := vault.NewVault(nil)
|
||||||
|
|
@ -187,174 +134,6 @@ func newSyncFilesTestApp(t *testing.T, perms []string, deviceID string) (*App, s
|
||||||
return app, root
|
return app, root
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReplacePluginNotificationsRequiresCapabilityAndPermission(t *testing.T) {
|
|
||||||
requests := []notifications.Request{{ID: "reminder", DueAt: "2026-07-14T10:00:00Z", Title: "Reminder"}}
|
|
||||||
for _, manifest := range []plugin.Manifest{
|
|
||||||
{ID: "notifications.test", Permissions: []string{"notifications.schedule"}},
|
|
||||||
{ID: "notifications.test", Requires: []string{"verstak/core/notifications/v1"}},
|
|
||||||
} {
|
|
||||||
app, scheduler := newNotificationsTestApp(manifest)
|
|
||||||
if got := app.ReplacePluginNotifications(manifest.ID, requests); got == "" {
|
|
||||||
t.Fatalf("manifest %#v unexpectedly scheduled notifications", manifest)
|
|
||||||
}
|
|
||||||
if scheduler.replaceCalls != 0 {
|
|
||||||
t.Fatalf("rejected request called scheduler %d times", scheduler.replaceCalls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReplaceAndClearPluginNotificationsStayWithinPluginNamespace(t *testing.T) {
|
|
||||||
manifest := plugin.Manifest{
|
|
||||||
ID: "notifications.test",
|
|
||||||
Permissions: []string{"notifications.schedule"},
|
|
||||||
Requires: []string{"verstak/core/notifications/v1"},
|
|
||||||
}
|
|
||||||
app, scheduler := newNotificationsTestApp(manifest)
|
|
||||||
requests := []notifications.Request{{ID: "reminder", DueAt: "2026-07-14T10:00:00Z", Title: "Reminder"}}
|
|
||||||
if got := app.ReplacePluginNotifications(manifest.ID, requests); got != "" {
|
|
||||||
t.Fatalf("ReplacePluginNotifications error = %q", got)
|
|
||||||
}
|
|
||||||
if scheduler.replaceCalls != 1 || scheduler.pluginID != manifest.ID || len(scheduler.requests) != 1 {
|
|
||||||
t.Fatalf("scheduler state = %#v", scheduler)
|
|
||||||
}
|
|
||||||
if got := app.ClearPluginNotifications(manifest.ID); got != "" {
|
|
||||||
t.Fatalf("ClearPluginNotifications error = %q", got)
|
|
||||||
}
|
|
||||||
if scheduler.clearCalls != 1 || scheduler.pluginID != manifest.ID {
|
|
||||||
t.Fatalf("clear state = %#v", scheduler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDomReadyInitializesNotificationsBeforeScheduler(t *testing.T) {
|
|
||||||
oldInitialize := initializeNativeNotifications
|
|
||||||
defer func() { initializeNativeNotifications = oldInitialize }()
|
|
||||||
|
|
||||||
order := []string{}
|
|
||||||
initializeNativeNotifications = func(context.Context) error {
|
|
||||||
order = append(order, "initialize")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
app, scheduler := newNotificationsTestApp(plugin.Manifest{ID: "notifications.test"})
|
|
||||||
scheduler.onStart = func() { order = append(order, "start") }
|
|
||||||
|
|
||||||
app.DomReady(context.Background())
|
|
||||||
|
|
||||||
if got, want := strings.Join(order, ","), "initialize,start"; got != want {
|
|
||||||
t.Fatalf("notification startup order = %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
if scheduler.startCalls != 1 {
|
|
||||||
t.Fatalf("scheduler start calls = %d, want 1", scheduler.startCalls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDomReadyDoesNotStartSchedulerWhenNotificationInitializationFails(t *testing.T) {
|
|
||||||
oldInitialize := initializeNativeNotifications
|
|
||||||
defer func() { initializeNativeNotifications = oldInitialize }()
|
|
||||||
|
|
||||||
initializeNativeNotifications = func(context.Context) error { return fmt.Errorf("unavailable") }
|
|
||||||
app, scheduler := newNotificationsTestApp(plugin.Manifest{ID: "notifications.test"})
|
|
||||||
|
|
||||||
app.DomReady(context.Background())
|
|
||||||
|
|
||||||
if scheduler.startCalls != 0 {
|
|
||||||
t.Fatalf("scheduler start calls = %d, want 0 when native notifications are unavailable", scheduler.startCalls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShutdownStopsSchedulerBeforeCleaningUpNotifications(t *testing.T) {
|
|
||||||
oldCleanup := cleanupNativeNotifications
|
|
||||||
defer func() { cleanupNativeNotifications = oldCleanup }()
|
|
||||||
|
|
||||||
order := []string{}
|
|
||||||
cleanupNativeNotifications = func(context.Context) { order = append(order, "cleanup") }
|
|
||||||
app, scheduler := newNotificationsTestApp(plugin.Manifest{ID: "notifications.test"})
|
|
||||||
scheduler.onStop = func() { order = append(order, "stop") }
|
|
||||||
|
|
||||||
app.Shutdown(context.Background())
|
|
||||||
|
|
||||||
if got, want := strings.Join(order, ","), "stop,cleanup"; got != want {
|
|
||||||
t.Fatalf("notification shutdown order = %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
if scheduler.stopCalls != 1 {
|
|
||||||
t.Fatalf("scheduler stop calls = %d, want 1", scheduler.stopCalls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNativeNotificationSenderUsesStablePluginScopedID(t *testing.T) {
|
|
||||||
oldSend := sendNativeNotification
|
|
||||||
defer func() { sendNativeNotification = oldSend }()
|
|
||||||
|
|
||||||
var gotTitle, gotBody, gotID string
|
|
||||||
sendNativeNotification = func(_ context.Context, options runtime.NotificationOptions) error {
|
|
||||||
gotID = options.ID
|
|
||||||
gotTitle = options.Title
|
|
||||||
gotBody = options.Body
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := NewNativeNotificationSender().Send(context.Background(), notifications.Item{
|
|
||||||
PluginID: "verstak.todo",
|
|
||||||
ID: "task-42",
|
|
||||||
Title: "Todo reminder",
|
|
||||||
Body: "Prepare alpha release",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Send() error = %v", err)
|
|
||||||
}
|
|
||||||
if gotID != "verstak:verstak.todo:task-42" || gotTitle != "Todo reminder" || gotBody != "Prepare alpha release" {
|
|
||||||
t.Fatalf("native notification = id=%q title=%q body=%q", gotID, gotTitle, gotBody)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBeforeCloseHidesWindowUntilUserChoosesQuit(t *testing.T) {
|
|
||||||
oldHide := hideNativeWindow
|
|
||||||
defer func() { hideNativeWindow = oldHide }()
|
|
||||||
|
|
||||||
hideCalls := 0
|
|
||||||
hideNativeWindow = func(context.Context) { hideCalls++ }
|
|
||||||
app := &App{}
|
|
||||||
|
|
||||||
if prevent := app.BeforeClose(context.Background()); !prevent {
|
|
||||||
t.Fatal("BeforeClose() = false, want true while tray mode is active")
|
|
||||||
}
|
|
||||||
if hideCalls != 1 {
|
|
||||||
t.Fatalf("hide calls = %d, want 1", hideCalls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTrayQuitAllowsWindowCloseAndQuitsApplication(t *testing.T) {
|
|
||||||
oldQuit := quitNativeApplication
|
|
||||||
defer func() { quitNativeApplication = oldQuit }()
|
|
||||||
|
|
||||||
quitCalls := 0
|
|
||||||
quitNativeApplication = func(context.Context) { quitCalls++ }
|
|
||||||
app := &App{ctx: context.Background()}
|
|
||||||
|
|
||||||
app.Quit()
|
|
||||||
|
|
||||||
if quitCalls != 1 {
|
|
||||||
t.Fatalf("quit calls = %d, want 1", quitCalls)
|
|
||||||
}
|
|
||||||
if prevent := app.BeforeClose(context.Background()); prevent {
|
|
||||||
t.Fatal("BeforeClose() = true after Quit(), want false")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShowWindowUsesWailsContext(t *testing.T) {
|
|
||||||
oldShow := showNativeWindow
|
|
||||||
defer func() { showNativeWindow = oldShow }()
|
|
||||||
|
|
||||||
showCalls := 0
|
|
||||||
showNativeWindow = func(context.Context) { showCalls++ }
|
|
||||||
app := &App{ctx: context.Background()}
|
|
||||||
|
|
||||||
app.ShowWindow()
|
|
||||||
|
|
||||||
if showCalls != 1 {
|
|
||||||
t.Fatalf("show calls = %d, want 1", showCalls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetPluginFrontendInfo_KnownPluginWithFrontend verifies that
|
// TestGetPluginFrontendInfo_KnownPluginWithFrontend verifies that
|
||||||
// GetPluginFrontendInfo returns correct metadata for a plugin with a frontend.
|
// GetPluginFrontendInfo returns correct metadata for a plugin with a frontend.
|
||||||
func TestGetPluginFrontendInfo_KnownPluginWithFrontend(t *testing.T) {
|
func TestGetPluginFrontendInfo_KnownPluginWithFrontend(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -1,254 +0,0 @@
|
||||||
// Package notifications persists plugin-owned reminder schedules and delivers
|
|
||||||
// due entries through a desktop-native sender.
|
|
||||||
package notifications
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxRequestsPerPlugin = 500
|
|
||||||
maxIDLength = 256
|
|
||||||
maxTitleLength = 512
|
|
||||||
maxBodyLength = 4096
|
|
||||||
)
|
|
||||||
|
|
||||||
// VaultPath provides a stable operation while a vault is open.
|
|
||||||
type VaultPath interface {
|
|
||||||
WithOpenPath(func(string) error) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request is a desired scheduled notification in a plugin's own namespace.
|
|
||||||
type Request struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
DueAt string `json:"dueAt"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Body string `json:"body,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Item is the persisted form of a plugin notification schedule.
|
|
||||||
type Item struct {
|
|
||||||
PluginID string `json:"pluginId"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
DueAt string `json:"dueAt"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Body string `json:"body,omitempty"`
|
|
||||||
SentForDueAt string `json:"sentForDueAt,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sender delivers one native desktop notification.
|
|
||||||
type Sender interface {
|
|
||||||
Send(context.Context, Item) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manager owns canonical schedules for every authorized plugin.
|
|
||||||
type Manager struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
vault VaultPath
|
|
||||||
sender Sender
|
|
||||||
now func() time.Time
|
|
||||||
|
|
||||||
items []Item
|
|
||||||
loaded bool
|
|
||||||
stop chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a notification manager. The supplied clock is used by tests; a
|
|
||||||
// nil clock uses the current time.
|
|
||||||
func New(vault VaultPath, sender Sender, now func() time.Time) *Manager {
|
|
||||||
if now == nil {
|
|
||||||
now = time.Now
|
|
||||||
}
|
|
||||||
return &Manager{vault: vault, sender: sender, now: now}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace atomically replaces one plugin's desired schedules.
|
|
||||||
func (m *Manager) Replace(pluginID string, requests []Request) error {
|
|
||||||
if err := validateRequests(pluginID, requests); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
if err := m.loadLocked(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
previous := make(map[string]Item)
|
|
||||||
kept := make([]Item, 0, len(m.items)+len(requests))
|
|
||||||
for _, item := range m.items {
|
|
||||||
if item.PluginID == pluginID {
|
|
||||||
previous[item.ID] = item
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
kept = append(kept, item)
|
|
||||||
}
|
|
||||||
for _, request := range requests {
|
|
||||||
item := Item{
|
|
||||||
PluginID: pluginID,
|
|
||||||
ID: request.ID,
|
|
||||||
DueAt: request.DueAt,
|
|
||||||
Title: request.Title,
|
|
||||||
Body: request.Body,
|
|
||||||
}
|
|
||||||
if old, ok := previous[request.ID]; ok && old.DueAt == request.DueAt {
|
|
||||||
item.SentForDueAt = old.SentForDueAt
|
|
||||||
}
|
|
||||||
kept = append(kept, item)
|
|
||||||
}
|
|
||||||
sortItems(kept)
|
|
||||||
if err := m.writeLocked(kept); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.items = kept
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear removes every schedule owned by a plugin.
|
|
||||||
func (m *Manager) Clear(pluginID string) error {
|
|
||||||
return m.Replace(pluginID, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Items returns a snapshot of the current schedule state.
|
|
||||||
func (m *Manager) Items() []Item {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
if err := m.loadLocked(); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return append([]Item(nil), m.items...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path reports the canonical schedule path while the vault is open.
|
|
||||||
func (m *Manager) Path() string {
|
|
||||||
if m == nil || m.vault == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
var path string
|
|
||||||
if err := m.vault.WithOpenPath(func(vaultPath string) error {
|
|
||||||
path = schedulePath(vaultPath)
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start immediately checks due schedules, then repeats every 30 seconds.
|
|
||||||
func (m *Manager) Start(ctx context.Context) {
|
|
||||||
if m == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.mu.Lock()
|
|
||||||
if m.stop != nil {
|
|
||||||
m.mu.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
stop := make(chan struct{})
|
|
||||||
m.stop = stop
|
|
||||||
m.mu.Unlock()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
m.Tick(ctx)
|
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
m.Tick(ctx)
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop stops a running scheduler. It is safe to call repeatedly.
|
|
||||||
func (m *Manager) Stop() {
|
|
||||||
if m == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
if m.stop == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
close(m.stop)
|
|
||||||
m.stop = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tick delivers each overdue schedule once. A sender failure leaves the item
|
|
||||||
// pending for a later tick.
|
|
||||||
func (m *Manager) Tick(ctx context.Context) {
|
|
||||||
if m == nil || m.sender == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
if err := m.loadLocked(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
now := m.now().UTC()
|
|
||||||
for index, item := range m.items {
|
|
||||||
dueAt, err := time.Parse(time.RFC3339, item.DueAt)
|
|
||||||
if err != nil || dueAt.After(now) || item.SentForDueAt == item.DueAt {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := m.sender.Send(ctx, item); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
updated := append([]Item(nil), m.items...)
|
|
||||||
updated[index].SentForDueAt = item.DueAt
|
|
||||||
if err := m.writeLocked(updated); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m.items = updated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateRequests(pluginID string, requests []Request) error {
|
|
||||||
if strings.TrimSpace(pluginID) == "" || strings.ContainsAny(pluginID, "/\\") {
|
|
||||||
return fmt.Errorf("invalid plugin ID %q", pluginID)
|
|
||||||
}
|
|
||||||
if len(requests) > maxRequestsPerPlugin {
|
|
||||||
return fmt.Errorf("plugin %q has more than %d notification schedules", pluginID, maxRequestsPerPlugin)
|
|
||||||
}
|
|
||||||
seen := make(map[string]bool, len(requests))
|
|
||||||
for _, request := range requests {
|
|
||||||
if request.ID == "" || len(request.ID) > maxIDLength {
|
|
||||||
return fmt.Errorf("notification ID is empty or too long")
|
|
||||||
}
|
|
||||||
if seen[request.ID] {
|
|
||||||
return fmt.Errorf("duplicate notification ID %q", request.ID)
|
|
||||||
}
|
|
||||||
seen[request.ID] = true
|
|
||||||
if !strings.HasSuffix(request.DueAt, "Z") {
|
|
||||||
return fmt.Errorf("notification %q dueAt must be UTC RFC3339", request.ID)
|
|
||||||
}
|
|
||||||
if _, err := time.Parse(time.RFC3339, request.DueAt); err != nil {
|
|
||||||
return fmt.Errorf("notification %q has invalid dueAt: %w", request.ID, err)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(request.Title) == "" || len(request.Title) > maxTitleLength {
|
|
||||||
return fmt.Errorf("notification %q title is empty or too long", request.ID)
|
|
||||||
}
|
|
||||||
if len(request.Body) > maxBodyLength {
|
|
||||||
return fmt.Errorf("notification %q body is too long", request.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sortItems(items []Item) {
|
|
||||||
sort.Slice(items, func(left, right int) bool {
|
|
||||||
if items[left].PluginID != items[right].PluginID {
|
|
||||||
return items[left].PluginID < items[right].PluginID
|
|
||||||
}
|
|
||||||
return items[left].ID < items[right].ID
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
package notifications
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type testVault struct {
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v testVault) WithOpenPath(operation func(string) error) error {
|
|
||||||
return operation(v.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeSender struct {
|
|
||||||
err error
|
|
||||||
items []Item
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fakeSender) Send(_ context.Context, item Item) error {
|
|
||||||
s.items = append(s.items, item)
|
|
||||||
return s.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func testManager(t *testing.T, sender *fakeSender, now time.Time) *Manager {
|
|
||||||
t.Helper()
|
|
||||||
return New(testVault{path: t.TempDir()}, sender, func() time.Time { return now })
|
|
||||||
}
|
|
||||||
|
|
||||||
func request(id, dueAt string) Request {
|
|
||||||
return Request{ID: id, DueAt: dueAt, Title: "Reminder " + id, Body: "Todo " + id}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReplaceRemovesStalePluginSchedules(t *testing.T) {
|
|
||||||
manager := testManager(t, &fakeSender{}, time.Date(2026, 7, 14, 9, 0, 0, 0, time.UTC))
|
|
||||||
if err := manager.Replace("verstak.todo", []Request{
|
|
||||||
request("first", "2026-07-14T10:00:00Z"),
|
|
||||||
request("second", "2026-07-14T11:00:00Z"),
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("initial replace: %v", err)
|
|
||||||
}
|
|
||||||
if err := manager.Replace("verstak.todo", []Request{request("second", "2026-07-14T11:00:00Z")}); err != nil {
|
|
||||||
t.Fatalf("stale-item replace: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
items := manager.Items()
|
|
||||||
if len(items) != 1 || items[0].PluginID != "verstak.todo" || items[0].ID != "second" {
|
|
||||||
t.Fatalf("items = %#v, want only second Todo reminder", items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReplaceReschedulesDeliveredItem(t *testing.T) {
|
|
||||||
now := time.Date(2026, 7, 14, 12, 0, 0, 0, time.UTC)
|
|
||||||
sender := &fakeSender{}
|
|
||||||
manager := testManager(t, sender, now)
|
|
||||||
if err := manager.Replace("verstak.todo", []Request{request("todo-1", "2026-07-14T11:00:00Z")}); err != nil {
|
|
||||||
t.Fatalf("initial replace: %v", err)
|
|
||||||
}
|
|
||||||
manager.Tick(context.Background())
|
|
||||||
if len(sender.items) != 1 || manager.Items()[0].SentForDueAt == "" {
|
|
||||||
t.Fatalf("first delivery = %#v, state = %#v", sender.items, manager.Items())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := manager.Replace("verstak.todo", []Request{request("todo-1", "2026-07-14T13:00:00Z")}); err != nil {
|
|
||||||
t.Fatalf("reschedule: %v", err)
|
|
||||||
}
|
|
||||||
if got := manager.Items()[0].SentForDueAt; got != "" {
|
|
||||||
t.Fatalf("rescheduled item kept old acknowledgment %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTickRetriesFailedSendAndAcknowledgesOneDelivery(t *testing.T) {
|
|
||||||
now := time.Date(2026, 7, 14, 12, 0, 0, 0, time.UTC)
|
|
||||||
sender := &fakeSender{err: errors.New("notification unavailable")}
|
|
||||||
manager := testManager(t, sender, now)
|
|
||||||
if err := manager.Replace("verstak.todo", []Request{request("todo-1", "2026-07-14T11:00:00Z")}); err != nil {
|
|
||||||
t.Fatalf("replace: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.Tick(context.Background())
|
|
||||||
if len(sender.items) != 1 || manager.Items()[0].SentForDueAt != "" {
|
|
||||||
t.Fatalf("failed send was acknowledged: sent=%d items=%#v", len(sender.items), manager.Items())
|
|
||||||
}
|
|
||||||
|
|
||||||
sender.err = nil
|
|
||||||
manager.Tick(context.Background())
|
|
||||||
manager.Tick(context.Background())
|
|
||||||
if len(sender.items) != 2 {
|
|
||||||
t.Fatalf("send attempts = %d, want 2", len(sender.items))
|
|
||||||
}
|
|
||||||
if got := manager.Items()[0].SentForDueAt; got != "2026-07-14T11:00:00Z" {
|
|
||||||
t.Fatalf("sent acknowledgment = %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPersistentSchedulesReloadAndDeliverOverdueOnlyOnce(t *testing.T) {
|
|
||||||
path := t.TempDir()
|
|
||||||
now := time.Date(2026, 7, 14, 12, 0, 0, 0, time.UTC)
|
|
||||||
first := New(testVault{path: path}, &fakeSender{}, func() time.Time { return now })
|
|
||||||
if err := first.Replace("verstak.todo", []Request{request("todo-1", "2026-07-14T11:00:00Z")}); err != nil {
|
|
||||||
t.Fatalf("replace: %v", err)
|
|
||||||
}
|
|
||||||
if got, want := filepath.Join(path, ".verstak", "notifications", "schedules.json"), first.Path(); got != want {
|
|
||||||
t.Fatalf("schedule path = %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
sender := &fakeSender{}
|
|
||||||
restarted := New(testVault{path: path}, sender, func() time.Time { return now })
|
|
||||||
restarted.Tick(context.Background())
|
|
||||||
restarted.Tick(context.Background())
|
|
||||||
if len(sender.items) != 1 || sender.items[0].ID != "todo-1" {
|
|
||||||
t.Fatalf("overdue delivery = %#v", sender.items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReplaceRejectsInvalidRequests(t *testing.T) {
|
|
||||||
manager := testManager(t, &fakeSender{}, time.Now().UTC())
|
|
||||||
for _, requests := range [][]Request{
|
|
||||||
{{ID: "", DueAt: "2026-07-14T10:00:00Z", Title: "title"}},
|
|
||||||
{{ID: "same", DueAt: "2026-07-14T10:00:00Z", Title: "title"}, {ID: "same", DueAt: "2026-07-14T11:00:00Z", Title: "title"}},
|
|
||||||
{{ID: "bad-date", DueAt: "not-a-date", Title: "title"}},
|
|
||||||
} {
|
|
||||||
if err := manager.Replace("verstak.todo", requests); err == nil {
|
|
||||||
t.Fatalf("Replace(%#v) succeeded", requests)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
package notifications
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type storedSchedules struct {
|
|
||||||
Items []Item `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func schedulePath(vaultPath string) string {
|
|
||||||
return filepath.Join(vaultPath, ".verstak", "notifications", "schedules.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) loadLocked() error {
|
|
||||||
if m.loaded {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if m.vault == nil {
|
|
||||||
return fmt.Errorf("notification vault is not initialized")
|
|
||||||
}
|
|
||||||
var loaded []Item
|
|
||||||
if err := m.vault.WithOpenPath(func(vaultPath string) error {
|
|
||||||
path := schedulePath(vaultPath)
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
loaded = []Item{}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("read notification schedules: %w", err)
|
|
||||||
}
|
|
||||||
var stored storedSchedules
|
|
||||||
if err := json.Unmarshal(data, &stored); err != nil {
|
|
||||||
return fmt.Errorf("decode notification schedules: %w", err)
|
|
||||||
}
|
|
||||||
for _, item := range stored.Items {
|
|
||||||
if err := validateRequests(item.PluginID, []Request{{ID: item.ID, DueAt: item.DueAt, Title: item.Title, Body: item.Body}}); err != nil {
|
|
||||||
return fmt.Errorf("invalid stored notification schedule: %w", err)
|
|
||||||
}
|
|
||||||
loaded = append(loaded, item)
|
|
||||||
}
|
|
||||||
sortItems(loaded)
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.items = loaded
|
|
||||||
m.loaded = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) writeLocked(items []Item) error {
|
|
||||||
if m.vault == nil {
|
|
||||||
return fmt.Errorf("notification vault is not initialized")
|
|
||||||
}
|
|
||||||
return m.vault.WithOpenPath(func(vaultPath string) error {
|
|
||||||
data, err := json.MarshalIndent(storedSchedules{Items: items}, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("encode notification schedules: %w", err)
|
|
||||||
}
|
|
||||||
path := schedulePath(vaultPath)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
||||||
return fmt.Errorf("create notification schedule directory: %w", err)
|
|
||||||
}
|
|
||||||
temporary := filepath.Join(filepath.Dir(path), fmt.Sprintf(".schedules.%d.tmp", time.Now().UnixNano()))
|
|
||||||
if err := os.WriteFile(temporary, data, 0o644); err != nil {
|
|
||||||
return fmt.Errorf("write notification schedules: %w", err)
|
|
||||||
}
|
|
||||||
if err := os.Rename(temporary, path); err != nil {
|
|
||||||
_ = os.Remove(temporary)
|
|
||||||
return fmt.Errorf("replace notification schedules: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -39,7 +39,6 @@ func (r *Registry) registerDefaults() {
|
||||||
{Name: "files.openExternal", Description: "Open vault files and folders in external OS applications", Dangerous: true},
|
{Name: "files.openExternal", Description: "Open vault files and folders in external OS applications", Dangerous: true},
|
||||||
{Name: "storage.namespace", Description: "Read/write plugin's own storage namespace", Dangerous: false},
|
{Name: "storage.namespace", Description: "Read/write plugin's own storage namespace", Dangerous: false},
|
||||||
{Name: "storage.migrations", Description: "Run database migrations in plugin namespace", Dangerous: false},
|
{Name: "storage.migrations", Description: "Run database migrations in plugin namespace", Dangerous: false},
|
||||||
{Name: "notifications.schedule", Description: "Schedule native notifications in the plugin's own namespace", Dangerous: false},
|
|
||||||
{Name: "events.publish", Description: "Publish events to the event bus", Dangerous: false},
|
{Name: "events.publish", Description: "Publish events to the event bus", Dangerous: false},
|
||||||
{Name: "events.subscribe", Description: "Subscribe to events on the event bus", Dangerous: false},
|
{Name: "events.subscribe", Description: "Subscribe to events on the event bus", Dangerous: false},
|
||||||
{Name: "ui.register", Description: "Register UI components and contributions", Dangerous: false},
|
{Name: "ui.register", Description: "Register UI components and contributions", Dangerous: false},
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
// Package tray owns the native tray menu wiring for the desktop shell.
|
|
||||||
package tray
|
|
||||||
|
|
||||||
import "sync"
|
|
||||||
|
|
||||||
// MenuItem exposes click events from a native tray menu item.
|
|
||||||
type MenuItem interface {
|
|
||||||
Clicked() <-chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backend is the platform tray implementation.
|
|
||||||
type Backend interface {
|
|
||||||
Register(onReady func(), onExit func())
|
|
||||||
SetIcon(icon []byte)
|
|
||||||
SetTooltip(tooltip string)
|
|
||||||
AddMenuItem(title, tooltip string) MenuItem
|
|
||||||
Quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions are executed from the native tray menu.
|
|
||||||
type Actions struct {
|
|
||||||
Show func()
|
|
||||||
Quit func()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Controller initializes one native tray and routes its menu actions.
|
|
||||||
type Controller struct {
|
|
||||||
backend Backend
|
|
||||||
icon []byte
|
|
||||||
start sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a tray controller for one application process.
|
|
||||||
func New(backend Backend, icon []byte) *Controller {
|
|
||||||
return &Controller{
|
|
||||||
backend: backend,
|
|
||||||
icon: append([]byte(nil), icon...),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start registers the native tray without taking over the Wails event loop.
|
|
||||||
func (c *Controller) Start(actions Actions) {
|
|
||||||
if c == nil || c.backend == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.start.Do(func() {
|
|
||||||
c.backend.Register(func() {
|
|
||||||
c.backend.SetIcon(c.icon)
|
|
||||||
c.backend.SetTooltip("Verstak")
|
|
||||||
show := c.backend.AddMenuItem("Show Verstak", "Show the Verstak window")
|
|
||||||
quit := c.backend.AddMenuItem("Quit", "Quit Verstak")
|
|
||||||
if actions.Show != nil && show != nil {
|
|
||||||
go routeClicks(show.Clicked(), actions.Show)
|
|
||||||
}
|
|
||||||
if actions.Quit != nil && quit != nil {
|
|
||||||
go routeClicks(quit.Clicked(), actions.Quit)
|
|
||||||
}
|
|
||||||
}, nil)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop releases the native tray after Wails has begun application shutdown.
|
|
||||||
func (c *Controller) Stop() {
|
|
||||||
if c == nil || c.backend == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.backend.Quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func routeClicks(clicked <-chan struct{}, action func()) {
|
|
||||||
for range clicked {
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
package tray
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fakeMenuItem struct {
|
|
||||||
clicked chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *fakeMenuItem) Clicked() <-chan struct{} {
|
|
||||||
return i.clicked
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeBackend struct {
|
|
||||||
icon []byte
|
|
||||||
tooltip string
|
|
||||||
items map[string]*fakeMenuItem
|
|
||||||
quitCalls int
|
|
||||||
registering bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *fakeBackend) Register(onReady func(), _ func()) {
|
|
||||||
b.registering = true
|
|
||||||
onReady()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *fakeBackend) SetIcon(icon []byte) {
|
|
||||||
b.icon = append([]byte(nil), icon...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *fakeBackend) SetTooltip(tooltip string) {
|
|
||||||
b.tooltip = tooltip
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *fakeBackend) AddMenuItem(title, _ string) MenuItem {
|
|
||||||
item := &fakeMenuItem{clicked: make(chan struct{}, 1)}
|
|
||||||
b.items[title] = item
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *fakeBackend) Quit() {
|
|
||||||
b.quitCalls++
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitFor(t *testing.T, signal <-chan struct{}) {
|
|
||||||
t.Helper()
|
|
||||||
select {
|
|
||||||
case <-signal:
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatal("timed out waiting for tray action")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestControllerInitializesTrayAndRoutesMenuActions(t *testing.T) {
|
|
||||||
backend := &fakeBackend{items: make(map[string]*fakeMenuItem)}
|
|
||||||
showCalls := make(chan struct{}, 1)
|
|
||||||
quitCalls := make(chan struct{}, 1)
|
|
||||||
controller := New(backend, []byte{1, 2, 3})
|
|
||||||
|
|
||||||
controller.Start(Actions{
|
|
||||||
Show: func() { showCalls <- struct{}{} },
|
|
||||||
Quit: func() { quitCalls <- struct{}{} },
|
|
||||||
})
|
|
||||||
|
|
||||||
if !backend.registering || string(backend.icon) != string([]byte{1, 2, 3}) || backend.tooltip != "Verstak" {
|
|
||||||
t.Fatalf("tray initialization = registering:%t icon:%v tooltip:%q", backend.registering, backend.icon, backend.tooltip)
|
|
||||||
}
|
|
||||||
showItem := backend.items["Show Verstak"]
|
|
||||||
quitItem := backend.items["Quit"]
|
|
||||||
if showItem == nil || quitItem == nil {
|
|
||||||
t.Fatalf("tray menu = %#v, want Show Verstak and Quit", backend.items)
|
|
||||||
}
|
|
||||||
|
|
||||||
showItem.clicked <- struct{}{}
|
|
||||||
waitFor(t, showCalls)
|
|
||||||
quitItem.clicked <- struct{}{}
|
|
||||||
waitFor(t, quitCalls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestControllerStopsNativeTrayBackend(t *testing.T) {
|
|
||||||
backend := &fakeBackend{items: make(map[string]*fakeMenuItem)}
|
|
||||||
controller := New(backend, []byte{1})
|
|
||||||
|
|
||||||
controller.Stop()
|
|
||||||
|
|
||||||
if backend.quitCalls != 1 {
|
|
||||||
t.Fatalf("backend quit calls = %d, want 1", backend.quitCalls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package tray
|
|
||||||
|
|
||||||
import "encoding/base64"
|
|
||||||
|
|
||||||
const iconPNGBase64 = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACT1BMVEUAAACOjo7///8RERHY2NghISEDAwOjo6MODg7GxsYQEBDIyMgPDw/Hx8cKCgq9vb12dnaHh4fFxcV1dXWjo6OoqKinp6eNjY25ubnr6+v29vb29vb29vbx8fHV1dXm5ub9/f3z8/OUlJQAAADu7u74+Pi5ubkAAADu7u74+Pi6urq6urrs7Oz39/ezs7PZ2dn5+fn8/Pzq6uobGxtdXV3U1NTl5eXl5eXl5eXm5ubd3d2urq7////+/v7//v7//v/s7OzLy8vLy8ru7u708/POzc3Kysrd3d39/f3Z2dnU1NT7+/u0tLQpKSkoKCcnJycsLCy+vr63t7crKyskJCRzc3NQUFAlJSUoKChvb28gICAdHR0cHBzDwsJ6enoaGhkZGRltbW3MzMwqKioeHh6pqam+vb0iIiJGRkYbGxseHR0aGhpubm6cnJw6Ojrg4ODExMQmJiYeHh36+vppaWhxcXH8/PwvLy/a2tmKiorl5eU+Pj6zs7PQ0NA1NTXR0dFSUlIbGhq/v79BQUHn5+fW1tYuLi48PDyysrItLS02NjaNjY17e3v///7b29syMjJ/f38hISEqKSlqampaWlq9vb3f398cGxs9PT1KSko3NzeioqJRUVFJSUnk5OQaGRljY2OGhobp6ek/Pz+dnZ3IyMgoKSjHx8ZDQ0PHx8fx8fHw8PBVVVX39/cdHh2RkZHz8/NPT0+Ojo7Dw8PPz8/29vZUVFTCwsL19fX4+PhYWFgWFhYXFxfAwMCqqqqFhYWHhoaEhITe3t6MjIzi4uIwLXXJAAAAPHRSTlMAAAAAAAAAAAAAAAAAAAAAAAAABRIWFQoUis3R0K82b/22DQGZ2x0Cm9wfHo/UGULi+IIEAzZudHNzUA0lLC8OAAAAAWJLR0QCZgt8ZAAAAAd0SU1FB+oHDRIyN3EuEOkAAAHzSURBVDjLvdPXXxNBEAdwPHvvFewde8WyG3NGT41KlFxMwoyKguLeCCZRUCzRgIXYUexEjR17713/MPfCB+Vy8ZXfw+3tzvdlbueyslolbfr26z8gQwYqbVP1doMGD8nOsSU7Z+gwpb2sd1CGj2CZM3JURwk6jR4j37l5wDlvejrkssjBxo7rLEGX8RPk1qmqi1UXW7JU05bx5Ss0VXOvZBNzu/4Fq1bne9asZQVej+7jrnX+gB6UYNI/UFAIqK9nGzZC0Sa2uRihZAuzgK2lAB7VtU0YtJ2VIWD5DisI+QG87lAYIhTcuYsiVFFpBVo+4O49VQKAKvbuE1C6XzbWEhwoBowePBSDEvJV+0HUHE4DR44iHquN43G9yHdCROgkP2UFp8+gqDkLdcFzdL6ewH9Bfi8LuBjFS5ev0NVrAdEQRZFwpoPr5QiB8I2b1WHw1oFIMkcauHVbGAB4565sF+De/dS9tATOB8IwMO5uNEGs4aENPEpIQI+fhHR4CvjMrFvB8xcxA8RL9kqHCMZf28Gbt2RAoJG9k4Dq39vBh4+f6HPCxb4UElFZqm4FlV+/ff9Ry9nPqmTy128L6GYCR/OwNY9eE8jtLkEPZTL/z9BOmdrTHHtl2nSeMTNmzuplgt6z58zNm2dL3vwFC/u0yo/7B9C02RGfGBOeAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI2LTA3LTEzVDE0OjIyOjMzKzAwOjAwJHU36gAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNi0wNy0xM1QxNDoyMjozMyswMDowMFUoj1YAAAAASUVORK5CYII="
|
|
||||||
|
|
||||||
// DefaultIcon returns the compact source-controlled Verstak tray icon.
|
|
||||||
func DefaultIcon() []byte {
|
|
||||||
icon, err := base64.StdEncoding.DecodeString(iconPNGBase64)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return icon
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
package tray
|
|
||||||
|
|
||||||
import "github.com/getlantern/systray"
|
|
||||||
|
|
||||||
type systrayBackend struct{}
|
|
||||||
|
|
||||||
type systrayMenuItem struct {
|
|
||||||
item *systray.MenuItem
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewNativeBackend creates the cross-platform native tray backend.
|
|
||||||
func NewNativeBackend() Backend {
|
|
||||||
return systrayBackend{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (systrayBackend) Register(onReady func(), onExit func()) {
|
|
||||||
systray.Register(onReady, onExit)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (systrayBackend) SetIcon(icon []byte) {
|
|
||||||
systray.SetIcon(icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (systrayBackend) SetTooltip(tooltip string) {
|
|
||||||
systray.SetTooltip(tooltip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (systrayBackend) AddMenuItem(title, tooltip string) MenuItem {
|
|
||||||
return systrayMenuItem{item: systray.AddMenuItem(title, tooltip)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (systrayBackend) Quit() {
|
|
||||||
systray.Quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (item systrayMenuItem) Clicked() <-chan struct{} {
|
|
||||||
if item.item == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return item.item.ClickedCh
|
|
||||||
}
|
|
||||||
22
main.go
22
main.go
|
|
@ -1,11 +1,9 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"embed"
|
"embed"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2"
|
"github.com/wailsapp/wails/v2"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options"
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
|
|
@ -18,7 +16,6 @@ import (
|
||||||
"github.com/verstak/verstak-desktop/internal/core/contribution"
|
"github.com/verstak/verstak-desktop/internal/core/contribution"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/events"
|
"github.com/verstak/verstak-desktop/internal/core/events"
|
||||||
corefiles "github.com/verstak/verstak-desktop/internal/core/files"
|
corefiles "github.com/verstak/verstak-desktop/internal/core/files"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/notifications"
|
|
||||||
"github.com/verstak/verstak-desktop/internal/core/permissions"
|
"github.com/verstak/verstak-desktop/internal/core/permissions"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/plugin"
|
"github.com/verstak/verstak-desktop/internal/core/plugin"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/pluginstate"
|
"github.com/verstak/verstak-desktop/internal/core/pluginstate"
|
||||||
|
|
@ -27,7 +24,6 @@ import (
|
||||||
"github.com/verstak/verstak-desktop/internal/core/vault"
|
"github.com/verstak/verstak-desktop/internal/core/vault"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/workspace"
|
"github.com/verstak/verstak-desktop/internal/core/workspace"
|
||||||
"github.com/verstak/verstak-desktop/internal/shell/debug"
|
"github.com/verstak/verstak-desktop/internal/shell/debug"
|
||||||
"github.com/verstak/verstak-desktop/internal/shell/tray"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed frontend/dist
|
//go:embed frontend/dist
|
||||||
|
|
@ -95,7 +91,6 @@ func main() {
|
||||||
"verstak/core/events/v1",
|
"verstak/core/events/v1",
|
||||||
"verstak/core/files/v1",
|
"verstak/core/files/v1",
|
||||||
"verstak/core/workbench/v1",
|
"verstak/core/workbench/v1",
|
||||||
"verstak/core/notifications/v1",
|
|
||||||
}
|
}
|
||||||
if err := capRegistry.Register(corePluginID, coreCaps); err != nil {
|
if err := capRegistry.Register(corePluginID, coreCaps); err != nil {
|
||||||
log.Fatalf("[main] failed to register core capabilities: %v", err)
|
log.Fatalf("[main] failed to register core capabilities: %v", err)
|
||||||
|
|
@ -237,8 +232,6 @@ func main() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
app = api.NewApp(capRegistry, contribRegistry, permRegistry, eventBus, plugins, vaultService, storageService, filesService, appSettingsMgr, pluginStateMgr, workspaceMgr, syncService, browserReceiver, debugEnabled)
|
app = api.NewApp(capRegistry, contribRegistry, permRegistry, eventBus, plugins, vaultService, storageService, filesService, appSettingsMgr, pluginStateMgr, workspaceMgr, syncService, browserReceiver, debugEnabled)
|
||||||
app.SetNotificationService(notifications.New(vaultService, api.NewNativeNotificationSender(), time.Now))
|
|
||||||
trayController := tray.New(tray.NewNativeBackend(), tray.DefaultIcon())
|
|
||||||
if browserReceiver != nil {
|
if browserReceiver != nil {
|
||||||
browserReceiverServer, err := browserreceiver.Start(browserreceiver.DefaultAddr, browserReceiver)
|
browserReceiverServer, err := browserreceiver.Start(browserreceiver.DefaultAddr, browserReceiver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -258,21 +251,6 @@ func main() {
|
||||||
MinHeight: 600,
|
MinHeight: 600,
|
||||||
WindowStartState: options.Normal,
|
WindowStartState: options.Normal,
|
||||||
OnStartup: app.Startup,
|
OnStartup: app.Startup,
|
||||||
OnDomReady: func(ctx context.Context) {
|
|
||||||
app.DomReady(ctx)
|
|
||||||
trayController.Start(tray.Actions{Show: app.ShowWindow, Quit: app.Quit})
|
|
||||||
},
|
|
||||||
OnShutdown: func(ctx context.Context) {
|
|
||||||
trayController.Stop()
|
|
||||||
app.Shutdown(ctx)
|
|
||||||
},
|
|
||||||
OnBeforeClose: app.BeforeClose,
|
|
||||||
SingleInstanceLock: &options.SingleInstanceLock{
|
|
||||||
UniqueId: "605fba28-7cbf-4f14-9d1b-a4da0c1723f8",
|
|
||||||
OnSecondInstanceLaunch: func(options.SecondInstanceData) {
|
|
||||||
app.ShowWindow()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
AssetServer: &assetserver.Options{
|
AssetServer: &assetserver.Options{
|
||||||
Assets: assets,
|
Assets: assets,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ Version: @VERSION@
|
||||||
Section: utils
|
Section: utils
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Architecture: amd64
|
Architecture: amd64
|
||||||
Depends: libwebkit2gtk-4.1-0, libgtk-3-0t64 | libgtk-3-0, libayatana-appindicator3-1
|
Depends: libwebkit2gtk-4.1-0, libgtk-3-0t64 | libgtk-3-0
|
||||||
Maintainer: Verstak contributors <dev@verstak.app>
|
Maintainer: Verstak contributors <dev@verstak.app>
|
||||||
Homepage: https://github.com/mirivlad/verstak
|
Homepage: https://github.com/mirivlad/verstak
|
||||||
Description: Local-first workspace
|
Description: Local-first workspace
|
||||||
|
|
|
||||||
|
|
@ -26,15 +26,6 @@ if ! command -v npm &>/dev/null; then
|
||||||
fi
|
fi
|
||||||
echo " ✅ npm $(npm --version)"
|
echo " ✅ npm $(npm --version)"
|
||||||
|
|
||||||
if [[ "$(go env GOOS)" == "linux" ]]; then
|
|
||||||
if ! command -v pkg-config &>/dev/null || ! pkg-config --exists ayatana-appindicator3-0.1; then
|
|
||||||
echo " ❌ native tray development files are missing"
|
|
||||||
echo " Debian/Ubuntu: sudo apt install libayatana-appindicator3-dev"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo " ✅ ayatana-appindicator3 $(pkg-config --modversion ayatana-appindicator3-0.1)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Frontend (build first — Go //go:embed needs frontend/dist/) ──
|
# ── Frontend (build first — Go //go:embed needs frontend/dist/) ──
|
||||||
echo ""
|
echo ""
|
||||||
echo "[frontend]"
|
echo "[frontend]"
|
||||||
|
|
|
||||||
|
|
@ -105,11 +105,6 @@ while IFS= read -r candidate; do
|
||||||
done < "$APPDIR/.elf-queue"
|
done < "$APPDIR/.elf-queue"
|
||||||
rm -f "$APPDIR/.elf-queue"
|
rm -f "$APPDIR/.elf-queue"
|
||||||
|
|
||||||
if ! compgen -G "$APPDIR/usr/lib/libayatana-appindicator3.so.*" >/dev/null; then
|
|
||||||
echo "native tray runtime library was not bundled" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v glib-compile-schemas >/dev/null; then
|
if command -v glib-compile-schemas >/dev/null; then
|
||||||
glib-compile-schemas "$APPDIR/usr/share/glib-2.0/schemas"
|
glib-compile-schemas "$APPDIR/usr/share/glib-2.0/schemas"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,8 @@ grep -Fq 'dpkg-deb' "$ROOT/scripts/package-deb.sh"
|
||||||
grep -Fq -- '--build' "$ROOT/scripts/package-deb.sh"
|
grep -Fq -- '--build' "$ROOT/scripts/package-deb.sh"
|
||||||
grep -Fq 'packaging/deb/control' "$ROOT/scripts/package-deb.sh"
|
grep -Fq 'packaging/deb/control' "$ROOT/scripts/package-deb.sh"
|
||||||
grep -Fq 'libwebkit2gtk-4.1-0' "$ROOT/packaging/deb/control"
|
grep -Fq 'libwebkit2gtk-4.1-0' "$ROOT/packaging/deb/control"
|
||||||
grep -Fq 'libayatana-appindicator3-1' "$ROOT/packaging/deb/control"
|
|
||||||
grep -Fq 'appimagetool' "$ROOT/scripts/package-appimage.sh"
|
grep -Fq 'appimagetool' "$ROOT/scripts/package-appimage.sh"
|
||||||
grep -Fq 'WebKitWebProcess' "$ROOT/scripts/package-appimage.sh"
|
grep -Fq 'WebKitWebProcess' "$ROOT/scripts/package-appimage.sh"
|
||||||
grep -Fq 'libayatana-appindicator3.so.' "$ROOT/scripts/package-appimage.sh"
|
|
||||||
grep -Fq 'ayatana-appindicator3-0.1' "$ROOT/scripts/build.sh"
|
|
||||||
if grep -Fq 'FixedVersionRuntime' "$ROOT/scripts/package-windows-portable.sh"; then
|
if grep -Fq 'FixedVersionRuntime' "$ROOT/scripts/package-windows-portable.sh"; then
|
||||||
echo "Windows portable archive must not bundle Fixed Version WebView2" >&2
|
echo "Windows portable archive must not bundle Fixed Version WebView2" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -39,7 +36,6 @@ grep -Fq 'WebView2 Runtime' "$ROOT/README.md"
|
||||||
grep -Fq 'package-deb.sh' "$ROOT/scripts/release.sh"
|
grep -Fq 'package-deb.sh' "$ROOT/scripts/release.sh"
|
||||||
grep -Fq 'package-appimage.sh' "$ROOT/scripts/release.sh"
|
grep -Fq 'package-appimage.sh' "$ROOT/scripts/release.sh"
|
||||||
grep -Fq 'package-windows-portable.sh' "$ROOT/scripts/release.sh"
|
grep -Fq 'package-windows-portable.sh' "$ROOT/scripts/release.sh"
|
||||||
git -C "$ROOT" check-ignore -q verstak-desktop-res.syso
|
|
||||||
grep -Fq 'chmod -R a+rX' "$ROOT/scripts/build-linux-bundle.sh"
|
grep -Fq 'chmod -R a+rX' "$ROOT/scripts/build-linux-bundle.sh"
|
||||||
|
|
||||||
echo "desktop package script contracts passed"
|
echo "desktop package script contracts passed"
|
||||||
|
|
|
||||||
|
|
@ -39,18 +39,6 @@ OUTPUT=$(cd "$ROOT" && go test -count=1 -v ./... 2>&1) || GO_TEST_STATUS=$?
|
||||||
echo "$OUTPUT" | grep -E '(FAIL|PASS|---)' || true
|
echo "$OUTPUT" | grep -E '(FAIL|PASS|---)' || true
|
||||||
report "go test" "$GO_TEST_STATUS"
|
report "go test" "$GO_TEST_STATUS"
|
||||||
|
|
||||||
WAILS_BINDINGS_STATUS=0
|
|
||||||
(cd "$ROOT" && node frontend/tests/wails-bindings-test.mjs) || WAILS_BINDINGS_STATUS=$?
|
|
||||||
report "Wails notification bindings" "$WAILS_BINDINGS_STATUS"
|
|
||||||
|
|
||||||
DEBUG_MODE_STATUS=0
|
|
||||||
(cd "$ROOT" && node --experimental-vm-modules frontend/tests/debug-mode-test.mjs) || DEBUG_MODE_STATUS=$?
|
|
||||||
report "session-only debug mode" "$DEBUG_MODE_STATUS"
|
|
||||||
|
|
||||||
SELECT_STYLE_STATUS=0
|
|
||||||
(cd "$ROOT" && node frontend/tests/select-styles-test.mjs) || SELECT_STYLE_STATUS=$?
|
|
||||||
report "workspace select styles" "$SELECT_STYLE_STATUS"
|
|
||||||
|
|
||||||
# ── Frontend tests ──
|
# ── Frontend tests ──
|
||||||
echo "[frontend]"
|
echo "[frontend]"
|
||||||
if ensure_npm_deps "$ROOT/frontend"; then
|
if ensure_npm_deps "$ROOT/frontend"; then
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue