Initial release: Better Bujo v0.1.0

This commit is contained in:
Sebastien Delisle 2026-06-08 16:34:59 -04:00
commit 811b0b89d8
17 changed files with 5656 additions and 0 deletions

11
.editorconfig Normal file
View file

@ -0,0 +1,11 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
quote_type = single
tab_width = 4

27
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Node.js build
on:
push:
branches: ['**']
pull_request:
branches: ['**']
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x, 22.x, 24.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
- run: npm run lint

50
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,50 @@
name: Release Obsidian plugin
on:
push:
tags:
- '*'
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@v6
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- name: Build plugin
run: |
npm ci
npm run build
- name: Check for optional styles
id: styles
run: |
[ -f styles.css ] && echo "exists=true" >> "$GITHUB_OUTPUT" || echo "exists=false" >> "$GITHUB_OUTPUT"
- name: Attest build provenance
uses: actions/attest-build-provenance@v2
with:
subject-path: |
main.js
${{ steps.styles.outputs.exists == 'true' && 'styles.css' || '' }}
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
gh release create "$tag" \
--title="$tag" \
--draft \
main.js manifest.json ${{ steps.styles.outputs.exists == 'true' && 'styles.css' || '' }}

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
node_modules/
main.js
data.json
*.js.map
.DS_Store
**/.DS_Store

2
.npmrc Normal file
View file

@ -0,0 +1,2 @@
tag-version-prefix=""
legacy-peer-deps=true

21
LICENSE Normal file
View file

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

48
README.md Normal file
View file

@ -0,0 +1,48 @@
# Better Bujo
Render bullet-journal (BuJo) markers in your notes instead of Obsidian's
checkboxes — in both **Reading mode** and **Live Preview**.
## Markers
At the beginning of a line:
| You type | Renders as | Meaning |
| ---------- | ---------- | ----------------------------- |
| `- item` | `` | Plain note (a dash, not a bullet) |
| `- [ ]` | `•` | Open task |
| `- [x]` | `x` | Done |
| `- [>]` | `>` | Migrated to the month note |
| `- [<]` | `<` | Sent to the future log |
| `- [o]` | `⊙` | Event |
| `- [O]` | `⊗` | Completed event |
| `~ ...` | `~` (styled) | An emotion or a thought |
## How it works
The marker glyphs are pure CSS, keyed on the `data-task` attribute Obsidian
sets on each list item, so they render identically in Reading mode and Live
Preview. Emotion lines (`~ …`) are tagged by a small markdown post-processor
and a CodeMirror extension so they can be styled.
All styling is scoped under a `better-bujo` body class — disabling the
plugin restores Obsidian's native rendering.
## Acknowledgements
Inspired by [obsidian-bujo-bullets](https://github.com/frankolson/obsidian-bujo-bullets) by Frank Olson — a great starting point for BuJo-style checkboxes in Obsidian.
## My other Obsidian plugins
- **[Date List](https://github.com/lumargh/obsidian-date-list)** — Returns a list of dates according to the conditions you supply.
- **[Calendar List](https://github.com/lumargh/obsidian-calendar-list)** — Insert macOS Calendar events into your notes.
- **[File Filter](https://github.com/lumargh/obsidian-file-filter)** — Filter your pages and sidebar by a search term; everything else fades away.
## Development
```bash
npm install
npm run dev # watch build
npm run build # production build + type-check
npm run lint
```

49
esbuild.config.mjs Normal file
View file

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

33
eslint.config.mts Normal file
View file

@ -0,0 +1,33 @@
import tseslint from 'typescript-eslint';
import obsidianmd from 'eslint-plugin-obsidianmd';
import globals from 'globals';
import { globalIgnores } from 'eslint/config';
export default tseslint.config(
globalIgnores([
'node_modules',
'dist',
'esbuild.config.mjs',
'version-bump.mjs',
'versions.json',
'main.js',
'package.json',
'package-lock.json',
'tsconfig.json',
]),
{
languageOptions: {
globals: {
...globals.browser,
},
parserOptions: {
projectService: {
allowDefaultProject: ['eslint.config.mts', 'manifest.json'],
},
tsconfigRootDir: import.meta.dirname,
extraFileExtensions: ['.json'],
},
},
},
...obsidianmd.configs.recommended,
);

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "better-bujo",
"name": "Better Bujo",
"version": "0.1.0",
"minAppVersion": "1.0.0",
"description": "Render bullet-journal markers (events, migration, future log, emotions) instead of checkboxes.",
"author": "Sebastien",
"authorUrl": "https://github.com/lumargh",
"isDesktopOnly": false
}

5040
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

31
package.json Normal file
View file

@ -0,0 +1,31 @@
{
"name": "better-bujo",
"version": "0.1.0",
"description": "Render bullet-journal markers (events, migration, future log, emotions) instead of checkboxes.",
"main": "main.js",
"type": "module",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"lint": "eslint ."
},
"keywords": [],
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/node": "^22.15.17",
"esbuild": "0.25.5",
"eslint": "^9.39.4",
"eslint-plugin-obsidianmd": "^0.3.0",
"globals": "^17.6.0",
"jiti": "^2.6.1",
"obsidian": "latest",
"typescript": "^5.8.3",
"typescript-eslint": "^8.59.1"
}
}

83
src/main.ts Normal file
View file

@ -0,0 +1,83 @@
// TODO
// Add right-click functionality on all bullets. Reveals menu that allows users to change a bullet's type.
// Clicking an open event turns it into a completed event. Clicking a completed event toggles back to open event.
import { Plugin } from 'obsidian';
import { RangeSetBuilder } from '@codemirror/state';
import {
Decoration,
DecorationSet,
EditorView,
PluginValue,
ViewPlugin,
ViewUpdate,
} from '@codemirror/view';
// A line that conveys an emotion or a thought, e.g. `~ feeling restless`.
// The `~` glyph renders as itself; the plugin only marks the line so styles.css
// can give it a distinct (muted/italic) look. The actual marker glyphs for
// tasks/events/migration are handled entirely in styles.css, keyed on the
// `data-task` attribute Obsidian sets on each list item — see that file for the
// canonical glyph table.
const EMOTION_LINE = /^~\s/;
// ---- Live Preview / Source mode: tag `~ ` lines for styling. -----------------
const emotionLine = Decoration.line({ class: 'bb-emotion' });
class EmotionView implements PluginValue {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.build(view);
}
update(update: ViewUpdate): void {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.build(update.view);
}
}
private build(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
for (const { from, to } of view.visibleRanges) {
let pos = from;
while (pos <= to) {
const line = view.state.doc.lineAt(pos);
if (EMOTION_LINE.test(line.text)) {
builder.add(line.from, line.from, emotionLine);
}
pos = line.to + 1;
}
}
return builder.finish();
}
}
const emotionExtension = ViewPlugin.fromClass(EmotionView, {
decorations: (value) => value.decorations,
});
export default class BetterBujoPlugin extends Plugin {
onload(): void {
// All glyph styling is scoped under this class so disabling the plugin
// fully reverts to Obsidian's native rendering.
activeDocument.body.classList.add('better-bujo');
// Reading mode: tag `~ ` paragraphs so styles.css can style them.
this.registerMarkdownPostProcessor((el) => {
for (const p of Array.from(el.querySelectorAll('p'))) {
if (EMOTION_LINE.test(p.textContent ?? '')) {
p.addClass('bb-emotion');
}
}
});
// Live Preview / Source mode equivalent.
this.registerEditorExtension(emotionExtension);
}
onunload(): void {
activeDocument.body.classList.remove('better-bujo');
}
}

209
styles.css Normal file
View file

@ -0,0 +1,209 @@
/* Better Bujo bullet-journal markers in place of Obsidian's checkboxes.
*
* Canonical glyph table (keyed on the `data-task` attribute Obsidian sets on
* each list item, in both Reading mode and Live Preview):
*
* - [ ] open task (a bullet point)
* - [x] done x
* - [>] migrated (month) >
* - [<] future log <
* - [o] event
* - [O] event done
* - plain list item (en dash, not a bullet)
* ~ emotion/thought styled line (the `~` glyph stays as typed)
*
* Every rule is scoped under `.better-bujo` (added to <body> on load) so
* disabling the plugin restores Obsidian's native rendering. */
.better-bujo {
--bb-marker-color: var(--text-muted);
--bb-event-color: var(--text-accent);
--bb-done-color: var(--text-faint);
/* Horizontal nudge that lines the Live Preview en dash up with the task /
event marker column. Cancels the `padding-inline-start` Obsidian puts on
`.cm-formatting-list-ul` (= --list-indent-editing, 0.75em by default).
Tweak if your theme's list metrics differ. */
--bb-lp-dash-shift: -1.1em;
}
/* ===========================================================================
* Task / event markers replace the checkbox with a glyph.
* The glyph is emitted via ::before; the native <input> is hidden.
* ======================================================================== */
/* --- Reading mode -------------------------------------------------------- */
.better-bujo .markdown-rendered li.task-list-item {
list-style: none;
}
.better-bujo .markdown-rendered li.task-list-item input.task-list-item-checkbox {
display: none;
}
.better-bujo .markdown-rendered li.task-list-item::before {
content: "•"; /* open task — the default for any unrecognised marker */
display: inline-block;
width: 1.5em;
/* margin-left: -0.2em;
margin-right: 0.3em;
padding-right: 1em !important; */
text-align: center;
color: var(--bb-marker-color);
font-variant-numeric: tabular-nums;
}
.better-bujo .markdown-rendered li.task-list-item[data-task="x"]::before,
.better-bujo .markdown-rendered li.task-list-item[data-task="X"]::before {
content: "\2A2F"; /* */
color: var(--bb-done-color);
font-size: 1.3em;
}
.better-bujo .markdown-rendered li.task-list-item[data-task=">"]::before {
content: ">";
}
.better-bujo .markdown-rendered li.task-list-item[data-task="<"]::before {
content: "<";
}
.better-bujo .markdown-rendered li.task-list-item[data-task="o"]::before {
content: "\2299"; /* ⊙ */
/* color: var(--bb-event-color); */
}
.better-bujo .markdown-rendered li.task-list-item[data-task="O"]::before {
content: "\2297"; /* ⊗ */
/* color: var(--bb-done-color); */
}
.better-bujo .markdown-rendered li.task-list-item[data-task="~"]::before {
content: "~";
color: var(--bb-marker-color);
}
/* Plain "- " items reuse the exact same marker box as tasks, so the dash
shares the one marker column. Hide Obsidian's bullet disc and draw a dash. */
.better-bujo .markdown-rendered ul > li:not(.task-list-item) {
list-style: none;
}
.better-bujo .markdown-rendered ul > li:not(.task-list-item) > .list-bullet {
display: none;
}
.better-bujo .markdown-rendered ul > li:not(.task-list-item)::before {
content: "\2013"; /* */
display: inline-block;
width: 1.5em;
text-align: center;
color: var(--bb-marker-color);
font-variant-numeric: tabular-nums;
}
/* --- Live Preview / Source mode ------------------------------------------ */
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line .task-list-label > input.task-list-item-checkbox {
display: none;
}
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line .task-list-label::before {
content: "•"; /* open task — default */
display: inline-block;
width: 1.2em;
text-align: center;
color: var(--bb-marker-color);
font-variant-numeric: tabular-nums;
margin-right: 0.4em;
}
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="x"] .task-list-label::before,
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="X"] .task-list-label::before {
content: "\2A2F"; /* */
/* color: var(--bb-done-color); */
font-size: 1.7em;
margin-top: -1em;
margin-right: 0em;
margin-bottom: -1em;
padding-bottom: -1em;
margin-left: -0.2em;
}
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task=">"] .task-list-label::before {
content: ">";
}
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="<"] .task-list-label::before {
content: "<";
}
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="o"] .task-list-label::before {
content: "\2299"; /* ⊙ */
font-size: 1.4em !important;
margin-left: -0.15em;
margin-right: 0.1em;
margin-top: -1.1em;
/* color: var(--bb-event-color); */
}
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="O"] .task-list-label::before {
content: "\2297"; /* ⊗ */
/* color: var(--bb-done-color); */
}
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="~"] .task-list-label::before {
content: "~";
color: var(--bb-marker-color);
}
/* Non-standard markers (>, <, o, O, ~) are not a checkbox state, so clicking
them shouldn't toggle anything. The hidden <input> still forwards clicks via
its <label>, so make those labels non-interactive a click just places the
cursor. The standard open/done pair ([ ] [x]) stays clickable. */
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task=">"] .task-list-label,
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="<"] .task-list-label,
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="o"] .task-list-label,
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="O"] .task-list-label,
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="~"] .task-list-label {
pointer-events: none;
}
/* ===========================================================================
* Events ( / ) share one colour, size and font so the open and completed
* markers match in both modes.
* ======================================================================== */
.better-bujo .markdown-rendered li.task-list-item[data-task="o"]::before,
.better-bujo .markdown-rendered li.task-list-item[data-task="O"]::before,
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="o"] .task-list-label::before,
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="O"] .task-list-label::before {
/* color: var(--bb-event-color); */
font-family: var(--font-text);
font-size: 1em;
line-height: 1;
}
/* ===========================================================================
* Plain "- " list items en dash instead of a bullet (Live Preview).
*
* Obsidian draws the bullet as a disc on `.list-bullet::after` (content is a
* zero-width space; the dot is `background-color` + `border-radius`). We
* neutralise that disc and put an en dash in its place rather than appending,
* then shift the dash into the marker column. (Reading mode is handled above
* via the shared marker box.)
* ======================================================================== */
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-list-line:not(.HyperMD-task-line) .list-bullet::after {
content: "\2013"; /* */
/* Relative positioning shifts only the dash glyph into the marker column;
the line's text stays put (a margin here is absorbed by the parent's
padding-inline-start, which is why it had no effect). */
position: relative;
inset-inline-start: var(--bb-lp-dash-shift);
margin-right: -0.5em; /* less negative = more space before the text */
width: auto;
height: auto;
border: none;
border-radius: 0;
background-color: transparent;
transform: none;
color: var(--bb-marker-color);
}
/* ===========================================================================
* Emotion / thought lines ("~ …"). Tagged with .bb-emotion by the plugin.
* ======================================================================== */
.better-bujo .bb-emotion {
font-style: italic;
color: var(--text-muted);
}

19
tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES2021",
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"moduleResolution": "node",
"isolatedModules": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"lib": ["ES2021", "DOM"]
},
"include": ["src/**/*.ts"]
}

14
version-bump.mjs Normal file
View file

@ -0,0 +1,14 @@
import { readFileSync, writeFileSync } from 'fs';
const targetVersion = process.env.npm_package_version;
const manifest = JSON.parse(readFileSync('manifest.json', 'utf8'));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync('manifest.json', JSON.stringify(manifest, null, '\t'));
const versions = JSON.parse(readFileSync('versions.json', 'utf8'));
if (!(targetVersion in versions)) {
versions[targetVersion] = minAppVersion;
writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));
}

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"0.1.0": "1.0.0"
}