From 7d6d345f7759692aefebdef9b9fc5cc79776772b Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Fri, 1 May 2026 08:28:05 +1200 Subject: [PATCH] feat: status bar option to include the folder path --- .../TimesheetStatusBar.test.ts | 2 +- .../TimesheetStatusBar/TimesheetStatusBar.ts | 29 ++++- .../TimesheetStatusBarItem.test.ts | 104 +++++++++++++++--- .../TimesheetStatusBarItem.ts | 39 ++++++- src/main.ts | 7 +- src/settings-tab.ts | 15 +++ src/settings.ts | 2 + 7 files changed, 173 insertions(+), 25 deletions(-) diff --git a/src/components/TimesheetStatusBar/TimesheetStatusBar.test.ts b/src/components/TimesheetStatusBar/TimesheetStatusBar.test.ts index f0b1edd..f2ca2fe 100644 --- a/src/components/TimesheetStatusBar/TimesheetStatusBar.test.ts +++ b/src/components/TimesheetStatusBar/TimesheetStatusBar.test.ts @@ -35,7 +35,7 @@ describe("TimesheetStatusBar", () => { settings = createStore(defaultSettings); registry = new TimekeepRegistry(vault.asVault(), settings); - component = new TimesheetStatusBar(containerEl, app, registry); + component = new TimesheetStatusBar(containerEl, app, registry, settings); }); it("should load without error", () => { diff --git a/src/components/TimesheetStatusBar/TimesheetStatusBar.ts b/src/components/TimesheetStatusBar/TimesheetStatusBar.ts index 8c93673..63a1d8d 100644 --- a/src/components/TimesheetStatusBar/TimesheetStatusBar.ts +++ b/src/components/TimesheetStatusBar/TimesheetStatusBar.ts @@ -1,6 +1,8 @@ import { type App } from "obsidian"; import { Component } from "obsidian"; +import { TimekeepSettings } from "@/settings"; +import { Store } from "@/store"; import { assert } from "@/utils/assert"; import { TimesheetStatusBarItem } from "./TimesheetStatusBarItem"; @@ -20,17 +22,25 @@ export class TimesheetStatusBar extends Component { app: App; /** Access to the app registry */ registry: TimekeepRegistry; + /** Access to the timekeep settings */ + settings: Store; /** Currently rendered items */ items: TimesheetStatusBarItem[] = []; - constructor(containerEl: HTMLElement, app: App, registry: TimekeepRegistry) { + constructor( + containerEl: HTMLElement, + app: App, + registry: TimekeepRegistry, + settings: Store + ) { super(); this.#containerEl = containerEl; this.app = app; this.registry = registry; + this.settings = settings; } onload(): void { @@ -51,6 +61,10 @@ export class TimesheetStatusBar extends Component { } render() { + // No need to subscribe to settings, this component is recreated when settings changes + const settings = this.settings.getState(); + + const entries = this.registry.entries.getState(); const runningEntries = TimekeepRegistry.getRunningEntries(entries); @@ -61,14 +75,21 @@ export class TimesheetStatusBar extends Component { // Load the new children for (const runningEntry of runningEntries) { - this.renderEntry(runningEntry.running, runningEntry.ref); + this.renderEntry(runningEntry.running, runningEntry.ref, settings); } } - renderEntry(entry: TimeEntry, ref: TimekeepRegistryItemRef) { + renderEntry(entry: TimeEntry, ref: TimekeepRegistryItemRef, settings: TimekeepSettings) { const wrapperEl = this.wrapperEl; assert(wrapperEl, "Wrapper element should be defined on render"); - const item = new TimesheetStatusBarItem(wrapperEl, this.app, this.registry, entry, ref); + const item = new TimesheetStatusBarItem( + wrapperEl, + this.app, + this.registry, + entry, + ref, + settings.statusBarShowFolderPath + ); this.items.push(item); item.load(); } diff --git a/src/components/TimesheetStatusBar/TimesheetStatusBarItem.test.ts b/src/components/TimesheetStatusBar/TimesheetStatusBarItem.test.ts index 7d6b2f6..fd9b568 100644 --- a/src/components/TimesheetStatusBar/TimesheetStatusBarItem.test.ts +++ b/src/components/TimesheetStatusBar/TimesheetStatusBarItem.test.ts @@ -51,20 +51,34 @@ describe("TimesheetStatusBarItem", () => { it("should load without error", () => { const file = vault.addFile("test.timekeep", ""); - const component = new TimesheetStatusBarItem(containerEl, app, registry, entry, { - file, - type: TimekeepEntryItemType.FILE, - }); + const component = new TimesheetStatusBarItem( + containerEl, + app, + registry, + entry, + { + file, + type: TimekeepEntryItemType.FILE, + }, + false + ); expect(() => component.load()).not.toThrow(); }); it("should call onStop when the stop icon is clicked", () => { const file = vault.addFile("test.timekeep", ""); - const component = new TimesheetStatusBarItem(containerEl, app, registry, entry, { - file, - type: TimekeepEntryItemType.FILE, - }); + const component = new TimesheetStatusBarItem( + containerEl, + app, + registry, + entry, + { + file, + type: TimekeepEntryItemType.FILE, + }, + false + ); const onStop = vi.spyOn(component, "onStop"); component.load(); @@ -85,10 +99,17 @@ describe("TimesheetStatusBarItem", () => { // .mockRejectedValue(new Error("failed to stop")); - const component = new TimesheetStatusBarItem(containerEl, app, registry, entry, { - file, - type: TimekeepEntryItemType.FILE, - }); + const component = new TimesheetStatusBarItem( + containerEl, + app, + registry, + entry, + { + file, + type: TimekeepEntryItemType.FILE, + }, + false + ); const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); const onStop = vi.spyOn(component, "onStop"); component.load(); @@ -101,10 +122,17 @@ describe("TimesheetStatusBarItem", () => { it("should call onOpen when the content area is clicked", () => { const file = vault.addFile("test.timekeep", ""); - const component = new TimesheetStatusBarItem(containerEl, app, registry, entry, { - file, - type: TimekeepEntryItemType.FILE, - }); + const component = new TimesheetStatusBarItem( + containerEl, + app, + registry, + entry, + { + file, + type: TimekeepEntryItemType.FILE, + }, + false + ); const onOpen = vi.spyOn(component, "onOpen"); component.load(); @@ -118,4 +146,48 @@ describe("TimesheetStatusBarItem", () => { expect(onOpen).toHaveBeenCalledTimes(1); }); + + it("nested file path should be included in name when showFolderPath is true", () => { + const file = vault.addFile("nested/path/test.timekeep", ""); + const component = new TimesheetStatusBarItem( + containerEl, + app, + registry, + entry, + { + file, + type: TimekeepEntryItemType.FILE, + }, + true + ); + + component.load(); + + const nameEl = component.containerEl.querySelector(".timekeep-status-item__name"); + expect(nameEl).not.toBeNull(); + + expect(nameEl!.textContent.startsWith("nested/path: ")).toBeTruthy(); + }); + + it("nested file path should not be included in name when showFolderPath is false", () => { + const file = vault.addFile("nested/path/test.timekeep", ""); + const component = new TimesheetStatusBarItem( + containerEl, + app, + registry, + entry, + { + file, + type: TimekeepEntryItemType.FILE, + }, + false + ); + + component.load(); + + const nameEl = component.containerEl.querySelector(".timekeep-status-item__name"); + expect(nameEl).not.toBeNull(); + + expect(nameEl!.textContent.startsWith("nested/path: ")).not.toBeTruthy(); + }); }); diff --git a/src/components/TimesheetStatusBar/TimesheetStatusBarItem.ts b/src/components/TimesheetStatusBar/TimesheetStatusBarItem.ts index 57dcfaa..3896bce 100644 --- a/src/components/TimesheetStatusBar/TimesheetStatusBarItem.ts +++ b/src/components/TimesheetStatusBar/TimesheetStatusBarItem.ts @@ -6,7 +6,11 @@ import { TimesheetEntryDuration } from "@/components/TimesheetEntryDuration"; import { TimeEntry } from "@/timekeep/schema"; -import { TimekeepRegistry, TimekeepRegistryItemRef } from "@/service/registry"; +import { + TimekeepEntryItemType, + TimekeepRegistry, + TimekeepRegistryItemRef, +} from "@/service/registry"; export class TimesheetStatusBarItem extends DomComponent { /** Access to the obsidian app */ @@ -15,15 +19,20 @@ export class TimesheetStatusBarItem extends DomComponent { entry: TimeEntry; /** The registry for timekeeps */ registry: TimekeepRegistry; + /** The reference to the item */ ref: TimekeepRegistryItemRef; + /** Whether to show the folder path in the name */ + showFolderPath: boolean; + constructor( containerEl: HTMLElement, app: App, registry: TimekeepRegistry, entry: TimeEntry, - ref: TimekeepRegistryItemRef + ref: TimekeepRegistryItemRef, + showFolderPath: boolean ) { super(containerEl); @@ -31,6 +40,7 @@ export class TimesheetStatusBarItem extends DomComponent { this.registry = registry; this.entry = entry; this.ref = ref; + this.showFolderPath = showFolderPath; } onload(): void { @@ -53,7 +63,8 @@ export class TimesheetStatusBarItem extends DomComponent { contentEl.createSpan({ cls: "timekeep-status-item__name", - text: entry.name + ":", + text: this.getFolderPath() + entry.name + ":", + title: this.getDisplayTitle(), }); this.addChild(new TimesheetEntryDuration(contentEl, entry)); @@ -62,6 +73,28 @@ export class TimesheetStatusBarItem extends DomComponent { this.registerDomEvent(contentEl, "click", this.onOpen.bind(this)); } + getDisplayTitle() { + switch (this.ref.type) { + case TimekeepEntryItemType.FILE: + return `${this.ref.file.path}`; + case TimekeepEntryItemType.MARKDOWN: + return `${this.ref.file.path} - ${this.ref.position.startLine}:${this.ref.position.endLine}`; + /* v8 ignore start -- @preserve */ + default: { + throw new Error("unknown entry type"); + } + /* v8 ignore stop -- @preserve */ + } + } + + getFolderPath() { + if (!this.showFolderPath) return ""; + const path = this.ref.file.path; + const parts = path.split("/"); + if (parts.length < 2) return ""; + return parts.slice(0, parts.length - 1).join("/") + ": "; + } + async onStop() { try { await this.registry.tryStopEntry(this.ref); diff --git a/src/main.ts b/src/main.ts index 76d178c..4b72842 100644 --- a/src/main.ts +++ b/src/main.ts @@ -198,7 +198,12 @@ export default class TimekeepPlugin extends Plugin { if (!settings.statusBarEnabled) return; const containerEl = this.addStatusBarItem(); - const statusBarView = new TimesheetStatusBar(containerEl, this.app, this.registry); + const statusBarView = new TimesheetStatusBar( + containerEl, + this.app, + this.registry, + this.settingsStore + ); this.addChild(statusBarView); this.#statusBarView = statusBarView; } diff --git a/src/settings-tab.ts b/src/settings-tab.ts index 4d18378..3236671 100644 --- a/src/settings-tab.ts +++ b/src/settings-tab.ts @@ -442,6 +442,21 @@ export class TimekeepSettingsTab extends PluginSettingTab { }); }); + new Setting(this.containerEl) + .setName("Show Folder Path") + .setDesc( + 'Whether to include the folder path of the file in the status item (i.e "Path/To/Entry: Block 1: 3h 5min 30s").' + ) + .addToggle((t) => { + t.setValue(settings.statusBarShowFolderPath); + t.onChange((v) => { + this.settingsStore.setState((currentValue) => ({ + ...currentValue, + statusBarShowFolderPath: v, + })); + }); + }); + // Autocomplete section new Setting(this.containerEl) .setName("Autocomplete") diff --git a/src/settings.ts b/src/settings.ts index 950d725..90151e5 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -74,6 +74,7 @@ export interface TimekeepSettings { registryConcurrencyLimit: number; statusBarEnabled: boolean; + statusBarShowFolderPath: boolean; autocompleteEnabled: boolean; } @@ -104,6 +105,7 @@ export const defaultSettings: TimekeepSettings = { registryConcurrencyLimit: 15, statusBarEnabled: true, + statusBarShowFolderPath: true, autocompleteEnabled: true, };