diff --git a/README.md b/README.md index df87636..707f31b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This plugin integrates [Markwhen](https://github.com/mark-when/markwhen/) into [Obsidian.md](https://obsidian.md/). You can use markwhen syntax to create timelines. > [!Note] -> Latest release: 0.0.2 +> Latest release: 0.0.3 > Document version: 0.0.2 ## Installation diff --git a/copyAssets.sh b/copyAssets.sh index 03d80d1..98431b6 100755 --- a/copyAssets.sh +++ b/copyAssets.sh @@ -2,4 +2,5 @@ cp ./out/* ~/Documents/Obsidian\ Vault/.obsidian/plugins/markwhen cp ./styles.css ~/Documents/Obsidian\ Vault/.obsidian/plugins/markwhen -cp ./manifest.json ~/Documents/Obsidian\ Vault/.obsidian/plugins/markwhen \ No newline at end of file +cp ./manifest.json ~/Documents/Obsidian\ Vault/.obsidian/plugins/markwhen +cp ./markwhen.md ~/Documents/Obsidian\ Vault/.obsidian/plugins/markwhen \ No newline at end of file diff --git a/manifest.json b/manifest.json index 31da89c..81c3293 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "id": "markwhen", "name": "Markwhen", - "version": "0.0.2", + "version": "0.0.3", "minAppVersion": "1.0.0", "description": "Create timelines, gantt charts, calendars, and more using markwhen.", "author": "Markwhen", "authorUrl": "https://github.com/mark-when", "isDesktopOnly": true -} \ No newline at end of file +} diff --git a/markwhen.md b/markwhen.md new file mode 100644 index 0000000..4761401 --- /dev/null +++ b/markwhen.md @@ -0,0 +1,197 @@ +--- +title: Welcome to Markwhen! + +edit: + - rob@markwhen.com + +view: \* + +#Project1: #d336b1 +--- +section Welcome #welcome +now: This example timeline showcases some of markwhen's features. + +now: For more information, view the documentation [here](https://docs.markwhen.com) or join the [discord](https://discord.gg/3rTpUD94ac) +#welcome + +now: Note that changes you make here are not saved +If you want to make a new markwhen you should open a tab at the bottom or click open in the sidebar +endSection + +section All Projects +group Project 1 #Project1 +// Supports ISO8601 +2023-01/2023-03: Sub task #John +2023-03/2023-06: Sub task 2 #Michelle +More info about sub task 2 + +- [ ] We need to get this done +- [x] And this +- [ ] This one is extra + +2023-07: Yearly planning +endGroup + group Project 2 #Project2 +2023-04/4 months: Larger sub task #Danielle + +// Supports American date formats +03/2023 - 1 year: Longer ongoing task #Michelle + +- [x] Sub task 1 +- [x] Sub task 2 +- [ ] Sub task 3 +- [ ] Sub task 4 +- [ ] so many checkboxes omg + +10/2023 - 2 months: Holiday season +endGroup + +group Project 3 +01/2024: Project kickoff +02/2024-04/2024: Other stuff +endGroup +endSection + +2023-01-03 every other week for 1 year: Biweekly meeting + +// Events that don't have explicit end dates have inferred ranges - for example, when a year is specified, it lasts from the beginning of that year to the end of it. +2024: A year-long event + +// Inferred ranges are as granular as their definitions. +09/2024: one month + +2025-05-05: one day + +Jan 4 2025 8am: instant + +// You can also be specific with your ranges +2024/2025: An event that lasts two years + +November 8, 2022 9am - November 9, 2023, 10am: one year, one day, and one hour + +now: [More documentation](https://docs.markwhen.com/syntax/events.html) + +// Event descriptions last from the date range definition up to the next event +2029-04-25/2029-05-03: Descriptions can be one line + +2029-04-25/2029-05-03: Or +they can span +multiple lines + +1/27/2025: [] An event can have a checkbox for completion +Put square brackets at the start of the event description + +1/27/2026: [x] To mark an event as completed, put an x in the square brackets + +1/27/2027: Events can have lists + +- [ ] checkbox list item +- [x] a completed checkbox list item +- simple list item +- another simple list item + +1/27/2028 - 1 year: 68% Manually indicate an event's completion with a percentage in the description + +Partially completed events will have their event bar partially filled that amount + +1 year: Links are markdown-style: [This is a link](https://markwhen.com) + +1 year: Images are also markdown-style: +![](https://blog.markwhen.com/images/calendar1.png) + +1 year: Locations (which are more useful for the map view) can be indicated in a similar way: [Hawaii](location) [Alaska](map) + +2024: Refer to other markwhen documents with `@` syntax: @rob + +now: [More documentation](https://docs.markwhen.com/syntax/event-descriptions.html) + +// Events can be grouped together + +group +1/27/2024: Happy birthday +2020-03: Covid started in the US +endGroup + +group Group with title + +Feb 2 2025: Interviewing +Feb 8 2025: Write report +Feb 19 2025: Presentation + +endGroup + +group Groups can contain other groups #big + +group Smaller plan #small #nested + +1 year: Accomplish something + +2 years: Accomplish something else + +endGroup + +1 year: Things are accomplished + +endGroup + +section Sections extend across the screen + +2023: Start year + +section Nested section #nested + +2025: End year + +endSection +endSection + +now: [More documentation](https://docs.markwhen.com/syntax/groups-and-sections.html) + +// Specify tag colors in the header (before any event) +#Timeline: #abf + +now: Events and groups can have tags + +section Tagged events #Timeline +Feb 18 1999: back in the day #Past #The90s +2043: in the future #TheOther90s + +now: [More documentation](https://docs.markwhen.com/syntax/event-descriptions.html#tag) + +2025: Event + +1 year: This event happens immediately after the previous event and lasts for 1 year + +#after + +3 months - 1 month: This event happens 3 months after the previous event and lasts for 1 month +#after + +by 2 weeks - 1 month: This event happens 2 weeks before the previous event and lasts 1 month +#before + + +2023: Event !base + +after !base 1 year - 1 month: This event happens 1 year after the event with with id `base` and lasts for 1 month +#after + +before !base 1 week day - 1 hour: This event happens 1 week day before the event with id `base` and lasts 1 hour +#before + +October 7, 1989 every year for 10 years: ... +2025-03-04 every week for 12 weeks: ... +2022-01/2022-03 every other year x9: ... +Feb 1 2023 every 6 months for 10 times: ... + +// Visually indicate that an event is an era or milestone with the tag #era or #milestone + +2023-06-01/2023-08-20: Summer time #vacation + +2023-08-21/2023-12-17: Back to school + +2023-12-18/2024-01-05: Winter break #vacation #milestone + +2024-01-08/2024-05-31: Back to school + +2024-05-27/2024-05-31: Final project due #milestone diff --git a/package-lock.json b/package-lock.json index 662cd7a..b61b02e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "markwhen", - "version": "0.0.2", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "markwhen", - "version": "0.0.2", + "version": "0.0.3", "license": "MIT", "dependencies": { "@codemirror/state": "^6.4.1", @@ -16,7 +16,8 @@ "@markwhen/parser": "^0.10.15", "@markwhen/resume": "^1.1.0", "@markwhen/timeline": "^1.3.3", - "@markwhen/view-client": "^1.4.4" + "@markwhen/view-client": "^1.4.4", + "async-mutex": "^0.5.0" }, "devDependencies": { "@types/node": "^20.12.7", @@ -1527,6 +1528,14 @@ "node": ">=8" } }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2879,8 +2888,7 @@ "node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "node_modules/tsutils": { "version": "3.21.0", diff --git a/package.json b/package.json index 892d23c..d4b6cee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "markwhen", - "version": "0.0.2", + "version": "0.0.3", "description": "Markwhen integration for Obsidian.md", "main": "main.js", "type": "module", @@ -33,6 +33,7 @@ "@markwhen/parser": "^0.10.15", "@markwhen/resume": "^1.1.0", "@markwhen/timeline": "^1.3.3", - "@markwhen/view-client": "^1.4.4" + "@markwhen/view-client": "^1.4.4", + "async-mutex": "^0.5.0" } } diff --git a/src/MarkwhenCodemirrorPlugin.ts b/src/MarkwhenCodemirrorPlugin.ts index 8c0f687..05a73ec 100644 --- a/src/MarkwhenCodemirrorPlugin.ts +++ b/src/MarkwhenCodemirrorPlugin.ts @@ -39,6 +39,7 @@ export class MarkwhenCodemirrorPlugin implements PluginValue { view: EditorView; worker = useParserWorker((mw) => { this.markwhen = mw; + this.view.dispatch({ effects: parseResult.of(mw), }); diff --git a/src/MarkwhenView.ts b/src/MarkwhenView.ts index 5b85889..b0b7c50 100644 --- a/src/MarkwhenView.ts +++ b/src/MarkwhenView.ts @@ -1,4 +1,4 @@ -import { WorkspaceLeaf, MarkdownView, TFile, Platform } from 'obsidian'; +import { Menu, WorkspaceLeaf, MarkdownView, TFile, Platform, TAbstractFile } from 'obsidian'; import MarkwhenPlugin from './main'; import { MARKWHEN_ICON } from './icons'; export const VIEW_TYPE_MARKWHEN = 'markwhen-view'; @@ -234,6 +234,102 @@ export class MarkwhenView extends MarkdownView { this.contentEl.addClass('markwhen-view'); }); super.onload(); + + const action= (viewType: ViewType) => async (evt: MouseEvent) => { + if (evt.metaKey || evt.ctrlKey) { + await this.split(viewType); + } else if (this.viewType !== viewType) { + await this.setViewType(viewType); + } + }; + this.registerEvent(this.app.workspace.on('file-menu', (menu, file: TAbstractFile) => { + // Check if the submenu should be displayed + if (this.plugin.settings.actionToContextSettingtoggle) { + /* + let submenuOpen = false; + let submenu: Menu | null = null; + const mainMenuItem = menu.addItem((item) => { + item.setTitle('-Markwhen'); // Main menu item title + item.setIcon(MARKWHEN_ICON); // Main item icon + item.onClick((evt: MouseEvent) => drawmenu()(evt)); + }); + const drawmenu = () => async (evt: MouseEvent) => { + if (!submenuOpen) { + submenuOpen = true; + submenu = new Menu(); + submenu.addItem((item) => { + item.setTitle('Edit Text'); + item.setIcon('pen-line'); + item.onClick((evt: MouseEvent) => handleAction('text')(evt)); + }); + + submenu.addItem((item) => { + item.setTitle('View Calendar'); + item.setIcon('calendar'); + item.onClick((evt: MouseEvent) => handleAction('calendar')(evt)); + }); + + submenu.addItem((item) => { + item.setTitle('View Timeline'); + item.setIcon(MARKWHEN_ICON); // Assuming MARKWHEN_ICON is defined + item.onClick((evt: MouseEvent) => handleAction('timeline')(evt)); + }); + + submenu.addItem((item) => { + item.setTitle('View Vertical Timeline'); + item.setIcon('oneview'); // Assuming MARKWHEN_ICON is defined + item.onClick((evt: MouseEvent) => handleAction('oneview')(evt)); + }); + + submenu.showAtPosition({ x: evt.x, y: evt.y }); + } else { + submenu?.hide(); + submenuOpen = false; + } + };*/ //this draws submenu but closes main menu. Maybe override main menu drawing and custom expose the X and Y coords, or hook contextmenu ?? + + // Add a submenu item for the plugin + const pluginSubMenu = menu.addItem((item) => { + item.setTitle('Markwhen'); // Submenu title + item.setIcon(MARKWHEN_ICON); // Main item icon + }); + + // Add custom actions under the submenu + pluginSubMenu.addItem((item) => { + item.setTitle('Edit Text'); + item.setIcon('pen-line'); + item.onClick((evt: MouseEvent) => handleAction('text')(evt)); + }); + + pluginSubMenu.addItem((item) => { + item.setTitle('View Calendar'); + item.setIcon('calendar'); + item.onClick((evt: MouseEvent) => handleAction('calendar')(evt)); + }); + + pluginSubMenu.addItem((item) => { + item.setTitle('View Timeline'); + item.setIcon(MARKWHEN_ICON); // Assuming MARKWHEN_ICON is defined + item.onClick((evt: MouseEvent) => handleAction('timeline')(evt)); + }); + + pluginSubMenu.addItem((item) => { + item.setTitle('View Vertical Timeline'); + item.setIcon('oneview'); // Assuming MARKWHEN_ICON is defined + item.onClick((evt: MouseEvent) => handleAction('oneview')(evt)); + }); + const handleAction = (viewType: ViewType) => async (evt: MouseEvent) => { + if (file instanceof TFile) { + await this.app.workspace.getLeaf().openFile(file); + await action(viewType)(evt); + } else { + console.error("Selected item is not a file."); + } + }; + + // Add more actions as needed + } + })); } async setViewType(viewType?: ViewType) { diff --git a/src/main.ts b/src/main.ts index 25868b3..38b83e8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,38 +8,178 @@ import { normalizePath, TextComponent, ExtraButtonComponent, + ToggleComponent, } from 'obsidian'; +import {Mutex} from 'async-mutex'; + import { MARKWHEN_ICON } from './icons'; import { VIEW_TYPE_MARKWHEN, MarkwhenView } from './MarkwhenView'; +import * as fs from 'fs'; +import * as path from 'path'; + interface MarkwhenPluginSettings { folder: string; + deftoselectedtoggle: boolean; + actionToContextSettingtoggle: boolean; } + const DEFAULT_SETTINGS: MarkwhenPluginSettings = { folder: 'Markwhen', + deftoselectedtoggle: false, + actionToContextSettingtoggle: false, }; + + + export default class MarkwhenPlugin extends Plugin { + public utilities = new class { + constructor(public superThis: MarkwhenPlugin) { + } + public testSetOuterPrivate(target: number) { + } + }(this); + + public fclass = new class { + constructor(public superThis: MarkwhenPlugin) { + + } + private lastCallTime = 0; + private notPassedCount = 1; + private isUpdating = false; + private timeMutex = new Mutex(); + private countMutex = new Mutex(); + + async checkAndCreateFolder(folderPath: string) { + const vault = this.superThis.app.vault; + folderPath = normalizePath(folderPath); + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/658 + const folder = vault.getAbstractFileByPathInsensitive(folderPath); + if (folder && folder instanceof TFolder) return; + if (folder && folder instanceof TFile) return; //File name corruption + await vault.createFolder(folderPath); + } + + async createMWFile( + filename: string, + folderPath: string, + initData?: string + ): Promise { + const fname = normalizePath(`${folderPath}/${filename}`); + await this.checkAndCreateFolder(folderPath); + // Acquire the time mutex to ensure exclusive access to the critical section for lastCallTime + const timeRelease = await this.timeMutex.acquire(); + try { + const currentTime = Date.now(); + if (!this.isUpdating && (Math.floor(currentTime / 1000) != Math.floor(this.lastCallTime / 1000))) { + // Set the flag to indicate that the value is being updated + this.isUpdating = true; + + // Release the time mutex to allow other functions to proceed + timeRelease(); + + // Update the value + const file = await this.updateLastCallTime(currentTime, fname, initData); + return file; + } else { + timeRelease(); + // Increment the not passed count + const file = await this.incrementNotPassedCount(fname, initData); + return file; + } + } finally { + // Release the time mutex to allow other functions to proceed + timeRelease(); + } + } + + private async updateLastCallTime( + currentTime: number, + filename: string, + initData?: string): Promise { + // Acquire the time mutex to ensure exclusive access to the critical section for lastCallTime + const timeRelease = await this.timeMutex.acquire(); + let file; + try { + // Update the last call time + this.lastCallTime = currentTime; + this.zeroNotPassedCount(); + file = await this.superThis.app.vault.create(filename+".mw", initData ?? ''); + this.isUpdating = false; + } finally { + // Release the time mutex to allow other functions to proceed + timeRelease(); + } + return file; + } + + private async incrementNotPassedCount( + filename: string, + initData?: string): Promise { + // Acquire the count mutex to ensure exclusive access to the critical section for notPassedCount + const countRelease = await this.countMutex.acquire(); + let file; + try { + // Increment the not passed count + file = await this.superThis.app.vault.create(filename+ " (" + this.notPassedCount + ').mw', initData ?? ''); + this.notPassedCount++; + } finally { + // Release the count mutex to allow other functions to proceed + countRelease(); + } + return file; + } + private async zeroNotPassedCount(): Promise { + // Acquire the count mutex to ensure exclusive access to the critical section for notPassedCount + const countRelease = await this.countMutex.acquire(); + try { + // Increment the not passed count + this.notPassedCount = 1; + } finally { + // Release the count mutex to allow other functions to proceed + countRelease(); + } + } + + }(this); + + settings: MarkwhenPluginSettings; async onload() { await this.loadSettings(); + this.addCommand({ + id: 'markwhen-create-template', + name: 'Create Markwhen Template File', + callback: () => { + const markwhenfile = '/markwhen.mw'; + const obsidianConfigDir = this.app.vault.adapter.basePath; + const pluginFolderPath = path.join(obsidianConfigDir, this.manifest?.dir?.replace(/\//g, "\\") ?? ''); //check if configdir exists and if file does not exist create one + + fs.readFile(pluginFolderPath + markwhenfile, 'utf8', (err, data) => { + if (err) { + console.error('Error reading file:', err); + return; + } + this.createAndOpenMWFile(undefined,undefined,data); + // Do something with the file contents here + }); + // Your command logic goes here + console.log('My Plugin Command executed!'); + } + }); + this.addRibbonIcon( MARKWHEN_ICON, 'Create new Markwhen file', // tooltip () => { //TODO: better UX dealing with ribbon icons - this.createAndOpenMWFile( - `Markwhen ${new Date() - .toLocaleString('en-US', { hour12: false }) - .replace(/\//g, '-') - .replace(/:/g, '.') - .replace(/,/, '')}.mw` // improve this - ); - } + this.createAndOpenMWFile(); + } ); this.registerView( @@ -79,35 +219,25 @@ export default class MarkwhenPlugin extends Plugin { //Credits to https://github.com/yuleicul/obsidian-ketcher on file operations async createAndOpenMWFile( - filename: string, + filename?: string, foldername?: string, initData?: string ) { - const file = await this.createMWFile(filename, foldername, initData); + filename = filename ?? `Markwhen ${new Date() + .toLocaleString('en-US', { hour12: false }) + .replace(/\//g, '-') + .replace(/:/g, '.') + .replace(/,/, '')}`; + if (this.settings.deftoselectedtoggle && this.app.workspace.getActiveFile() instanceof TFile ) {foldername = this.app.workspace.getActiveFile()?.parent?.path ?? this.app.workspace.lastActiveFile.parent.path;} + const folderPath = normalizePath(foldername ?? this.settings.folder) + const file = await this.fclass.createMWFile(filename, folderPath, initData); this.app.workspace.getLeaf('tab').openFile(file); } - async createMWFile( - filename: string, - foldername?: string, - initData?: string - ): Promise { - const folderPath = normalizePath(foldername || this.settings.folder); - await this.checkAndCreateFolder(folderPath); - const fname = normalizePath(`${folderPath}/${filename}`); - const file = await this.app.vault.create(fname, initData ?? ''); - return file; + async fileExists(filePath: string): Promise { + return await this.app.vault.adapter.exists(filePath); } - async checkAndCreateFolder(folderPath: string) { - const vault = this.app.vault; - folderPath = normalizePath(folderPath); - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/658 - const folder = vault.getAbstractFileByPathInsensitive(folderPath); - if (folder && folder instanceof TFolder) return; - if (folder && folder instanceof TFile) return; //File name corruption - await vault.createFolder(folderPath); - } } class MarkwhenPluginSettingTab extends PluginSettingTab { @@ -130,6 +260,8 @@ class MarkwhenPluginSettingTab extends PluginSettingTab { button.setIcon('rotate-ccw').onClick(async () => { folderText.setValue(DEFAULT_SETTINGS.folder); this.plugin.settings.folder = DEFAULT_SETTINGS.folder; // Extract to Default Object + deftoselecttoggle.setValue(DEFAULT_SETTINGS.deftoselectedtoggle); + this.plugin.settings.deftoselectedtoggle = DEFAULT_SETTINGS.deftoselectedtoggle; await this.plugin.saveSettings(); }); }); @@ -141,5 +273,40 @@ class MarkwhenPluginSettingTab extends PluginSettingTab { this.plugin.settings.folder = value; await this.plugin.saveSettings(); }); + + if (this.plugin.settings.deftoselectedtoggle) { + folderText.inputEl.classList.add('disabled-text-field'); + } else { + folderText.inputEl.classList.remove('disabled-text-field'); + } + + const defToSelectedSetting = new Setting(containerEl) + .setName('Default To Current Folder') + .setDesc('Create new MW file under current folder'); + + const deftoselecttoggle = new ToggleComponent(defToSelectedSetting.controlEl) + .setValue(this.plugin.settings.deftoselectedtoggle) + .onChange(async (value) => { + this.plugin.settings.deftoselectedtoggle = value; + await this.plugin.saveSettings(); + folderText.setDisabled(value); + // Apply the CSS class to gray out the folder name text field + if (value) { + folderText.inputEl.classList.add('disabled-text-field'); + } else { + folderText.inputEl.classList.remove('disabled-text-field'); + } + }); + + const actionToContextSetting = new Setting(containerEl) + .setName('Actions on Context Menu') + .setDesc('Add Default Actions to Context Menu'); + + const actionToContextSettingtoggle = new ToggleComponent(actionToContextSetting.controlEl) + .setValue(this.plugin.settings.actionToContextSettingtoggle) + .onChange(async (value) => { + this.plugin.settings.actionToContextSettingtoggle = value; + await this.plugin.saveSettings(); + }); } }