Add time event style setting, hand-drawn cancelled strikethrough

This commit is contained in:
Sebastien Delisle 2026-06-30 16:48:36 -04:00
parent daed00bed3
commit 2182b528e3
5 changed files with 188 additions and 78 deletions

View file

@ -1,7 +1,7 @@
{
"id": "better-bujo",
"name": "Better Bujo",
"version": "0.4.0",
"version": "0.4.1",
"minAppVersion": "1.5.7",
"description": "Render bullet-journal markers (events, migration, future log, emotions) instead of checkboxes.",
"author": "Sebastien",

View file

@ -1,6 +1,6 @@
{
"name": "better-bujo",
"version": "0.4.0",
"version": "0.4.1",
"description": "Render bullet-journal markers (events, migration, future log, emotions) instead of checkboxes.",
"main": "main.js",
"type": "module",

View file

@ -1,7 +1,7 @@
import { App, Editor, MarkdownView, Menu, Plugin, PluginSettingTab, Setting } from 'obsidian';
import { RangeSetBuilder } from '@codemirror/state';
import { RangeSetBuilder, StateEffect } from '@codemirror/state';
import {
Decoration,
DecorationSet,
@ -131,17 +131,19 @@ const emotionExtension = ViewPlugin.fromClass(EmotionView, {
decorations: (value) => value.decorations,
});
// ---- Time-based events: `- [9:30 AM] …` → a clock on the bullet + the time. --
// ---- Time-based events: `- [9:30 AM] …` ----
// Group 1: the bullet lead. Group 2: the time inside the brackets.
const TIME_EVENT_LINE = /^(\s*(?:[-*+]|\d+[.)])\s+)\[(\d{1,2}:\d{2}\s*[AaPp][Mm])\]/;
// The same time bracket, anchored to the start of a list item's rendered text.
const TIME_EVENT_TEXT = /^\[(\d{1,2}:\d{2}\s*[AaPp][Mm])\]/;
const clockLine = Decoration.line({ class: 'bb-clock-line' });
// Dispatched to open editors when the timeEventStyle setting changes so the
// decorations rebuild without waiting for a doc/viewport event.
const refreshClockEffect = StateEffect.define<void>();
// Live Preview: replace the `[9:30 AM]` source with just the time; the clock
// glyph itself is drawn on the list bullet by styles.css.
// Live Preview: replace `[9:30 AM]` with just the time text when the style
// removes the brackets; the bullet glyph is drawn by styles.css.
class ClockWidget extends WidgetType {
constructor(private readonly time: string) {
super();
@ -156,59 +158,77 @@ class ClockWidget extends WidgetType {
}
}
class ClockView implements PluginValue {
decorations: DecorationSet;
function makeClockExtension(getStyle: () => TimeEventStyle) {
class ClockView implements PluginValue {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.build(view);
}
update(update: ViewUpdate): void {
if (update.docChanged || update.viewportChanged || update.selectionSet) {
this.decorations = this.build(update.view);
constructor(view: EditorView) {
this.decorations = this.build(view);
}
}
private build(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
const { selection } = view.state;
for (const { from, to } of view.visibleRanges) {
let pos = from;
while (pos <= to) {
const line = view.state.doc.lineAt(pos);
const match = TIME_EVENT_LINE.exec(line.text);
if (match) {
const time = match[2] ?? '';
const start = line.from + (match[1] ?? '').length;
const end = start + time.length + 2; // '[' + time + ']'
// Reveal the raw source while editing on the brackets.
const editing = selection.ranges.some((r) => r.from <= end && r.to >= start);
if (!editing) {
builder.add(line.from, line.from, clockLine);
builder.add(start, end, Decoration.replace({ widget: new ClockWidget(time) }));
}
}
pos = line.to + 1;
update(update: ViewUpdate): void {
if (
update.docChanged ||
update.viewportChanged ||
update.selectionSet ||
update.transactions.some((tr) => tr.effects.some((e) => e.is(refreshClockEffect)))
) {
this.decorations = this.build(update.view);
}
}
return builder.finish();
private build(view: EditorView): DecorationSet {
const style = getStyle();
const builder = new RangeSetBuilder<Decoration>();
const { selection } = view.state;
for (const { from, to } of view.visibleRanges) {
let pos = from;
while (pos <= to) {
const line = view.state.doc.lineAt(pos);
const match = TIME_EVENT_LINE.exec(line.text);
if (match) {
const time = match[2] ?? '';
const start = line.from + (match[1] ?? '').length;
const end = start + time.length + 2; // '[' + time + ']'
if (style === 'default') {
// Keep brackets visible; highlight with a mark span.
builder.add(start, end, Decoration.mark({ class: 'bb-clock-bracket' }));
} else {
// Reveal source while the cursor is on the brackets.
const editing = selection.ranges.some((r) => r.from <= end && r.to >= start);
if (!editing) {
const lineClass =
style === 'circle'
? 'bb-clock-line-circle'
: `bb-clock-line-${/p/i.test(time) ? 'pm' : 'am'}`;
builder.add(line.from, line.from, Decoration.line({ class: lineClass }));
builder.add(start, end, Decoration.replace({ widget: new ClockWidget(time) }));
}
}
}
pos = line.to + 1;
}
}
return builder.finish();
}
}
return ViewPlugin.fromClass(ClockView, { decorations: (v) => v.decorations });
}
const clockExtension = ViewPlugin.fromClass(ClockView, {
decorations: (value) => value.decorations,
});
type TimeEventStyle = 'default' | 'ampm' | 'circle';
interface BetterBujoSettings {
strikeDoneTasks: boolean;
dottedGrid: boolean;
gridSpacing: number;
timeEventStyle: TimeEventStyle;
}
const DEFAULT_SETTINGS: BetterBujoSettings = {
strikeDoneTasks: false,
dottedGrid: false,
gridSpacing: 20,
timeEventStyle: 'default',
};
export default class BetterBujoPlugin extends Plugin {
@ -299,8 +319,7 @@ export default class BetterBujoPlugin extends Plugin {
p.insertBefore(marker, first);
}
// Time-based events: tag `- [9:30 AM] …` items and drop the brackets
// so styles.css can draw a clock on the bullet, leaving the time text.
// Time-based events: render `- [9:30 AM] …` according to the setting.
for (const li of Array.from(el.querySelectorAll('li:not(.task-list-item)'))) {
const host = li.firstElementChild instanceof HTMLParagraphElement ? li.firstElementChild : li;
let node: ChildNode | null = host.firstChild;
@ -312,13 +331,28 @@ export default class BetterBujoPlugin extends Plugin {
if (!node || !match) {
continue;
}
li.addClass('bb-clock');
node.textContent = text.replace(TIME_EVENT_TEXT, match[1] ?? '');
const time = match[1] ?? '';
const full = match[0];
if (this.settings.timeEventStyle === 'default') {
// Keep brackets; wrap them in a highlight span.
node.textContent = text.slice(full.length);
const span = li.ownerDocument.createElement('span');
span.className = 'bb-clock-bracket';
span.textContent = full;
host.insertBefore(span, node);
} else {
li.addClass(
this.settings.timeEventStyle === 'circle'
? 'bb-clock-circle'
: /p/i.test(time) ? 'bb-clock-pm' : 'bb-clock-am'
);
node.textContent = text.replace(TIME_EVENT_TEXT, time);
}
}
});
// Live Preview / Source mode equivalents.
this.registerEditorExtension([emotionExtension, clockExtension]);
this.registerEditorExtension([emotionExtension, makeClockExtension(() => this.settings.timeEventStyle)]);
// A command per bullet type (so each can take a hotkey) plus a
// right-click submenu — both rewrite the selected line(s) to that type.
@ -358,6 +392,13 @@ export default class BetterBujoPlugin extends Plugin {
for (const doc of this.styledDocs) {
this.styleDoc(doc);
}
// Force live-preview clock decorations to rebuild in all open editors.
this.app.workspace.iterateAllLeaves((leaf) => {
if (leaf.view instanceof MarkdownView) {
const cm = (leaf.view.editor as unknown as { cm: EditorView }).cm;
cm.dispatch({ effects: refreshClockEffect.of(undefined) });
}
});
}
private styleDoc(doc: Document): void {
@ -498,5 +539,20 @@ class BetterBujoSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Time event style')
.setDesc('How to render time bullets like "- [9:30 am] …".')
.addDropdown((drop) =>
drop
.addOption('default', 'Dash — show [time] highlighted')
.addOption('ampm', 'Am/pm label on bullet')
.addOption('circle', 'Circle (like an event)')
.setValue(this.plugin.settings.timeEventStyle)
.onChange(async (value) => {
this.plugin.settings.timeEventStyle = value as TimeEventStyle;
await this.plugin.saveSettings();
})
);
}
}

View file

@ -14,8 +14,7 @@
* - [O] event done (same size as ; click cycles back)
* - [-] cancelled , marker and whole line struck through, faint
* - plain list item (en dash, not a bullet)
* - [9:30 AM] time event clock (hand-drawn clock on the bullet; the
* time stays as text, brackets dropped)
* - [9:30 AM] time event configurable: dash/AM-PM/circle
* ~ emotion/thought styled line (the `~` glyph stays as typed)
*
* Strategy: never remove Obsidian's native marker elements restyle them in
@ -43,11 +42,6 @@
done , so the slash is literally the 's first stroke at the same angle. */
--bb-stroke-w: max(1.5px, calc(var(--checkbox-size) * 0.1));
--bb-stroke-h: calc(var(--checkbox-size) * 0.62);
/* Hand-drawn clock face for time-based events (`- [9:30 AM] `); used as a
mask so it takes --bb-marker-color, and roughened further by --bb-rough.
The face is a wobbly path with uneven radii that overshoots where it
closes (the way a penned circle does), and the hands are slightly skew. */
--bb-clock-svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.3C16.8 3 21.2 7.6 21 12.2C20.8 16.9 16.5 21.3 11.8 21C7 20.7 2.8 16.4 3 11.6C3.2 7.1 7.4 3.5 12.7 3.1'/%3E%3Cpath d='M12.1 12.2L11.7 7.4'/%3E%3Cpath d='M12.1 12.2L15.7 13.8'/%3E%3C/svg%3E");
/* Wet-ink bleed: a tight dark halo plus a soft outer one and a faint
bottom-right pooling, so each glyph reads like ink soaked into paper
rather than a crisp printed mark. Reused by every marker below. */
@ -308,17 +302,36 @@
out an irrelevant task on paper. Mirrors Obsidian's native done styling,
which targets these same elements. The marker's ::after is absolutely
positioned, so the line's text-decoration doesn't propagate into it the
strike on the is applied explicitly below. */
strike across the full item is drawn via ::before instead, so the same
hand-drawn filter can be applied. */
.better-bujo .markdown-rendered li[data-task="-"],
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="-"] {
text-decoration: line-through;
text-decoration: none;
color: var(--bb-done-color);
position: relative;
}
/* Hand-drawn strikethrough: a thin bar absolutely centred on the line, warped
by the same rough filter as every other glyph. `rotate(-0.6deg)` gives a
barely-perceptible descending tilt, the way a hand-ruled line would. */
.better-bujo .markdown-rendered li[data-task="-"]::before,
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="-"]::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 50%;
height: max(2px, 0.09em);
transform: translateY(-50%) rotate(-0.6deg);
background-color: currentColor;
filter: var(--bb-rough);
pointer-events: none;
z-index: 1;
}
.better-bujo li[data-task="-"] > input.task-list-item-checkbox::after,
.better-bujo li[data-task="-"] > p > input.task-list-item-checkbox::after,
.better-bujo .HyperMD-task-line[data-task="-"] input.task-list-item-checkbox::after {
text-decoration: line-through;
color: var(--bb-done-color);
}
@ -395,34 +408,74 @@
}
/* ===========================================================================
* Time-based events ("- [9:30 AM] …"). The plugin tags the item (.bb-clock in
* Reading mode, .bb-clock-line in Live Preview) and drops the brackets; here we
* swap the dash on the bullet for a hand-drawn clock face. `inset: 0; margin:
* auto` centers it in the same bullet box the dash used.
* Time-based events ("- [9:30 AM] …").
*
* Three rendering styles, chosen in settings:
*
* default dash bullet unchanged; [time] wrapped in .bb-clock-bracket for
* a muted highlight, brackets kept visible.
* ampm brackets stripped; bullet replaced with a small "AM"/"PM" label
* (classes bb-clock-am / bb-clock-pm / bb-clock-line-am / -pm).
* circle brackets stripped; bullet replaced with an event-style ring
* (classes bb-clock-circle / bb-clock-line-circle).
*
* `inset: 0; margin: auto` centers the replacement in the bullet box used by
* the dash, so it aligns with every other marker.
* ======================================================================== */
.better-bujo .markdown-rendered li.bb-clock .list-bullet::after,
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-list-line.bb-clock-line .list-bullet::after {
/* "default" style: muted colour on the [time] bracket span. */
.better-bujo .bb-clock-bracket {
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
/* "ampm" style: shared layout for the AM/PM text label. */
.better-bujo .markdown-rendered li.bb-clock-am .list-bullet::after,
.better-bujo .markdown-rendered li.bb-clock-pm .list-bullet::after,
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-list-line.bb-clock-line-am .list-bullet::after,
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-list-line.bb-clock-line-pm .list-bullet::after {
position: absolute;
inset: 0;
margin: auto;
display: flex;
align-items: center;
justify-content: center;
width: var(--checkbox-size);
height: var(--checkbox-size);
font-size: calc(var(--checkbox-size) * 0.34);
font-weight: 700;
letter-spacing: -0.02em;
color: var(--bb-marker-color);
transform: rotate(-4deg);
filter: var(--bb-rough);
}
.better-bujo .markdown-rendered li.bb-clock-am .list-bullet::after,
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-list-line.bb-clock-line-am .list-bullet::after {
content: "AM";
}
.better-bujo .markdown-rendered li.bb-clock-pm .list-bullet::after,
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-list-line.bb-clock-line-pm .list-bullet::after {
content: "PM";
}
/* "circle" style: wobbly hand-drawn ring, matching the event ○ proportions. */
.better-bujo .markdown-rendered li.bb-clock-circle .list-bullet::after,
.better-bujo .markdown-source-view.mod-cm6 .HyperMD-list-line.bb-clock-line-circle .list-bullet::after {
content: "";
position: absolute;
inset: 0;
margin: auto;
width: calc(var(--checkbox-size) * 0.82);
height: calc(var(--checkbox-size) * 0.82);
border: none;
border-radius: 0;
background-color: var(--bb-marker-color);
-webkit-mask-image: var(--bb-clock-svg);
mask-image: var(--bb-clock-svg);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center;
-webkit-mask-size: contain;
mask-size: contain;
transform: rotate(-4deg);
box-sizing: border-box;
width: calc(var(--checkbox-size) * 0.5);
height: calc(var(--checkbox-size) * 0.5);
background-color: transparent;
border: max(1px, calc(var(--checkbox-size) * 0.08)) solid var(--bb-marker-color);
border-radius: 47% 53% 52% 48% / 53% 47% 53% 47%;
transform: rotate(-8deg);
filter: var(--bb-rough);
text-shadow: none;
box-shadow: var(--bb-ink-shadow);
}
/* ===========================================================================

View file

@ -2,5 +2,6 @@
"0.1.0": "1.0.0",
"0.2.0": "1.5.7",
"0.3.0": "1.5.7",
"0.4.0": "1.5.7"
"0.4.0": "1.5.7",
"0.4.1": "1.5.7"
}