generated from obsidianmd/obsidian-sample-plugin
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathmain.ts
377 lines (312 loc) · 16 KB
/
main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
import { App, Notice, Plugin, PluginSettingTab, Setting, moment, MarkdownView } from 'obsidian';
import { createDailyNote, getAllDailyNotes, getDailyNote } from 'obsidian-daily-notes-interface';
interface LumberjackSettings {
logPrefix: string;
useTimestamp: boolean;
alwaysOpenInNewLeaf: boolean;
inboxFilePath: string;
newDraftFilenameTemplate: string;
targetHeader: string;
timestampFormat: string;
}
const DEFAULT_SETTINGS: LumberjackSettings = {
logPrefix: '- [ ] ',
useTimestamp: true,
alwaysOpenInNewLeaf: false,
inboxFilePath: "Inbox",
newDraftFilenameTemplate: "YYYYMMDDHHmmss",
targetHeader: "Journal",
timestampFormat: "HH:mm"
}
const editModeState = {
state: { mode: "source" },
active: true,
focus: true
};
interface Parameters {
data?: string;
vault?: string;
name?: string;
}
export default class LumberjackPlugin extends Plugin {
settings: LumberjackSettings;
async onload() {
console.debug('Loading the Lumberjack plugin. 𖥧');
await this.loadSettings();
this.addCommand({
id: 'lumberjack-log',
name: 'Log something new on your daily note',
callback: () => {
this.newLog(this.settings.alwaysOpenInNewLeaf);
}
});
this.addSettingTab(new LumberjackSettingsTab(this.app, this));
this.registerObsidianProtocolHandler("log", async (𖣂) => {
// Need to handle multiple vaults, I guess. Sigh.
const parameters = 𖣂 as unknown as Parameters;
for (const parameter in parameters) { // not yet using parameters
(parameters as any)[parameter] = (parameters as any)[parameter]; // Thanks to @Vinzent03 for a clear-cut (pun intended) example of how to do this
}
this.newLog(this.settings.alwaysOpenInNewLeaf);
});
this.registerObsidianProtocolHandler("timber", async (𖣂) => {
// Need to handle multiple vaults, I guess. Sigh.
const parameters = 𖣂 as unknown as Parameters;
for (const parameter in parameters) { // not yet actually using parameters
(parameters as any)[parameter] = (parameters as any)[parameter]; // Thanks to @Vinzent03 for a clear-cut (pun intended) example of how to do this
}
if (parameters.name) {
// this.ifATreeFallsInTheBackgroundDidItEverReallyFall(parameters.name); // not yet implemented
} else {
this.timber();
}
});
}
// newLog creates a new item with a user-configured prefix under a user-configured heading in the daily note, and gives them editing ability in that position immediately.
async newLog(openFileInNewLeaf: boolean) {
// find or create the daily note
let dailyNote = getDailyNote(moment(), getAllDailyNotes());
if (!dailyNote) { dailyNote = await createDailyNote(moment()); }
// set up the timestamp string, if the user is using it
let tampTime: string;
if (this.settings.useTimestamp) {
tampTime = moment().format(this.settings.timestampFormat) + " ";
} else {
tampTime = "";
}
// open the daily note in edit mode and get the editor
let openedDailyNote = await this.app.workspace.openLinkText(dailyNote.name, dailyNote.path, openFileInNewLeaf, editModeState);
if (this.app.workspace.getActiveViewOfType(MarkdownView)) {
let editor = this.app.workspace.getActiveViewOfType(MarkdownView).editor;
if (editor == null) {
new Notice(`Could not find the daily note. Check your daily note settings, or report this as a bug on the plugin repository.`);
return
}
// establish the line prefix to add, if anything
let linePrefix = `
${this.settings.logPrefix}${tampTime}`
// Make sure the editor has focus
editor.focus();
// Inserting the cursor
// The goal is to set the cursor either at the end of the user's target section, if set, or at the end of the note
// find the section to insert the log item into and set the insert position to append into it, or set the insert position to the end of the note
let sections = this.app.metadataCache.getFileCache(dailyNote).headings;
if (!sections) {
// The daily note does not have sections. Insert the log item at the bottom of the note.
editor.setCursor(editor.lastLine());
editor.replaceSelection(linePrefix);
editor.setCursor(editor.lastLine());
} else {
if (this.settings.targetHeader != "") { // If the user has set a target header
// need to figure out which line the _next_ section is on, if any, then use that line number in the functions below
let targetSection = sections.find( (eachSection) => (eachSection.heading == this.settings.targetHeader)); // does the heading we're looking for exist?
if (targetSection) { // The target section exists
let nextSection = sections.find( (eachSection) => ((eachSection.position.start.line > targetSection.position.start.line) && (eachSection.level <= targetSection.level))); // matches sections _after_ our target, with the same level or greater
console.debug(nextSection);
if (!nextSection) {
// There is no section following the target section. Look for the end of the document
// A better approach would append the item at the end of the content inside the user's target section, because it's possible that someone would put more stuff in their daily note without a following section, but that will have to be implemented later.
console.debug("No section follows the target section. Inserting the log item at the end of the target section.")
editor.setCursor(editor.lastLine());
editor.replaceSelection(linePrefix);
editor.setCursor(editor.lastLine());
} else {
if (typeof(nextSection) !== undefined) { // The search for a following section did not return undefined, therefore it exists
// Find out if there is a preceding blank line before the next section. E.g., does the user use linebreaks to separate content in edit mode? If so, inserting the next item after that line break will look messy.
if (editor.getLine(nextSection.position.start.line - 1) === editor.getLine(targetSection.position.start.line)) {
// There are no lines between the next section and the target section. Insert the log item immediately after the target section.
editor.setCursor(nextSection.position.start.line - 1);
editor.replaceSelection(linePrefix);
editor.setCursor(nextSection.position.start.line);
} else if (editor.getLine(nextSection.position.start.line - 1).length > 0) {
// The line just before the next section header is not blank. Insert the log item just before the next section, without a line break.
console.debug("No blank lines found between the target section and the next section.");
editor.setCursor(nextSection.position.start.line - 2);
editor.replaceSelection(linePrefix);
editor.setCursor(nextSection.position.start.line);
} else {
console.debug(`The line before the next section has 0 length. It is line number: ${nextSection.position.start.line - 1}`)
// The line just before the next section header is blank. It's likely that the user uses line breaks to clean up their note in edit mode.
// The approach here is to iterate over the lines preceding the next section header until a non-blank line is reached, then insert the log item at (iterator.position.start.line + 1)...
let lastBlankLineFound = false;
let noBlankLines = false;
let lastLineBeforeLineBreakIteratorLineNumber = nextSection.position.start.line - 1; // `lastLineBeforeLineBreakIteratorNumber: this wordy variable represents the number of the last-line-before-line-break iterator's current line
while (lastBlankLineFound == false) {
if (lastLineBeforeLineBreakIteratorLineNumber == 0) { // This condition would mean the iterator found the start of the document
noBlankLines = true;
lastBlankLineFound = true;
} else {
let blankLineFinderCurrentLine = editor.getLine(lastLineBeforeLineBreakIteratorLineNumber);
if (blankLineFinderCurrentLine.toString() === "") {
lastBlankLineFound = true;
console.debug("found the last line");
} else {
lastLineBeforeLineBreakIteratorLineNumber = lastLineBeforeLineBreakIteratorLineNumber - 1; // Move to the next line up
}
}
}
if (noBlankLines) { // this means the iterator failed to find any blanks at all; insert the log item just before the next section.
console.debug("No blank lines found.");
editor.setCursor(nextSection.position.start.line - 1);
editor.replaceSelection(linePrefix);
editor.setCursor(nextSection.position.start.line - 1);
} else { // There were an arbitrary number of blank lines before the next section header. Insert the log item _after_ the last (length > 0) line before the next section header.
console.debug(`Iterator stopped at line ${lastLineBeforeLineBreakIteratorLineNumber}, with text ${editor.getLine(lastLineBeforeLineBreakIteratorLineNumber)}`);
editor.setCursor(lastLineBeforeLineBreakIteratorLineNumber - 1);
editor.replaceSelection(linePrefix);
editor.setCursor(lastLineBeforeLineBreakIteratorLineNumber);
}
}
}
}
} else {
new Notice(`Could not run the Log command. Your daily note does not contain the target section you've specified in Preferences → Lumberjack—that is, the heading "${this.settings.targetHeader}". Please enter a heading that exists in your daily note, or leave this setting blank.`);
return;
}
} else {
// The user has not set a target header. Insert the log item at the bottom of the note.
editor.setCursor(editor.lastLine());
editor.replaceSelection(linePrefix);
editor.setCursor(editor.lastLine());
}
}
}
}
// // Log the thought in the background.
// // Not yet implemented.
// async ifATreeFallsInTheBackgroundDidItEverReallyFall(someData: string) {
// let dailyNote = getDailyNote(moment(), getAllDailyNotes());
// if (!dailyNote) { dailyNote = await createDailyNote(moment()); }
// let tampTime: string;
// if (this.settings.useTimestamp) {
// tampTime = moment().format("HH:mm") + " ";
// }
// let dailyNoteOldText = await this.app.vault.read(dailyNote); // unsure about using .read versus .cachedRead here. as this is meant to be used when Obsidian is in the background
// let dailyNoteNewText = `${dailyNoteOldText}
// ${this.settings.logPrefix}${tampTime}${someData}`
// this.app.vault.modify(dailyNote, dailyNoteNewText) // write the new line in
// new Notice('Data "' + someData + '" logged to the daily note.');
// }
async timber() {
let zkUUIDNoteName = moment().format(this.settings.newDraftFilenameTemplate);
const isInboxPathEmpty = this.settings.inboxFilePath === undefined || this.settings.inboxFilePath.trim() === '';
const folderName = isInboxPathEmpty ? '' : this.settings.inboxFilePath;
const inboxPath = isInboxPathEmpty ? '' : `/${this.settings.inboxFilePath}/`;
console.debug(`${this.settings.inboxFilePath}`);
if (!isInboxPathEmpty && !(this.app.vault.getAbstractFileByPath(`${folderName}`))) { // In the future, handle folder creation as necessary. For now, error and tell the user if the inbox folder does not exist.
new Notice(`Error. Lumberjack couldn't create the draft. Does the inbox folder you've set in Preferences -> Lumberjack 🪓🪵 exist?`);
return;
}
await this.app.vault.create(`${inboxPath}${zkUUIDNoteName}.md`, "");
let newDraft = await this.app.workspace.openLinkText(zkUUIDNoteName, `${inboxPath}`, this.settings.alwaysOpenInNewLeaf, editModeState);
if (this.app.workspace.getActiveViewOfType(MarkdownView)) {
let editor = this.app.workspace.getActiveViewOfType(MarkdownView).editor;
if (editor == null) {
new Notice(`Couldn't run the Timber command. Please report this as a bug in the GitHub repository for Lumberjack 🪓🪵`);
return
}
editor.focus();
let startChar = "";
editor.setCursor(editor.lastLine());
editor.replaceSelection(startChar);
editor.setCursor(editor.lastLine());
}
}
onunload() {
console.debug('Unloading the Lumberjack plugin. Watch out for splinters.');
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class LumberjackSettingsTab extends PluginSettingTab {
plugin: LumberjackPlugin;
constructor(app: App, plugin: LumberjackPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
let {containerEl} = this;
containerEl.empty();
containerEl.createEl('h2', {text: 'Lumberjack Settings'});
new Setting(containerEl)
.setName('Target header')
.setDesc('Append logged items to a target header in your daily note')
.addText(text => text
.setPlaceholder(this.plugin.settings.targetHeader)
.setValue(this.plugin.settings.targetHeader)
.onChange(async (value) => {
this.plugin.settings.targetHeader = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Prefix for logging')
.setDesc('Sets a prefix for lines added via the log command')
.addText(text => text
.setPlaceholder('Prefix:')
.setValue(this.plugin.settings.logPrefix)
.onChange(async (value) => {
this.plugin.settings.logPrefix = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Timestamp')
.setDesc('If enabled, the log command will prefix newly added lines with a timestamp. (Changing this setting reloads the plugin.)')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.useTimestamp)
.onChange(async (value) => {
this.plugin.settings.useTimestamp = value;
await this.plugin.saveSettings();
if (value) {
this.containerEl.appendChild(timestampFormatSetting.settingEl);
} else {
timestampFormatSetting.settingEl.remove();
}
}));
new Setting(containerEl)
.setName('Always open in a new pane')
.setDesc('If enabled, Lumberjack commands will always open in a new pane.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.alwaysOpenInNewLeaf)
.onChange(async (value) => {
this.plugin.settings.alwaysOpenInNewLeaf = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Inbox folder')
.setDesc('Set the destination for notes created with `obsidian://timber`, e.g., `My Notes/Inbox`. Do not include leading or trailing slashes.')
.addText(text => text
.setValue(this.plugin.settings.inboxFilePath)
.setPlaceholder(this.plugin.settings.inboxFilePath)
.onChange(async (value) => {
this.plugin.settings.inboxFilePath = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Filename for new drafts')
.setDesc('Set the filename template for new drafts created with `obsidian://timber`. Uses Moment formatting, same as daily notes. Default: YYYYMMDDHHmm')
.addText(text => text
.setValue(this.plugin.settings.newDraftFilenameTemplate)
.setPlaceholder(this.plugin.settings.newDraftFilenameTemplate)
.onChange(async (value) => {
this.plugin.settings.newDraftFilenameTemplate = value;
await this.plugin.saveSettings();
}));
let timestampFormatSetting = new Setting(containerEl)
.setName('Timestamp format')
.setDesc('Set the format for timestamp prefixes. Follows Moment.js formatting, same as Obsidian\'s core daily notes.')
.addText(text => text
.setValue(this.plugin.settings.timestampFormat)
.setPlaceholder(this.plugin.settings.timestampFormat)
.onChange(async (value) => {
this.plugin.settings.timestampFormat = value;
await this.plugin.saveSettings();
}))
if (!this.plugin.settings.useTimestamp) {
timestampFormatSetting.settingEl.remove();
}
}
}