Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Build

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build

- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: plugin
path: |
main.js
manifest.json
versions.json
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
# OpenCMD-PS
# OpenTerm

Obsidian plugin that adds context menu options to open files and folders in a terminal.

## Context Menu Options

- **Open in default app** - Opens the directory in the system default terminal (available on all platforms).
- **Open in PowerShell** - Opens the directory in PowerShell (Windows only).
- **Open in default terminal** - Opens the directory in the configured terminal (available on all platforms).
- **Open in PowerShell** - Opens the directory in PowerShell (available on all platforms with `pwsh` installed; falls back to `powershell` on Windows).
- **Open in CMD** - Opens the directory in Command Prompt (Windows only).

On Linux and macOS, only "Open in default app" is shown.
Each option can be toggled on or off from the plugin settings.

## Settings

The plugin provides a settings tab where you can configure:

- Which context menu items to show
- The PowerShell executable (default: `pwsh` for PowerShell 7+)
- The CMD executable (default: `cmd.exe`)
- The default terminal executable / app per OS

## Installation

Copy `main.js`, `manifest.json`, and `versions.json` into your vault's `.obsidian/plugins/openterm/` directory.

## Development

Expand All @@ -17,4 +30,3 @@ npm install
npm run build
```

Copy `main.js` and `manifest.json` into your vault's `.obsidian/plugins/opencmd-ps/` directory.
9 changes: 5 additions & 4 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"id": "opencmd-ps",
"name": "OpenCMD-PS",
"version": "1.0.0",
"id": "openterm",
"name": "OpenTerm",
"version": "1.1.0",
"minAppVersion": "0.15.0",
"description": "Adds context menu options to open files and folders in CMD, PowerShell, or the default terminal.",
"description": "Adds context menu options to open files and folders in a terminal, with configurable executables per OS.",
"author": "Daolyap",
"authorUrl": "https://github.com/Daolyap",
"isDesktopOnly": true
}
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "opencmd-ps",
"version": "1.0.0",
"description": "Obsidian plugin to open files and folders in CMD, PowerShell, or the default terminal.",
"name": "openterm",
"version": "1.1.0",
"description": "Obsidian plugin to open files and folders in a terminal, with configurable executables per OS.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
Expand Down
210 changes: 189 additions & 21 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
import { Plugin, TFolder, TAbstractFile, Platform } from "obsidian";
import { Plugin, PluginSettingTab, Setting, TFolder, TAbstractFile, Platform, App } from "obsidian";
import { execFile } from "child_process";
import * as path from "path";

const SECTION_ID = "opencmd-ps";
const SECTION_ID = "openterm";

interface OpenTermSettings {
showDefaultTerminal: boolean;
showPowerShell: boolean;
showCmd: boolean;
windowsDefaultExe: string;
powershellExe: string;
cmdExe: string;
macTerminalApp: string;
linuxTerminalExe: string;
}

const DEFAULT_SETTINGS: OpenTermSettings = {
showDefaultTerminal: true,
showPowerShell: true,
showCmd: true,
windowsDefaultExe: "cmd.exe",
powershellExe: "pwsh",
cmdExe: "cmd.exe",
macTerminalApp: "Terminal",
linuxTerminalExe: "x-terminal-emulator",
};

interface VaultAdapterWithBasePath {
getBasePath(): string;
Expand All @@ -19,63 +41,209 @@ function getDirectory(vaultBasePath: string, file: TAbstractFile): string {
return path.join(vaultBasePath, file.parent?.path ?? "");
}

function openInDefaultTerminal(directory: string): void {
function openInDefaultTerminal(directory: string, settings: OpenTermSettings): void {
if (Platform.isWin) {
execFile("cmd.exe", ["/c", "start", "cmd", "/k", `cd /d "${directory}"`]);
const exe = settings.windowsDefaultExe || "cmd.exe";
execFile("cmd.exe", ["/c", "start", exe, "/k", `cd /d "${directory}"`]);
} else if (Platform.isMacOS) {
execFile("open", ["-a", "Terminal", directory]);
const app = settings.macTerminalApp || "Terminal";
execFile("open", ["-a", app, directory]);
} else {
execFile("x-terminal-emulator", [`--working-directory=${directory}`], (err) => {
const exe = settings.linuxTerminalExe || "x-terminal-emulator";
execFile(exe, [`--working-directory=${directory}`], (err) => {
if (err) {
execFile("xdg-open", [directory]);
}
});
}
}

function openInPowerShell(directory: string): void {
execFile("cmd.exe", ["/c", "start", "powershell", "-NoExit", "-Command",
`Set-Location -LiteralPath '${directory.replace(/'/g, "''")}'`]);
function openInPowerShell(directory: string, settings: OpenTermSettings): void {
const exe = settings.powershellExe || "pwsh";
const psArgs = ["-NoExit", "-Command",
`Set-Location -LiteralPath '${directory.replace(/'/g, "''")}'`];

if (Platform.isWin) {
execFile("cmd.exe", ["/c", "start", exe, ...psArgs], (err) => {
const isNotFound = err && (err as NodeJS.ErrnoException).code === "ENOENT";
if (isNotFound && exe !== "powershell") {
execFile("cmd.exe", ["/c", "start", "powershell", ...psArgs]);
}
});
} else {
execFile(exe, psArgs);
}
}

function openInCmd(directory: string): void {
execFile("cmd.exe", ["/c", "start", "cmd", "/k", `cd /d "${directory}"`]);
function openInCmd(directory: string, settings: OpenTermSettings): void {
const exe = settings.cmdExe || "cmd.exe";
execFile("cmd.exe", ["/c", "start", exe, "/k", `cd /d "${directory}"`]);
}

export default class OpenCmdPsPlugin extends Plugin {
export default class OpenTermPlugin extends Plugin {
settings: OpenTermSettings;

async onload(): Promise<void> {
await this.loadSettings();
this.addSettingTab(new OpenTermSettingTab(this.app, this));

const adapter = this.app.vault.adapter;

this.registerEvent(
this.app.workspace.on("file-menu", (menu, file) => {
const basePath = hasBasePath(adapter) ? adapter.getBasePath() : "";
const directory = getDirectory(basePath, file);

menu.addItem((item) => {
item.setTitle("Open in default app")
.setIcon("terminal")
.setSection(SECTION_ID)
.onClick(() => openInDefaultTerminal(directory));
});
if (this.settings.showDefaultTerminal) {
menu.addItem((item) => {
item.setTitle("Open in terminal")
.setIcon("terminal")
.setSection(SECTION_ID)
.onClick(() => openInDefaultTerminal(directory, this.settings));
});
}

if (Platform.isWin) {
if (this.settings.showPowerShell) {
menu.addItem((item) => {
item.setTitle("Open in PowerShell")
.setIcon("terminal")
.setSection(SECTION_ID)
.onClick(() => openInPowerShell(directory));
.onClick(() => openInPowerShell(directory, this.settings));
});
}

if (Platform.isWin && this.settings.showCmd) {
menu.addItem((item) => {
item.setTitle("Open in CMD")
.setIcon("terminal")
.setSection(SECTION_ID)
.onClick(() => openInCmd(directory));
.onClick(() => openInCmd(directory, this.settings));
});
}
})
);
}

onunload(): void {}

async loadSettings(): Promise<void> {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}

async saveSettings(): Promise<void> {
await this.saveData(this.settings);
}
}

class OpenTermSettingTab extends PluginSettingTab {
plugin: OpenTermPlugin;

constructor(app: App, plugin: OpenTermPlugin) {
super(app, plugin);
this.plugin = plugin;
}

display(): void {
const { containerEl } = this;
containerEl.empty();

new Setting(containerEl).setName("Context menu items").setHeading();

new Setting(containerEl)
.setName("Show default terminal")
.setDesc("Show the 'Open in terminal' option in the context menu.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showDefaultTerminal).onChange(async (value) => {
this.plugin.settings.showDefaultTerminal = value;
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Show PowerShell")
.setDesc("Show the 'Open in PowerShell' option in the context menu.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showPowerShell).onChange(async (value) => {
this.plugin.settings.showPowerShell = value;
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Show CMD (Windows only)")
.setDesc("Show the 'Open in CMD' option in the context menu.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showCmd).onChange(async (value) => {
this.plugin.settings.showCmd = value;
await this.plugin.saveSettings();
})
);

new Setting(containerEl).setName("Executables").setHeading();

new Setting(containerEl)
.setName("PowerShell executable")
.setDesc("Executable for PowerShell. Uses 'pwsh' (PowerShell 7+) by default, falling back to 'powershell' on Windows if not found.")
.addText((text) =>
text
.setPlaceholder("pwsh")
.setValue(this.plugin.settings.powershellExe)
.onChange(async (value) => {
this.plugin.settings.powershellExe = value.trim();
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("CMD executable (Windows)")
.setDesc("Executable for Command Prompt on Windows.")
.addText((text) =>
text
.setPlaceholder("cmd.exe")
.setValue(this.plugin.settings.cmdExe)
.onChange(async (value) => {
this.plugin.settings.cmdExe = value.trim();
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Default terminal executable (Windows)")
.setDesc("Executable used for the default terminal option on Windows.")
.addText((text) =>
text
.setPlaceholder("cmd.exe")
.setValue(this.plugin.settings.windowsDefaultExe)
.onChange(async (value) => {
this.plugin.settings.windowsDefaultExe = value.trim();
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Terminal app (macOS)")
.setDesc("Application used for the default terminal option on macOS.")
.addText((text) =>
text
.setPlaceholder("Terminal")
.setValue(this.plugin.settings.macTerminalApp)
.onChange(async (value) => {
this.plugin.settings.macTerminalApp = value.trim();
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Terminal executable (Linux)")
.setDesc("Executable used for the default terminal option on Linux.")
.addText((text) =>
text
.setPlaceholder("x-terminal-emulator")
.setValue(this.plugin.settings.linuxTerminalExe)
.onChange(async (value) => {
this.plugin.settings.linuxTerminalExe = value.trim();
await this.plugin.saveSettings();
})
);
}
}
4 changes: 4 additions & 0 deletions versions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"1.0.0": "0.15.0",
"1.1.0": "0.15.0"
}