diff --git a/src/Common.ts b/src/Common.ts
index 3d4cdaf6..dfd82807 100644
--- a/src/Common.ts
+++ b/src/Common.ts
@@ -1,6 +1,7 @@
import {App, Editor, FileSystemAdapter, MarkdownView, normalizePath} from "obsidian";
-import {OperatingSystemName} from "./settings/ShellCommandsPluginSettings";
+import {PlatformId} from "./settings/ShellCommandsPluginSettings";
import {platform} from "os";
+import * as path from "path";
export function getVaultAbsolutePath(app: App) {
// Original code was copied 2021-08-22 from https://github.com/phibr0/obsidian-open-with/blob/84f0e25ba8e8355ff83b22f4050adde4cc6763ea/main.ts#L66-L67
@@ -20,9 +21,10 @@ export function isWindows() {
}
/**
- * This is just a wrapper around platform() in order to cast the type to OperatingSystemName.
+ * This is just a wrapper around platform() in order to cast the type to PlatformId.
+ * TODO: Consider renaming this to getPlatformId().
*/
-export function getOperatingSystem(): OperatingSystemName {
+export function getOperatingSystem(): PlatformId {
// @ts-ignore In theory, platform() can return an OS name not included in OperatingSystemName. But as Obsidian
// currently does not support anything else than Windows, Mac and Linux (except mobile platforms, but they are
// ruled out by the manifest of this plugin), it should be safe to assume that the current OS is one of those
@@ -99,6 +101,10 @@ export function normalizePath2(path: string) {
return path;
}
+export function extractFileName(file_path: string) {
+ return path.parse(file_path).base;
+}
+
export function joinObjectProperties(object: {}, glue: string) {
let result = "";
for (let property_name in object) {
diff --git a/src/Migrations.ts b/src/Migrations.ts
index 62bf1899..a3541bb5 100644
--- a/src/Migrations.ts
+++ b/src/Migrations.ts
@@ -86,9 +86,9 @@ function MigrateShellCommandToPlatforms(plugin: ShellCommandsPlugin) {
let shell_command_configuration: ShellCommandConfiguration = plugin.settings.shell_commands[shell_command_id];
if (undefined !== shell_command_configuration.shell_command) {
// The shell command should be migrated.
- if (undefined === shell_command_configuration.platforms || shell_command_configuration.platforms.default === "") {
+ if (undefined === shell_command_configuration.platform_specific_commands || shell_command_configuration.platform_specific_commands.default === "") {
console.log("Migrating shell command #" + shell_command_id + ": shell_command string will be moved to platforms.default: " + shell_command_configuration.shell_command);
- shell_command_configuration.platforms = {
+ shell_command_configuration.platform_specific_commands = {
default: shell_command_configuration.shell_command,
};
delete shell_command_configuration.shell_command;
diff --git a/src/Shell.ts b/src/Shell.ts
index 7a3bb150..67435629 100644
--- a/src/Shell.ts
+++ b/src/Shell.ts
@@ -1,4 +1,5 @@
-import {isWindows} from "./Common";
+import {extractFileName, getOperatingSystem, isWindows} from "./Common";
+import {PlatformShells} from "./settings/ShellCommandsPluginSettings";
export function getUsersDefaultShell(): string {
if (isWindows()) {
@@ -6,4 +7,16 @@ export function getUsersDefaultShell(): string {
} else {
return process.env.SHELL;
}
+}
+
+export function isShellSupported(shell: string) {
+ const shell_file_name = extractFileName(shell);
+ const supported_shells = PlatformShells[getOperatingSystem()];
+ for (let supported_shell_path in supported_shells) {
+ if (supported_shell_path.substr(-shell_file_name.length, shell_file_name.length).toLowerCase() === shell_file_name.toLowerCase()) {
+ // If supported_shell_path (e.g. /bin/bash or CMD.EXE) ends with shell_file_name (e.g. bash, derived from /bin/bash or CMD.EXE, derived from C:\System32\CMD.EXE), then the shell can be considered to be supported.
+ return true;
+ }
+ }
+ return false;
}
\ No newline at end of file
diff --git a/src/TShellCommand.ts b/src/TShellCommand.ts
index 5aaee0ca..4fd72d36 100644
--- a/src/TShellCommand.ts
+++ b/src/TShellCommand.ts
@@ -47,6 +47,10 @@ export class TShellCommand {
}
}
+ public getShells() {
+ return this.configuration.shells;
+ }
+
/**
* Returns a shell command string specific for the current operating system, or a generic shell command if this shell
* command does not have an explicit version for the current OS.
@@ -55,13 +59,13 @@ export class TShellCommand {
let operating_system = getOperatingSystem();
// Check if the shell command has defined a specific command for this operating system.
- if (undefined === this.configuration.platforms[operating_system]) {
+ if (undefined === this.configuration.platform_specific_commands[operating_system]) {
// No command is defined specifically for this operating system.
// Return an "OS agnostic" command.
- return this.configuration.platforms.default;
+ return this.configuration.platform_specific_commands.default;
} else {
// The shell command has defined a specific command for this operating system.
- return this.configuration.platforms[operating_system];
+ return this.configuration.platform_specific_commands[operating_system];
}
}
@@ -71,7 +75,11 @@ export class TShellCommand {
* current platform into account.
*/
public getDefaultShellCommand() {
- return this.configuration.platforms.default;
+ return this.configuration.platform_specific_commands.default;
+ }
+
+ public getPlatformSpecificShellCommands() {
+ return this.configuration.platform_specific_commands;
}
public getAlias() {
diff --git a/src/main.ts b/src/main.ts
index c38e4a92..eb9b8314 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -16,7 +16,7 @@ import {ConfirmExecutionModal} from "./ConfirmExecutionModal";
import {handleShellCommandOutput} from "./output_channels/OutputChannelDriverFunctions";
import {BaseEncodingOptions} from "fs";
import {TShellCommand, TShellCommandContainer} from "./TShellCommand";
-import {getUsersDefaultShell} from "./Shell";
+import {getUsersDefaultShell, isShellSupported} from "./Shell";
import {TShellCommandTemporary} from "./TShellCommandTemporary";
export default class ShellCommandsPlugin extends Plugin {
@@ -111,7 +111,7 @@ export default class ShellCommandsPlugin extends Plugin {
} else {
// Variable parsing succeeded.
// Use the parsed values.
- preparsed_t_shell_command.getConfiguration().platforms = {default: parsed_shell_command}; // Overrides all possible OS specific shell command versions.
+ preparsed_t_shell_command.getConfiguration().platform_specific_commands = {default: parsed_shell_command}; // Overrides all possible OS specific shell command versions.
}
// Also parse variables in an alias, in case the command has one. Variables in aliases do not do anything practical, but they can reveal the user what variables are used in the command.
@@ -244,6 +244,17 @@ export default class ShellCommandsPlugin extends Plugin {
return;
}
+ // Check that the currently defined shell is supported by this plugin. If using system default shell, it's possible
+ // that the shell is something that is not supported. Also, the settings file can be edited manually, and incorrect
+ // shell can be written there.
+ const shell = t_shell_command.getShell();
+ if (!isShellSupported(shell)) {
+ console.log("Shell is not supported: " + shell);
+ this.newError("This plugin does not support the following shell: " + shell);
+ return;
+ }
+
+
// Check that the working directory exists and is a folder
if (!fs.existsSync(working_directory)) {
// Working directory does not exist
@@ -261,7 +272,7 @@ export default class ShellCommandsPlugin extends Plugin {
// Prepare execution options
let options: BaseEncodingOptions & ExecOptions = {
"cwd": working_directory,
- "shell": t_shell_command.getShell(),
+ "shell": shell,
};
// Execute the shell command
@@ -368,7 +379,7 @@ export default class ShellCommandsPlugin extends Plugin {
public getDefaultShell(): string {
let operating_system = getOperatingSystem()
- let shell_name = this.settings.default_shell[operating_system]; // Can also be undefined.
+ let shell_name = this.settings.default_shells[operating_system]; // Can also be undefined.
if (undefined === shell_name) {
shell_name = getUsersDefaultShell();
}
diff --git a/src/settings/ShellCommandConfiguration.ts b/src/settings/ShellCommandConfiguration.ts
index db39e2b4..edca0a5f 100644
--- a/src/settings/ShellCommandConfiguration.ts
+++ b/src/settings/ShellCommandConfiguration.ts
@@ -11,7 +11,7 @@ export interface ShellCommandConfiguration {
* - key: platform (= OS) name
* - value: shell command
*/
- platforms: IPlatformSpecificStringWithDefault;
+ platform_specific_commands: IPlatformSpecificStringWithDefault;
shells: IPlatformSpecificString;
alias: string;
confirm_execution: boolean;
@@ -29,7 +29,7 @@ export interface ShellCommandConfiguration {
export function newShellCommandConfiguration(shell_command: string = ""): ShellCommandConfiguration {
return {
- platforms: {
+ platform_specific_commands: {
default: shell_command,
},
shells: {},
diff --git a/src/settings/ShellCommandExtraOptionsModal.ts b/src/settings/ShellCommandExtraOptionsModal.ts
index b24e9458..767357d9 100644
--- a/src/settings/ShellCommandExtraOptionsModal.ts
+++ b/src/settings/ShellCommandExtraOptionsModal.ts
@@ -4,6 +4,13 @@ import {ShellCommandSettingGroup, ShellCommandsSettingsTab} from "./ShellCommand
import {getOutputChannelDriversOptionList} from "../output_channels/OutputChannelDriverFunctions";
import {OutputChannel, OutputChannelOrder, OutputStream} from "../output_channels/OutputChannel";
import {TShellCommand} from "../TShellCommand";
+import {PlatformId, PlatformNames} from "./ShellCommandsPluginSettings";
+import {createShellSelectionField} from "./setting_elements/CreateShellSelectionField";
+import {
+ generateIgnoredErrorCodesIconTitle,
+ generateShellCommandFieldName
+} from "./setting_elements/CreateShellCommandField";
+import {createPlatformSpecificShellCommandField} from "./setting_elements/CreatePlatformSpecificShellCommandField";
export class ShellCommandExtraOptionsModal extends Modal {
static OPTIONS_SUMMARY = "Alias, Output, Confirmation, Ignore errors";
@@ -26,6 +33,9 @@ export class ShellCommandExtraOptionsModal extends Modal {
onOpen() {
this.modalEl.createEl("h2", {text: this.t_shell_command.getDefaultShellCommand()});
+ // Make the modal scrollable if it has more content than what fits in the screen.
+ this.modalEl.addClass("SC-scrollable");
+
// Tab headers
let tab_header = this.modalEl.createEl("div", {attr: {class: "SC-tab-header"}});
tab_header.createEl("button", {text: "General"}).onclick = () => {
@@ -53,7 +63,7 @@ export class ShellCommandExtraOptionsModal extends Modal {
this.plugin.obsidian_commands[this.shell_command_id].name = this.plugin.generateObsidianCommandName(this.t_shell_command);
// UpdateShell commands settings panel
- this.name_setting.setName(this.setting_tab.generateCommandFieldName(this.shell_command_id, this.t_shell_command));
+ this.name_setting.setName(generateShellCommandFieldName(this.shell_command_id, this.t_shell_command));
// Save
await this.plugin.saveSettings();
@@ -132,7 +142,7 @@ export class ShellCommandExtraOptionsModal extends Modal {
let icon_container = this.name_setting.nameEl.find("span.shell-commands-ignored-error-codes-icon-container");
if (this.t_shell_command.getIgnoreErrorCodes().length) {
// Show icon
- icon_container.setAttr("aria-label", this.setting_tab.generateIgnoredErrorCodesIconTitle(this.t_shell_command.getIgnoreErrorCodes()));
+ icon_container.setAttr("aria-label", generateIgnoredErrorCodesIconTitle(this.t_shell_command.getIgnoreErrorCodes()));
icon_container.removeClass("shell-commands-hide");
} else {
// Hide icon
@@ -141,6 +151,15 @@ export class ShellCommandExtraOptionsModal extends Modal {
})
)
;
+
+ // Platform specific shell selection
+ createShellSelectionField(this.plugin, container_element, this.t_shell_command.getShells(), false);
+
+ // Platform specific shell commands
+ let platform_id: PlatformId;
+ for (platform_id in PlatformNames) {
+ createPlatformSpecificShellCommandField(this.plugin, container_element, this.t_shell_command, platform_id);
+ }
}
private newOutputChannelSetting(title: string, output_stream_name: OutputStream, description: string = "") {
diff --git a/src/settings/ShellCommandsPluginSettings.ts b/src/settings/ShellCommandsPluginSettings.ts
index e76776ce..f8b72efd 100644
--- a/src/settings/ShellCommandsPluginSettings.ts
+++ b/src/settings/ShellCommandsPluginSettings.ts
@@ -2,7 +2,7 @@
import {ShellCommandsConfiguration} from "./ShellCommandConfiguration";
export interface ShellCommandsPluginSettings {
- default_shell: IPlatformSpecificString;
+ default_shells: IPlatformSpecificString;
working_directory: string;
preview_variables_in_command_palette: boolean;
shell_commands: ShellCommandsConfiguration;
@@ -15,7 +15,7 @@ export interface ShellCommandsPluginSettings {
}
export const DEFAULT_SETTINGS: ShellCommandsPluginSettings = {
- default_shell: {},
+ default_shells: {},
working_directory: "",
preview_variables_in_command_palette: true,
shell_commands: {},
@@ -35,7 +35,13 @@ export const DEFAULT_SETTINGS: ShellCommandsPluginSettings = {
*
* @see NodeJS.Platform
*/
-export type OperatingSystemName = "darwin" | "linux" | "win32";
+export type PlatformId = "darwin" | "linux" | "win32";
+
+export const PlatformNames: IPlatformSpecificString = {
+ darwin: "Macintosh",
+ linux: "Linux",
+ win32: "Windows",
+};
/**
* All OSes supported by the Shell commands plugin.
@@ -55,3 +61,19 @@ export interface IPlatformSpecificString {
export interface IPlatformSpecificStringWithDefault extends IPlatformSpecificString{
default: string,
}
+
+export const PlatformShells = {
+ darwin: {
+ "/bin/bash": "Bash",
+ "/bin/zsh": "Zsh (Z shell)"
+ },
+ linux: {
+ "/bin/bash": "Bash",
+ "/bin/zsh": "Zsh (Z shell)"
+ },
+ win32: {
+ "pwsh.exe": "PowerShell Core",
+ "PowerShell.exe": "PowerShell 5",
+ "CMD.EXE": "cmd.exe",
+ }
+}
diff --git a/src/settings/ShellCommandsSettingsTab.ts b/src/settings/ShellCommandsSettingsTab.ts
index 4fde08b6..e602c00d 100644
--- a/src/settings/ShellCommandsSettingsTab.ts
+++ b/src/settings/ShellCommandsSettingsTab.ts
@@ -1,12 +1,9 @@
-import {App, Hotkey, PluginSettingTab, setIcon, Setting} from "obsidian";
+import {App, PluginSettingTab, Setting} from "obsidian";
import ShellCommandsPlugin from "../main";
import {getVaultAbsolutePath} from "../Common";
-import {ShellCommandExtraOptionsModal} from "./ShellCommandExtraOptionsModal";
-import {ShellCommandDeleteModal} from "./ShellCommandDeleteModal";
import {getShellCommandVariableInstructions} from "../variables/ShellCommandVariableInstructions";
-import {parseShellCommandVariables} from "../variables/parseShellCommandVariables";
-import {getHotkeysForShellCommand, HotkeyToString} from "../Hotkeys";
-import {TShellCommand} from "../TShellCommand";
+import {createShellSelectionField} from "./setting_elements/CreateShellSelectionField";
+import {createShellCommandField} from "./setting_elements/CreateShellCommandField";
export class ShellCommandsSettingsTab extends PluginSettingTab {
plugin: ShellCommandsPlugin;
@@ -38,12 +35,15 @@ export class ShellCommandsSettingsTab extends PluginSettingTab {
)
;
+ // Platforms' default shells
+ createShellSelectionField(this.plugin, containerEl, this.plugin.settings.default_shells, true);
+
// A
element for all command input fields. New command fields can be created at the bottom of this element.
let command_fields_container = containerEl.createEl("div");
// Fields for modifying existing commands
for (let command_id in this.plugin.getTShellCommands()) {
- this.createCommandField(command_fields_container, command_id);
+ createShellCommandField(this.plugin, command_fields_container, command_id);
}
// "New command" button
@@ -51,7 +51,7 @@ export class ShellCommandsSettingsTab extends PluginSettingTab {
.addButton(button => button
.setButtonText("New command")
.onClick(async () => {
- this.createCommandField(command_fields_container, "new");
+ createShellCommandField(this.plugin, command_fields_container, "new");
console.log("New empty command created.");
})
)
@@ -104,142 +104,6 @@ export class ShellCommandsSettingsTab extends PluginSettingTab {
this.rememberScrollPosition(containerEl);
}
- /**
- *
- * @param container_element
- * @param shell_command_id Either a string formatted integer ("0", "1" etc) or "new" if it's a field for a command that does not exist yet.
- */
- createCommandField(container_element: HTMLElement, shell_command_id: string) {
- let is_new = "new" === shell_command_id;
- let t_shell_command: TShellCommand;
- if (is_new) {
- // Create an empty command
- t_shell_command = this.plugin.newTShellCommand();
- } else {
- // Use an old shell command
- t_shell_command = this.plugin.getTShellCommands()[shell_command_id];
- }
- console.log("Create command field for command #" + shell_command_id + (is_new ? " (NEW)" : ""));
- let shell_command: string;
- if (is_new) {
- shell_command = "";
- } else {
- shell_command = t_shell_command.getDefaultShellCommand();
- }
- let setting_group: ShellCommandSettingGroup = {
- name_setting:
- new Setting(container_element)
- .setName(this.generateCommandFieldName(shell_command_id, this.plugin.getTShellCommands()[shell_command_id]))
- .addExtraButton(button => button
- .setTooltip("Execute now")
- .setIcon("run-command")
- .onClick(() => {
- // Execute the shell command now (for trying it out in the settings)
- let t_shell_command = this.plugin.getTShellCommands()[shell_command_id];
- let parsed_shell_command = parseShellCommandVariables(this.plugin, t_shell_command.getShellCommand());
- if (Array.isArray(parsed_shell_command)) {
- this.plugin.newErrors(parsed_shell_command);
- } else {
- this.plugin.confirmAndExecuteShellCommand(parsed_shell_command, t_shell_command);
- }
- })
- )
- .addExtraButton(button => button
- .setTooltip(ShellCommandExtraOptionsModal.OPTIONS_SUMMARY)
- .onClick(async () => {
- // Open an extra options modal
- let modal = new ShellCommandExtraOptionsModal(this.app, this.plugin, shell_command_id, setting_group, this);
- modal.open();
- })
- )
- .addExtraButton(button => button
- .setTooltip("Delete this shell command")
- .setIcon("trash")
- .onClick(async () => {
- // Open a delete modal
- let modal = new ShellCommandDeleteModal(this.plugin, shell_command_id, setting_group, container_element);
- modal.open();
- })
- )
- .setClass("shell-commands-name-setting")
- ,
- shell_command_setting:
- new Setting(container_element)
- .addText(text => text
- .setPlaceholder("Enter your command")
- .setValue(shell_command)
- .onChange(async (field_value) => {
- let shell_command = field_value;
- setting_group.preview_setting.setDesc(this.getShellCommandPreview(shell_command));
-
- if (is_new) {
- console.log("Creating new command " + shell_command_id + ": " + shell_command);
- } else {
- console.log("Command " + shell_command_id + " gonna change to: " + shell_command);
- }
-
- // Do this in both cases, when creating a new command and when changing an old one:
- t_shell_command.getConfiguration().platforms.default = shell_command;
-
- if (is_new) {
- // Create a new command
- // this.plugin.registerShellCommand(t_shell_command); // I don't think this is needed to be done anymore
- console.log("Command created.");
- } else {
- // Change an old command
- this.plugin.obsidian_commands[shell_command_id].name = this.plugin.generateObsidianCommandName(this.plugin.getTShellCommands()[shell_command_id]); // Change the command's name in Obsidian's command palette.
- console.log("Command changed.");
- }
- await this.plugin.saveSettings();
- })
- )
- .setClass("shell-commands-shell-command-setting")
- ,
- preview_setting:
- new Setting(container_element)
- .setDesc(this.getShellCommandPreview(shell_command))
- .setClass("shell-commands-preview-setting")
- ,
- };
-
- // Informational icons (= non-clickable)
- let icon_container = setting_group.name_setting.nameEl.createEl("span", {attr: {class: "shell-commands-main-icon-container"}});
-
- // "Ask confirmation" icon.
- let confirm_execution_icon_container = icon_container.createEl("span", {attr: {"aria-label": "Asks confirmation before execution.", class: "shell-commands-confirm-execution-icon-container"}});
- setIcon(confirm_execution_icon_container, "languages");
- if (!t_shell_command.getConfirmExecution()) {
- // Do not display the icon for commands that do not use confirmation.
- confirm_execution_icon_container.addClass("shell-commands-hide");
- }
-
- // "Ignored error codes" icon
- let ignored_error_codes_icon_container = icon_container.createEl("span", {attr: {"aria-label": this.generateIgnoredErrorCodesIconTitle(t_shell_command.getIgnoreErrorCodes()), class: "shell-commands-ignored-error-codes-icon-container"}});
- setIcon(ignored_error_codes_icon_container, "strikethrough-glyph");
- if (!t_shell_command.getIgnoreErrorCodes().length) {
- // Do not display the icon for commands that do not ignore any errors.
- ignored_error_codes_icon_container.addClass("shell-commands-hide");
- }
-
- // Add hotkey information
- if (!is_new) {
- let hotkeys = getHotkeysForShellCommand(this.plugin, shell_command_id);
- if (hotkeys) {
- let hotkeys_joined: string = "";
- hotkeys.forEach((hotkey: Hotkey) => {
- if (hotkeys_joined) {
- hotkeys_joined += "
"
- }
- hotkeys_joined += HotkeyToString(hotkey);
- });
- let hotkey_div = setting_group.preview_setting.controlEl.createEl("div", { attr: {class: "setting-item-description shell-commands-hotkey-info"}});
- // Comment out the icon because it would look like a clickable button (as there are other clickable icons in the settings).
- // setIcon(hotkey_div, "any-key", 22); // Hotkey icon
- hotkey_div.insertAdjacentHTML("beforeend", " " + hotkeys_joined);
- }
- }
- console.log("Created.");
- }
createNotificationDurationField(container_element: HTMLElement, title: string, description: string, setting_name: "error_message_duration" | "notification_message_duration") {
new Setting(container_element)
@@ -261,17 +125,6 @@ export class ShellCommandsSettingsTab extends PluginSettingTab {
;
}
- getShellCommandPreview(shell_command: string) {
- let parsed_shell_command = parseShellCommandVariables(this.plugin, shell_command); // false: disables notifications if variables have syntax errors.
- if (Array.isArray(parsed_shell_command)) {
- // Variable parsing failed.
- // Return just the first error message, even if there are multiple errors, because the preview space is limited.
- return parsed_shell_command[0];
- }
- // Variable parsing succeeded
- return parsed_shell_command;
- }
-
private scroll_position: number = 0;
private rememberScrollPosition(container_element: HTMLElement) {
container_element.scrollTo({
@@ -282,27 +135,6 @@ export class ShellCommandsSettingsTab extends PluginSettingTab {
this.scroll_position = container_element.scrollTop;
});
}
-
- /**
- * @param shell_command_id String like "0" or "1" etc. TODO: Remove this parameter and use id from t_shell_command.
- * @param t_shell_command
- * @public Public because ShellCommandExtraOptionsModal uses this too.
- */
- public generateCommandFieldName(shell_command_id: string, t_shell_command: TShellCommand) {
- if (t_shell_command.getAlias()) {
- return t_shell_command.getAlias();
- }
- return "Command #" + shell_command_id;
- }
-
- /**
- * @param ignored_error_codes
- * @public Public because ShellCommandExtraOptionsModal uses this too.
- */
- public generateIgnoredErrorCodesIconTitle(ignored_error_codes: number[]) {
- let plural = ignored_error_codes.length !== 1 ? "s" : "";
- return "Ignored error"+plural+": " + ignored_error_codes.join(",");
- }
}
export interface ShellCommandSettingGroup {
diff --git a/src/settings/setting_elements/CreatePlatformSpecificShellCommandField.ts b/src/settings/setting_elements/CreatePlatformSpecificShellCommandField.ts
new file mode 100644
index 00000000..a14ec1c0
--- /dev/null
+++ b/src/settings/setting_elements/CreatePlatformSpecificShellCommandField.ts
@@ -0,0 +1,26 @@
+import {CreateShellCommandFieldCore} from "./CreateShellCommandFieldCore";
+import ShellCommandsPlugin from "../../main";
+import {TShellCommand} from "../../TShellCommand";
+import {PlatformId, PlatformNames} from "../ShellCommandsPluginSettings";
+
+export function createPlatformSpecificShellCommandField(plugin: ShellCommandsPlugin, container_element: HTMLElement, t_shell_command: TShellCommand, platform_id: PlatformId) {
+ const platform_name = PlatformNames[platform_id];
+ const setting_group = CreateShellCommandFieldCore(
+ plugin,
+ container_element,
+ "Shell command on " + platform_name,
+ t_shell_command.getPlatformSpecificShellCommands()[platform_id] ?? "",
+ async (shell_command: string) => {
+ if (shell_command.length) {
+ // shell_command is not empty, so it's a normal command.
+ t_shell_command.getPlatformSpecificShellCommands()[platform_id] = shell_command;
+ } else {
+ // shell_command is empty, so the default command should be used.
+ delete t_shell_command.getPlatformSpecificShellCommands()[platform_id];
+ }
+ await plugin.saveSettings();
+ },
+ t_shell_command.getDefaultShellCommand(),
+ );
+ setting_group.name_setting.setDesc("If empty, the default shell command will be used on " + platform_name + ".");
+}
\ No newline at end of file
diff --git a/src/settings/setting_elements/CreateShellCommandField.ts b/src/settings/setting_elements/CreateShellCommandField.ts
new file mode 100644
index 00000000..7c072308
--- /dev/null
+++ b/src/settings/setting_elements/CreateShellCommandField.ts
@@ -0,0 +1,155 @@
+import {TShellCommand} from "../../TShellCommand";
+import {Hotkey, setIcon} from "obsidian";
+import {parseShellCommandVariables} from "../../variables/parseShellCommandVariables";
+import {ShellCommandExtraOptionsModal} from "../ShellCommandExtraOptionsModal";
+import {ShellCommandDeleteModal} from "../ShellCommandDeleteModal";
+import {getHotkeysForShellCommand, HotkeyToString} from "../../Hotkeys";
+import ShellCommandsPlugin from "../../main";
+import {CreateShellCommandFieldCore} from "./CreateShellCommandFieldCore";
+
+/**
+ *
+ * @param plugin
+ * @param container_element
+ * @param shell_command_id Either a string formatted integer ("0", "1" etc) or "new" if it's a field for a command that does not exist yet.
+ */
+export function createShellCommandField(plugin: ShellCommandsPlugin, container_element: HTMLElement, shell_command_id: string) {
+ let is_new = "new" === shell_command_id;
+ let t_shell_command: TShellCommand;
+ if (is_new) {
+ // Create an empty command
+ t_shell_command = plugin.newTShellCommand();
+ } else {
+ // Use an old shell command
+ t_shell_command = plugin.getTShellCommands()[shell_command_id];
+ }
+ console.log("Create command field for command #" + shell_command_id + (is_new ? " (NEW)" : ""));
+ let shell_command: string;
+ if (is_new) {
+ shell_command = "";
+ } else {
+ shell_command = t_shell_command.getDefaultShellCommand();
+ }
+
+ const setting_group = CreateShellCommandFieldCore(
+ plugin,
+ container_element,
+ generateShellCommandFieldName(shell_command_id, plugin.getTShellCommands()[shell_command_id]),
+ shell_command,
+ async (shell_command: string) => {
+ if (is_new) {
+ console.log("Creating new command " + shell_command_id + ": " + shell_command);
+ } else {
+ console.log("Command " + shell_command_id + " gonna change to: " + shell_command);
+ }
+
+ // Do this in both cases, when creating a new command and when changing an old one:
+ t_shell_command.getConfiguration().platform_specific_commands.default = shell_command;
+
+ if (is_new) {
+ // Create a new command
+ // plugin.registerShellCommand(t_shell_command); // I don't think this is needed to be done anymore
+ console.log("Command created.");
+ } else {
+ // Change an old command
+ plugin.obsidian_commands[shell_command_id].name = plugin.generateObsidianCommandName(plugin.getTShellCommands()[shell_command_id]); // Change the command's name in Obsidian's command palette.
+ console.log("Command changed.");
+ }
+ await plugin.saveSettings();
+ },
+ );
+
+ // Icon buttons
+ setting_group.name_setting
+ .addExtraButton(button => button
+ .setTooltip("Execute now")
+ .setIcon("run-command")
+ .onClick(() => {
+ // Execute the shell command now (for trying it out in the settings)
+ let t_shell_command = plugin.getTShellCommands()[shell_command_id];
+ let parsed_shell_command = parseShellCommandVariables(plugin, t_shell_command.getShellCommand());
+ if (Array.isArray(parsed_shell_command)) {
+ plugin.newErrors(parsed_shell_command);
+ } else {
+ plugin.confirmAndExecuteShellCommand(parsed_shell_command, t_shell_command);
+ }
+ })
+ )
+ .addExtraButton(button => button
+ .setTooltip(ShellCommandExtraOptionsModal.OPTIONS_SUMMARY)
+ .onClick(async () => {
+ // Open an extra options modal
+ let modal = new ShellCommandExtraOptionsModal(plugin.app, plugin, shell_command_id, setting_group, this);
+ modal.open();
+ })
+ )
+ .addExtraButton(button => button
+ .setTooltip("Delete this shell command")
+ .setIcon("trash")
+ .onClick(async () => {
+ // Open a delete modal
+ let modal = new ShellCommandDeleteModal(plugin, shell_command_id, setting_group, container_element);
+ modal.open();
+ })
+ )
+ ;
+
+ // Informational icons (= non-clickable)
+ let icon_container = setting_group.name_setting.nameEl.createEl("span", {attr: {class: "shell-commands-main-icon-container"}});
+
+ // "Ask confirmation" icon.
+ let confirm_execution_icon_container = icon_container.createEl("span", {attr: {"aria-label": "Asks confirmation before execution.", class: "shell-commands-confirm-execution-icon-container"}});
+ setIcon(confirm_execution_icon_container, "languages");
+ if (!t_shell_command.getConfirmExecution()) {
+ // Do not display the icon for commands that do not use confirmation.
+ confirm_execution_icon_container.addClass("shell-commands-hide");
+ }
+
+ // "Ignored error codes" icon
+ let ignored_error_codes_icon_container = icon_container.createEl("span", {attr: {"aria-label": generateIgnoredErrorCodesIconTitle(t_shell_command.getIgnoreErrorCodes()), class: "shell-commands-ignored-error-codes-icon-container"}});
+ setIcon(ignored_error_codes_icon_container, "strikethrough-glyph");
+ if (!t_shell_command.getIgnoreErrorCodes().length) {
+ // Do not display the icon for commands that do not ignore any errors.
+ ignored_error_codes_icon_container.addClass("shell-commands-hide");
+ }
+
+ // Add hotkey information
+ if (!is_new) {
+ let hotkeys = getHotkeysForShellCommand(plugin, shell_command_id);
+ if (hotkeys) {
+ let hotkeys_joined: string = "";
+ hotkeys.forEach((hotkey: Hotkey) => {
+ if (hotkeys_joined) {
+ hotkeys_joined += "
"
+ }
+ hotkeys_joined += HotkeyToString(hotkey);
+ });
+ let hotkey_div = setting_group.preview_setting.controlEl.createEl("div", { attr: {class: "setting-item-description shell-commands-hotkey-info"}});
+ // Comment out the icon because it would look like a clickable button (as there are other clickable icons in the settings).
+ // setIcon(hotkey_div, "any-key", 22); // Hotkey icon
+ hotkey_div.insertAdjacentHTML("beforeend", " " + hotkeys_joined);
+ }
+ }
+ console.log("Created.");
+}
+
+/**
+ * @param shell_command_id String like "0" or "1" etc. TODO: Remove this parameter and use id from t_shell_command.
+ * @param t_shell_command
+ * @public Exported because ShellCommandExtraOptionsModal uses this too.
+ */
+export function generateShellCommandFieldName(shell_command_id: string, t_shell_command: TShellCommand) {
+ if (t_shell_command.getAlias()) {
+ return t_shell_command.getAlias();
+ }
+ return "Command #" + shell_command_id;
+}
+
+/**
+ * @param ignored_error_codes
+ * @public Exported because ShellCommandExtraOptionsModal uses this too.
+ */
+export function generateIgnoredErrorCodesIconTitle(ignored_error_codes: number[]) {
+ let plural = ignored_error_codes.length !== 1 ? "s" : "";
+ return "Ignored error"+plural+": " + ignored_error_codes.join(",");
+}
\ No newline at end of file
diff --git a/src/settings/setting_elements/CreateShellCommandFieldCore.ts b/src/settings/setting_elements/CreateShellCommandFieldCore.ts
new file mode 100644
index 00000000..3b1a2813
--- /dev/null
+++ b/src/settings/setting_elements/CreateShellCommandFieldCore.ts
@@ -0,0 +1,59 @@
+import ShellCommandsPlugin from "../../main";
+import {ShellCommandSettingGroup} from "../ShellCommandsSettingsTab";
+import {Setting} from "obsidian";
+import {parseShellCommandVariables} from "../../variables/parseShellCommandVariables";
+
+export function CreateShellCommandFieldCore(
+ plugin: ShellCommandsPlugin,
+ container_element: HTMLElement,
+ setting_name: string,
+ shell_command: string,
+ on_change: (shell_command: string) => void,
+ shell_command_placeholder: string = "Enter your command"
+ ) {
+ let setting_group: ShellCommandSettingGroup = {
+ name_setting:
+ new Setting(container_element)
+ .setName(setting_name)
+ .setClass("shell-commands-name-setting")
+ ,
+ shell_command_setting:
+ new Setting(container_element)
+ .addText(text => text
+ .setPlaceholder(shell_command_placeholder)
+ .setValue(shell_command)
+ .onChange((shell_command) => {
+ // Update preview
+ setting_group.preview_setting.setDesc(getShellCommandPreview(plugin, shell_command));
+
+ // Let the caller extend this onChange, to preform saving the settings:
+ on_change(shell_command);
+ })
+ )
+ .setClass("shell-commands-shell-command-setting")
+ ,
+ preview_setting:
+ new Setting(container_element)
+ .setDesc(getShellCommandPreview(plugin,shell_command))
+ .setClass("shell-commands-preview-setting")
+ ,
+ };
+ return setting_group;
+}
+
+/**
+ *
+ * @param plugin
+ * @param shell_command
+ * @public Exported because createShellCommandField uses this.
+ */
+export function getShellCommandPreview(plugin: ShellCommandsPlugin, shell_command: string) {
+ let parsed_shell_command = parseShellCommandVariables(plugin, shell_command); // false: disables notifications if variables have syntax errors.
+ if (Array.isArray(parsed_shell_command)) {
+ // Variable parsing failed.
+ // Return just the first error message, even if there are multiple errors, because the preview space is limited.
+ return parsed_shell_command[0];
+ }
+ // Variable parsing succeeded
+ return parsed_shell_command;
+}
\ No newline at end of file
diff --git a/src/settings/setting_elements/CreateShellSelectionField.ts b/src/settings/setting_elements/CreateShellSelectionField.ts
new file mode 100644
index 00000000..63857685
--- /dev/null
+++ b/src/settings/setting_elements/CreateShellSelectionField.ts
@@ -0,0 +1,43 @@
+import {IPlatformSpecificString, PlatformId, PlatformNames, PlatformShells} from "../ShellCommandsPluginSettings";
+import {extractFileName, getOperatingSystem} from "../../Common";
+import {getUsersDefaultShell} from "../../Shell";
+import {Setting} from "obsidian";
+import ShellCommandsPlugin from "../../main";
+
+export function createShellSelectionField(plugin: ShellCommandsPlugin, container_element: HTMLElement, shells: IPlatformSpecificString, is_global_settings: boolean) {
+ let platform_id: PlatformId;
+ for (platform_id in PlatformNames) {
+ let platform_name = PlatformNames[platform_id];
+ let options: {};
+ if (is_global_settings) {
+ let current_system_default = (getOperatingSystem() === platform_id) ? " (" + extractFileName(getUsersDefaultShell()) + ")" : "";
+ options = {"default": "Use system default" + current_system_default};
+ } else {
+ options = {"default": "Use default"};
+ }
+ for (let shell_path in PlatformShells[platform_id]) {
+ // @ts-ignore // TODO: Get rid of these two ts-ignores.
+ let shell_name = PlatformShells[platform_id][shell_path];
+ // @ts-ignore
+ options[shell_path] = shell_name;
+ }
+ new Setting(container_element)
+ .setName(platform_name + (is_global_settings ? " default shell" : " shell"))
+ .setDesc((is_global_settings ? "Can be overridden by each shell command. " : "") + ("win32" === platform_id ? "Powershell is recommended over cmd.exe, because this plugin does not support escaping variables in CMD." : ""))
+ .addDropdown(dropdown => dropdown
+ .addOptions(options)
+ .setValue(shells[platform_id] ?? "default")
+ .onChange(((_platform_id: PlatformId) => { return async (value: string) => { // Need to use a nested function so that platform_id can be stored statically, otherwise it would always be "win32" (the last value of PlatformNames).
+ if ("default" === value) {
+ // When using default shell, the value should be unset.
+ delete shells[_platform_id];
+ } else {
+ // Normal case: assign the shell value.
+ shells[_platform_id] = value;
+ }
+ await plugin.saveSettings();
+ }})(platform_id))
+ )
+ ;
+ }
+}
\ No newline at end of file
diff --git a/styles.css b/styles.css
index 360a86fa..ed40988a 100644
--- a/styles.css
+++ b/styles.css
@@ -6,6 +6,10 @@
display: none;
}
+.SC-scrollable {
+ overflow-y: auto;
+}
+
/*
* SHELL COMMAND SETTING CONTAINERS