248 lines
7.5 KiB
TypeScript
248 lines
7.5 KiB
TypeScript
import { App, PluginSettingTab, Setting, Notice } from "obsidian";
|
|
import { existsSync, statSync } from "fs";
|
|
import { homedir } from "os";
|
|
import type OpenCodePlugin from "./main";
|
|
import type { ViewLocation } from "./types";
|
|
|
|
function expandTilde(path: string): string {
|
|
if (path === "~") {
|
|
return homedir();
|
|
}
|
|
if (path.startsWith("~/")) {
|
|
return path.replace("~", homedir());
|
|
}
|
|
return path;
|
|
}
|
|
|
|
export class OpenCodeSettingTab extends PluginSettingTab {
|
|
plugin: OpenCodePlugin;
|
|
private validateTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
constructor(app: App, plugin: OpenCodePlugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
display(): void {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
containerEl.createEl("h2", { text: "OpenCode Settings" });
|
|
containerEl.createEl("h3", { text: "Server Configuration" });
|
|
|
|
new Setting(containerEl)
|
|
.setName("Port")
|
|
.setDesc("Port number for the OpenCode web server")
|
|
.addText((text) =>
|
|
text
|
|
.setPlaceholder("14096")
|
|
.setValue(this.plugin.settings.port.toString())
|
|
.onChange(async (value) => {
|
|
const port = parseInt(value, 10);
|
|
if (!isNaN(port) && port > 0 && port < 65536) {
|
|
this.plugin.settings.port = port;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
})
|
|
);
|
|
|
|
new Setting(containerEl)
|
|
.setName("Hostname")
|
|
.setDesc("Hostname to bind the server to (usually 127.0.0.1)")
|
|
.addText((text) =>
|
|
text
|
|
.setPlaceholder("127.0.0.1")
|
|
.setValue(this.plugin.settings.hostname)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.hostname = value || "127.0.0.1";
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
|
|
new Setting(containerEl)
|
|
.setName("OpenCode path")
|
|
.setDesc(
|
|
"Path to the OpenCode executable. Leave as 'opencode' if it's in your PATH."
|
|
)
|
|
.addText((text) =>
|
|
text
|
|
.setPlaceholder("opencode")
|
|
.setValue(this.plugin.settings.opencodePath)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.opencodePath = value || "opencode";
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
|
|
new Setting(containerEl)
|
|
.setName("Project directory")
|
|
.setDesc(
|
|
"Override the starting directory for OpenCode. Leave empty to use the vault root. Supports ~ for home directory."
|
|
)
|
|
.addText((text) =>
|
|
text
|
|
.setPlaceholder("/path/to/project or ~/project")
|
|
.setValue(this.plugin.settings.projectDirectory)
|
|
.onChange((value) => {
|
|
// Debounce validation to avoid spamming notices on every keypress
|
|
if (this.validateTimeout) {
|
|
clearTimeout(this.validateTimeout);
|
|
}
|
|
this.validateTimeout = setTimeout(async () => {
|
|
await this.validateAndSetProjectDirectory(value);
|
|
}, 500);
|
|
})
|
|
);
|
|
|
|
containerEl.createEl("h3", { text: "Behavior" });
|
|
|
|
new Setting(containerEl)
|
|
.setName("Auto-start server")
|
|
.setDesc(
|
|
"Automatically start the OpenCode server when Obsidian opens (not recommended for faster startup)"
|
|
)
|
|
.addToggle((toggle) =>
|
|
toggle
|
|
.setValue(this.plugin.settings.autoStart)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.autoStart = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
|
|
new Setting(containerEl)
|
|
.setName("Default view location")
|
|
.setDesc(
|
|
"Where to open the OpenCode panel: sidebar opens in the right panel, main opens as a tab in the editor area"
|
|
)
|
|
.addDropdown((dropdown) =>
|
|
dropdown
|
|
.addOption("sidebar", "Sidebar")
|
|
.addOption("main", "Main window")
|
|
.setValue(this.plugin.settings.defaultViewLocation)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.defaultViewLocation = value as ViewLocation;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
|
|
containerEl.createEl("h3", { text: "Server Status" });
|
|
|
|
const statusContainer = containerEl.createDiv({ cls: "opencode-settings-status" });
|
|
this.renderServerStatus(statusContainer);
|
|
}
|
|
|
|
private async validateAndSetProjectDirectory(value: string): Promise<void> {
|
|
const trimmed = value.trim();
|
|
|
|
// Empty value is valid - means use vault root
|
|
if (!trimmed) {
|
|
await this.plugin.updateProjectDirectory("");
|
|
return;
|
|
}
|
|
|
|
// Validate absolute path (supports ~, /, and Windows drive letters)
|
|
if (!trimmed.startsWith("/") && !trimmed.startsWith("~") && !trimmed.match(/^[A-Za-z]:\\/)) {
|
|
new Notice("Project directory must be an absolute path (or start with ~)");
|
|
return;
|
|
}
|
|
|
|
const expanded = expandTilde(trimmed);
|
|
|
|
try {
|
|
if (!existsSync(expanded)) {
|
|
new Notice("Project directory does not exist");
|
|
return;
|
|
}
|
|
const stat = statSync(expanded);
|
|
if (!stat.isDirectory()) {
|
|
new Notice("Project directory path is not a directory");
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
new Notice(`Failed to validate path: ${(error as Error).message}`);
|
|
return;
|
|
}
|
|
|
|
await this.plugin.updateProjectDirectory(expanded);
|
|
}
|
|
|
|
private renderServerStatus(container: HTMLElement): void {
|
|
container.empty();
|
|
|
|
const state = this.plugin.getProcessState();
|
|
const statusText = {
|
|
stopped: "Stopped",
|
|
starting: "Starting...",
|
|
running: "Running",
|
|
error: "Error",
|
|
};
|
|
|
|
const statusClass = {
|
|
stopped: "status-stopped",
|
|
starting: "status-starting",
|
|
running: "status-running",
|
|
error: "status-error",
|
|
};
|
|
|
|
const statusEl = container.createDiv({ cls: "opencode-status-line" });
|
|
statusEl.createSpan({ text: "Status: " });
|
|
statusEl.createSpan({
|
|
text: statusText[state],
|
|
cls: `opencode-status-badge ${statusClass[state]}`,
|
|
});
|
|
|
|
if (state === "running") {
|
|
const urlEl = container.createDiv({ cls: "opencode-status-line" });
|
|
urlEl.createSpan({ text: "URL: " });
|
|
const linkEl = urlEl.createEl("a", {
|
|
text: this.plugin.getServerUrl(),
|
|
href: this.plugin.getServerUrl(),
|
|
});
|
|
linkEl.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
window.open(this.plugin.getServerUrl(), "_blank");
|
|
});
|
|
}
|
|
|
|
const buttonContainer = container.createDiv({ cls: "opencode-settings-buttons" });
|
|
|
|
if (state === "stopped" || state === "error") {
|
|
const startButton = buttonContainer.createEl("button", {
|
|
text: "Start Server",
|
|
cls: "mod-cta",
|
|
});
|
|
startButton.addEventListener("click", async () => {
|
|
await this.plugin.startServer();
|
|
this.renderServerStatus(container);
|
|
});
|
|
}
|
|
|
|
if (state === "running") {
|
|
const stopButton = buttonContainer.createEl("button", {
|
|
text: "Stop Server",
|
|
});
|
|
stopButton.addEventListener("click", () => {
|
|
this.plugin.stopServer();
|
|
this.renderServerStatus(container);
|
|
});
|
|
|
|
const restartButton = buttonContainer.createEl("button", {
|
|
text: "Restart Server",
|
|
cls: "mod-warning",
|
|
});
|
|
restartButton.addEventListener("click", async () => {
|
|
this.plugin.stopServer();
|
|
await this.plugin.startServer();
|
|
this.renderServerStatus(container);
|
|
});
|
|
}
|
|
|
|
if (state === "starting") {
|
|
buttonContainer.createSpan({
|
|
text: "Please wait...",
|
|
cls: "opencode-status-waiting",
|
|
});
|
|
}
|
|
}
|
|
}
|