14 Commits
7 changed files with 106 additions and 41 deletions
+12
View File
@@ -0,0 +1,12 @@
## Releasing new releases
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
- Create new GitHub release using your new version number as the "Tag version".
Use the exact version number, don't include a prefix `v`.
See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments.
Note: The `manifest.json` file must be in two places, first the root path of your repository and also in the release.
- Publish the release.
From <https://github.com/obsidianmd/obsidian-sample-plugin/blob/master/README.md#releasing-new-releases>
+14
View File
@@ -0,0 +1,14 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
syntax-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: node --check main.js
+25
View File
@@ -0,0 +1,25 @@
name: Release
on:
release:
types: [published]
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
env:
REPO_NAME: ${{ github.event.repository.name}}
steps:
- uses: actions/checkout@v4
- name: Create zip archive
run: |
zip -j "${REPO_NAME}.zip" main.js manifest.json
- name: Upload release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | #shell
gh release upload "${{ github.event.release.tag_name }}" \
main.js manifest.json "${REPO_NAME}.zip"
+6 -4
View File
@@ -1,6 +1,6 @@
# Ribbon Order Lock # Obsidian Ribbon Clasp
Obsidian plugin that stops the left ribbon icons from reshuffling themselves. An [Obsidian][^1] plugin that stops the left ribbon icons from reshuffling themselves.
The plugin should work completely transparently, but it relies on undocumented The plugin should work completely transparently, but it relies on undocumented
internals within Obsidian's `workspace.leftRibbon` object, so some future app internals within Obsidian's `workspace.leftRibbon` object, so some future app
@@ -8,9 +8,11 @@ update could break it. Please open an issue if the ribbon ever stops behaving.
## Install ## Install
**BRAT**: `bdeshi/obsidian-ribbon-order-lock` **BRAT**: `bdeshi/obsidian-ribbon-clasp`
**Manual**: extract the latest release archive inside **Manual**: extract the latest release archive inside
`.obsidian/plugins/ribbon-order-lock/`, and enable it. `.obsidian/plugins/ribbon-clasp/`, and enable it.
If you're using lazy-plugins, set this plugin's startup type to "Instant". If you're using lazy-plugins, set this plugin's startup type to "Instant".
[^1]: https://obsidian.md/
+41 -33
View File
@@ -1,36 +1,26 @@
const { Plugin } = require('obsidian'); const { Plugin, debounce } = require('obsidian');
// a bunch of ribbon icons can attach within milliseconds of each other on module.exports = class RibbonClasp extends Plugin {
// startup, so debounce instead of re-checking the order on every single one
function debounce(fn, wait) {
let timer = null;
return () => {
clearTimeout(timer);
timer = setTimeout(fn, wait);
};
}
module.exports = class RibbonOrderLock extends Plugin {
async onload() { async onload() {
this.settings = Object.assign({ order: [] }, await this.loadData()); this.settings = Object.assign({ order: [] }, await this.loadData());
this.enforcing = false; this.enforcing = false;
this.debouncedEnforce = debounce(() => this.enforceOrder(), 200); this.alive = true;
this.register(() => { this.alive = false; });
// icons can attach within milliseconds of each other on startup
// (resetTimer, or it fires 200ms after the first one instead of the last)
this.debouncedEnforce = debounce(() => this.enforceOrder(), 200, true);
// disconnecting the observer doesn't stop an already-scheduled call
this.register(() => this.debouncedEnforce.cancel());
this.app.workspace.onLayoutReady(() => { this.app.workspace.onLayoutReady(() => {
// not tied to our lifecycle; still fires if we were disabled during boot
if (!this.alive) return;
this.patchRibbon(); this.patchRibbon();
this.enforceOrder(); this.enforceOrder();
this.observeRibbon(); this.observeRibbon();
}); });
} }
onunload() {
this.observer?.disconnect();
const ribbon = this.getRibbon();
if (ribbon && this.originalOnChange) {
ribbon.onChange = this.originalOnChange;
}
}
getRibbon() { getRibbon() {
return this.app.workspace.leftRibbon ?? null; return this.app.workspace.leftRibbon ?? null;
} }
@@ -45,8 +35,9 @@ module.exports = class RibbonOrderLock extends Plugin {
const containerEl = this.getRibbonContainerEl(ribbon); const containerEl = this.getRibbonContainerEl(ribbon);
if (!containerEl) return; if (!containerEl) return;
this.observer = new MutationObserver(() => this.debouncedEnforce()); const observer = new MutationObserver(() => this.debouncedEnforce());
this.observer.observe(containerEl, { childList: true }); observer.observe(containerEl, { childList: true });
this.register(() => observer.disconnect());
} }
// hook the ribbon's own onChange so drags and hide/show toggles update // hook the ribbon's own onChange so drags and hide/show toggles update
@@ -58,12 +49,18 @@ module.exports = class RibbonOrderLock extends Plugin {
const original = ribbon.onChange.bind(ribbon); const original = ribbon.onChange.bind(ribbon);
this.originalOnChange = original; this.originalOnChange = original;
ribbon.onChange = (save) => { const wrapper = (save) => {
original(save); original(save);
if (save && !this.enforcing) { if (save && !this.enforcing && this.alive) {
this.recordOrder(ribbon); this.recordOrder(ribbon);
} }
}; };
ribbon.onChange = wrapper;
this.register(() => {
// don't restore over someone else's patch, just go inert instead
if (ribbon.onChange === wrapper) ribbon.onChange = original;
});
} }
enforceOrder() { enforceOrder() {
@@ -76,13 +73,12 @@ module.exports = class RibbonOrderLock extends Plugin {
return; return;
} }
// anything we haven't seen before sorts to the bottom, in the order it came in
const rank = new Map(order.map((id, i) => [id, i]));
const sorted = [...ribbon.items].sort((a, b) => { const sorted = [...ribbon.items].sort((a, b) => {
const ia = order.indexOf(a.id); const ia = rank.get(a.id) ?? Infinity;
const ib = order.indexOf(b.id); const ib = rank.get(b.id) ?? Infinity;
if (ia === -1 && ib === -1) return 0; return ia === ib ? 0 : ia - ib;
if (ia === -1) return 1;
if (ib === -1) return -1;
return ia - ib;
}); });
const changed = sorted.some((item, i) => ribbon.items[i] !== item); const changed = sorted.some((item, i) => ribbon.items[i] !== item);
@@ -100,13 +96,25 @@ module.exports = class RibbonOrderLock extends Plugin {
this.mergeNewItems(ribbon); this.mergeNewItems(ribbon);
} }
// a drag saves, and the dom change it makes wakes the observer up, which can
// save again before the first one has finished writing. queue them instead.
save() {
this.writing = Promise.resolve(this.writing)
.catch(() => {})
.then(() => this.saveData(this.settings));
return this.writing;
}
recordOrder(ribbon) { recordOrder(ribbon) {
const ids = ribbon.items.map((item) => item.id); const ids = ribbon.items.map((item) => item.id);
if (this.sameOrder(ids, this.settings.order)) return; if (this.sameOrder(ids, this.settings.order)) return;
this.settings.order = ids; this.settings.order = ids;
this.saveData(this.settings); this.save();
} }
// ids of plugins that are gone stay in the list on purpose -- nothing prunes
// them, so reinstalling one puts its icon back where it used to be
//
// only append icons we haven't seen before instead of overwriting the // only append icons we haven't seen before instead of overwriting the
// whole order -- enforceOrder() runs during boot too, before every // whole order -- enforceOrder() runs during boot too, before every
// plugin has attached its icon yet, and a full overwrite there was // plugin has attached its icon yet, and a full overwrite there was
@@ -116,7 +124,7 @@ module.exports = class RibbonOrderLock extends Plugin {
const newIds = ribbon.items.map((item) => item.id).filter((id) => !known.has(id)); const newIds = ribbon.items.map((item) => item.id).filter((id) => !known.has(id));
if (newIds.length === 0) return; if (newIds.length === 0) return;
this.settings.order = [...this.settings.order, ...newIds]; this.settings.order = [...this.settings.order, ...newIds];
this.saveData(this.settings); this.save();
} }
sameOrder(a, b) { sameOrder(a, b) {
+4 -4
View File
@@ -1,9 +1,9 @@
{ {
"id": "ribbon-order-lock", "id": "ribbon-clasp",
"name": "Ribbon Order Lock", "name": "Ribbon Clasp",
"version": "0.9.0", "version": "1.1.0",
"minAppVersion": "1.1.0", "minAppVersion": "1.1.0",
"description": "Keeps left-ribbon icon order stable across restarts", "description": "Stops the left ribbon icons from reshuffling themselves.",
"author": "bdeshi", "author": "bdeshi",
"authorUrl": "https://github.com/bdeshi", "authorUrl": "https://github.com/bdeshi",
"isDesktopOnly": false "isDesktopOnly": false
+4
View File
@@ -0,0 +1,4 @@
{
"1.0.0": "1.1.0",
"1.1.0": "1.1.0"
}