Skip to content

Commit

Permalink
use Automaton for easier code management.
Browse files Browse the repository at this point in the history
This should help, if more features are coming. Processor is now the single point of reusable code. Automaton.ts is the stuff, who processes the steps. So now you can go to a specific step and they do not interrupt each other. The automaton stops if there is happening any error.
  • Loading branch information
Heiss committed Jun 25, 2024
1 parent 5a9337a commit 5574cba
Show file tree
Hide file tree
Showing 9 changed files with 641 additions and 424 deletions.
4 changes: 1 addition & 3 deletions main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {Notice, Plugin} from 'obsidian';
import {DEFAULT_SETTINGS, SettingTab, VikunjaPluginSettings} from "./src/settings/SettingTab";
import {MainModal} from "./src/modals/mainModal";
import {Tasks} from "./src/vikunja/tasks";
import {Processor} from "./src/processing/processor";
import {Processor} from "./src/processing/Processor";
import {UserUser} from "./vikunja_sdk";
import {backendToFindTasks, chooseOutputFile} from "./src/enums";
import {appHasDailyNotesPluginLoaded} from "obsidian-daily-notes-interface";
Expand Down Expand Up @@ -96,7 +95,6 @@ export default class VikunjaPlugin extends Plugin {
// Called when the user clicks the icon.
new Notice('Start syncing with Vikunja');
await this.processor.exec();
new Notice('Syncing with Vikunja finished');
});

// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "vikunja-sync",
"name": "Vikunja Sync",
"version": "1.0.3",
"version": "1.0.4",
"minAppVersion": "0.15.0",
"description": "Integrates Vikunja into Obsidian as Task Management Platform.",
"author": "Peter Heiss",
Expand Down
79 changes: 79 additions & 0 deletions src/processing/Automaton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import VikunjaPlugin from "../../main";
import {App} from "obsidian";
import {ModelsTask} from "../../vikunja_sdk";
import {PluginTask} from "../vaultSearcher/vaultSearcher";
import {GetTasks} from "./getTasks";
import {RemoveTasks} from "./removeTasks";
import UpdateTasks from "./updateTasks";
import CreateTasks from "./createTasks";
import {Processor} from "./processor";

Check failure on line 9 in src/processing/Automaton.ts

View workflow job for this annotation

GitHub Actions / build

Cannot find module './processor' or its corresponding type declarations.

Check failure on line 9 in src/processing/Automaton.ts

View workflow job for this annotation

GitHub Actions / build

Cannot find module './processor' or its corresponding type declarations.

Check failure on line 9 in src/processing/Automaton.ts

View workflow job for this annotation

GitHub Actions / build

Cannot find module './processor' or its corresponding type declarations.

interface StepsOutput {
localTasks: PluginTask[];
vikunjaTasks: ModelsTask[];
}

interface IAutomatonSteps {
step(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): Promise<StepsOutput>;

}

enum AutomatonStatus {
READY,
RUNNING,
ERROR,
FINISHED,
}

class Automaton {
app: App;
plugin: VikunjaPlugin;
steps: IAutomatonSteps[];
currentStep: number = 0;
status: AutomatonStatus;
processor: Processor;

constructor(app: App, plugin: VikunjaPlugin, processor: Processor) {
this.app = app;
this.plugin = plugin;
this.processor = processor;

this.steps = [
new GetTasks(app, plugin, processor),
new RemoveTasks(app, plugin),
new CreateTasks(app, plugin, processor),
new UpdateTasks(app, plugin, processor),
];

this.status = AutomatonStatus.READY;
}

async run() {
let localTasks: PluginTask[] = [];
let vikunjaTasks: ModelsTask[] = [];
this.status = AutomatonStatus.RUNNING;

while (this.currentStep < this.steps.length) {
let output: StepsOutput;
try {
output = await this.execStep(localTasks, vikunjaTasks);
} catch (e) {
this.status = AutomatonStatus.ERROR;
console.error("Automaton: Error in step " + this.currentStep + ", Error: " + e);
return;
}
localTasks = output.localTasks;
vikunjaTasks = output.vikunjaTasks;
}

this.status = AutomatonStatus.FINISHED;
}

private async execStep(localTasks: PluginTask[], vikunjaTasks: ModelsTask[]): Promise<StepsOutput> {
return this.steps[this.currentStep++].step(localTasks, vikunjaTasks);
}

}

export {Automaton, AutomatonStatus};
export type {IAutomatonSteps, StepsOutput,};
225 changes: 225 additions & 0 deletions src/processing/Processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import Plugin from "../../main";
import {App, MarkdownView, Notice} from "obsidian";
import {PluginTask, VaultSearcher} from "../vaultSearcher/vaultSearcher";
import {TaskFormatter, TaskParser} from "../taskFormats/taskFormats";
import {Automaton, AutomatonStatus, IAutomatonSteps} from "./Automaton";
import UpdateTasks from "./updateTasks";
import {EmojiTaskFormatter, EmojiTaskParser} from "../taskFormats/emojiTaskFormat";
import {backendToFindTasks, supportedTasksPluginsFormat} from "../enums";
import {DataviewSearcher} from "../vaultSearcher/dataviewSearcher";


class Processor {
app: App;
plugin: Plugin;
vaultSearcher: VaultSearcher;
taskParser: TaskParser;
taskFormatter: TaskFormatter;
private alreadyUpdateTasksOnStartup = false;
private lastLineChecked: Map<string, number>;
private automaton: Automaton;

constructor(app: App, plugin: Plugin) {
this.app = app;
this.plugin = plugin;

this.vaultSearcher = this.getVaultSearcher();
this.taskParser = this.getTaskParser();
this.taskFormatter = this.getTaskFormatter();

this.lastLineChecked = new Map<string, number>();
}

/*
* The main method to sync tasks between Vikunja and Obsidian.
*/
async exec() {
if (this.plugin.settings.debugging) console.log("Processor: Start processing");

if (this.plugin.foundProblem) {
new Notice("Vikunja Plugin: Found problems in plugin. Have to be fixed first. Syncing is stopped.");
if (this.plugin.settings.debugging) console.log("Processor: Found problems in plugin. Have to be fixed first.");
return;
}

// Check if user is logged in
if (!this.plugin.userObject) {
// FIXME Currently cannot be used, because there is a bug in Vikunja, which returns 401 in api to get the user object.
//this.plugin.userObject = await new User(this.app, this.plugin).getUser();
}

if (this.plugin.settings.debugging) console.log("Processor: Reset automaton");
this.automaton = new Automaton(this.app, this.plugin, this);

await this.automaton.run();

switch (this.automaton.status) {
case AutomatonStatus.ERROR:
new Notice("Error while syncing tasks");
break;
case AutomatonStatus.FINISHED:
new Notice("Finished syncing tasks");
break;
}

if (this.plugin.settings.debugging) console.log("Processor: End processing");
}

async saveToVault(task: PluginTask) {
const newTask = await this.getTaskContent(task);

await this.app.vault.process(task.file, data => {
if (this.plugin.settings.appendMode) {
return data + "\n" + newTask;
} else {
const lines = data.split("\n");
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes(this.plugin.settings.insertAfter)) {
lines.splice(i + 1, 0, newTask);
break;
}
}
return lines.join("\n");
}
});
}

async getTaskContent(task: PluginTask) {
const content: string = await this.taskFormatter.format(task.task);
return `${content} `;
}

/*
* Split tasks into two groups:
* - tasksToUpdateInVault: Tasks which have updates in Vikunja
* - tasksToUpdateInVikunja: Tasks which have updates in the vault
*
* tasksToUpdateInVault has already all informations needed for vault update.
*
* This method should only be triggered on startup of obsidian and only once. After this, we cannot guerantee that the updated information of files are in sync.
*/
async updateTasksOnStartup() {
if (this.plugin.settings.debugging) console.log("Processor: Update tasks in vault and vikunja");
if (this.alreadyUpdateTasksOnStartup) throw new Error("Update tasks on startup can only be called once");
if (this.plugin.foundProblem) {
if (this.plugin.settings.debugging) console.log("Processor: Found problems in plugin. Have to be fixed first. Syncing is stopped.");
return;
}
if (!this.plugin.settings.updateOnStartup) {
if (this.plugin.settings.debugging) console.log("Processor: Update on startup is disabled");
return;
}

const localTasks = await this.vaultSearcher.getTasks(this.taskParser);
const vikunjaTasks = await this.plugin.tasksApi.getAllTasks();

const updateStep: IAutomatonSteps = new UpdateTasks(this.app, this.plugin, this);
await updateStep.step(localTasks, vikunjaTasks);

this.alreadyUpdateTasksOnStartup = true;
}

/*
* Check if an update is available in the line, where the cursor is currently placed, which should be pushed to Vikunja
*/
async checkUpdateInLineAvailable(): Promise<PluginTask | undefined> {
if (!this.plugin.settings.updateOnCursorMovement) {
if (this.plugin.settings.debugging) console.log("Processor: Update on cursor movement is disabled");
return;
}

const view = this.app.workspace.getActiveViewOfType(MarkdownView)
if (!view) {
if (this.plugin.settings.debugging) console.log("Processor: No markdown view found");
return;
}

const cursor = view.app.workspace.getActiveViewOfType(MarkdownView)?.editor.getCursor()

const currentFilename = view.app.workspace.getActiveViewOfType(MarkdownView)?.app.workspace.activeEditor?.file?.name;
if (!currentFilename) {

if (this.plugin.settings.debugging) console.log("Processor: No filename found");
return;
}

const currentLine = cursor?.line
if (!currentLine) {
if (this.plugin.settings.debugging) console.log("Processor: No line found");
return;
}

const file = view.file;
if (!file) {
if (this.plugin.settings.debugging) console.log("Processor: No file found");
return;
}

const lastLine = this.lastLineChecked.get(currentFilename);
let pluginTask = undefined;
if (!!lastLine) {
const lastLineText = view.editor.getLine(lastLine);
if (this.plugin.settings.debugging) console.log("Processor: Last line,", lastLine, "Last line text", lastLineText);
try {
const parsedTask = await this.taskParser.parse(lastLineText);
pluginTask = {
file: file,
lineno: lastLine,
task: parsedTask
};
} catch (e) {
if (this.plugin.settings.debugging) console.log("Processor: Error while parsing task", e);
}
}

this.lastLineChecked.set(currentFilename, currentLine);
return pluginTask;
}

async updateToVault(task: PluginTask) {
const newTask = await this.getTaskContent(task);

await this.app.vault.process(task.file, (data: string) => {
const lines = data.split("\n");
lines.splice(task.lineno, 1, newTask);
return lines.join("\n");
});
}

getVaultSearcher(): VaultSearcher {
let vaultSearcher: VaultSearcher;
switch (this.plugin.settings.backendToFindTasks) {
case backendToFindTasks.Dataview:
// Prepare dataview
vaultSearcher = new DataviewSearcher(this.app, this.plugin);
break;
default:
throw new Error("No valid backend to find tasks in vault selected");
}
return vaultSearcher;
}

getTaskFormatter(): EmojiTaskFormatter {
switch (this.plugin.settings.useTasksFormat) {
case supportedTasksPluginsFormat.Emoji:
return new EmojiTaskFormatter(this.app, this.plugin);
default:
throw new Error("No valid TaskFormat selected");
}
}

getTaskParser() {
let taskParser: TaskParser;
switch (this.plugin.settings.useTasksFormat) {
case supportedTasksPluginsFormat.Emoji:
taskParser = new EmojiTaskParser(this.app, this.plugin);
break;
default:
throw new Error("No valid TaskFormat selected");
}
return taskParser;
}

}

export {Processor};
Loading

0 comments on commit 5574cba

Please sign in to comment.