More fixes for password managers

This commit is contained in:
crschnick
2023-07-28 02:41:28 +00:00
parent a0588091d8
commit dc32adaf7c
11 changed files with 221 additions and 73 deletions
@@ -11,8 +11,10 @@ import javafx.css.Size;
import javafx.css.SizeUnits;
import javafx.scene.Node;
import javafx.scene.control.Button;
import lombok.Getter;
import org.kordamp.ikonli.javafx.FontIcon;
@Getter
public class ButtonComp extends Comp<CompStructure<Button>> {
private final ObservableValue<String> name;
@@ -31,10 +33,6 @@ public class ButtonComp extends Comp<CompStructure<Button>> {
this.listener = listener;
}
public ObservableValue<String> getName() {
return name;
}
public Node getGraphic() {
return graphic.get();
}
@@ -43,10 +41,6 @@ public class ButtonComp extends Comp<CompStructure<Button>> {
return graphic;
}
public Runnable getListener() {
return listener;
}
@Override
public CompStructure<Button> createBase() {
var button = new Button(null);
@@ -39,6 +39,10 @@ public class AppLayoutModel {
selected.setValue(entries.get(0));
}
public void selectSettings() {
selected.setValue(entries.get(2));
}
public void selectConnections() {
selected.setValue(entries.get(1));
}
@@ -13,10 +13,12 @@ import javafx.beans.property.ObjectProperty;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.control.ScrollPane;
import lombok.Getter;
import lombok.SneakyThrows;
import java.util.List;
@Getter
public class AppPreferencesFx {
private final PreferencesFxModel preferencesFxModel;
@@ -1,5 +1,6 @@
package io.xpipe.app.prefs;
import atlantafx.base.theme.Styles;
import com.dlsc.formsfx.model.structure.*;
import com.dlsc.preferencesfx.formsfx.view.controls.SimpleComboBoxControl;
import com.dlsc.preferencesfx.formsfx.view.controls.SimpleControl;
@@ -10,15 +11,23 @@ import com.dlsc.preferencesfx.model.Setting;
import com.dlsc.preferencesfx.util.VisibilityProperty;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.AppTheme;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.ext.PrefsHandler;
import io.xpipe.app.ext.PrefsProvider;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ApplicationHelper;
import io.xpipe.app.util.LockChangeAlert;
import io.xpipe.app.util.LockedSecretValue;
import io.xpipe.app.util.TerminalHelper;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.process.CommandControl;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.SecretValue;
import javafx.beans.binding.Bindings;
@@ -30,6 +39,7 @@ import javafx.geometry.Pos;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import lombok.SneakyThrows;
import org.kordamp.ikonli.javafx.FontIcon;
import java.nio.file.Path;
import java.util.*;
@@ -156,6 +166,10 @@ public class AppPrefs {
private final BooleanField preferTerminalTabsField =
BooleanField.ofBooleanType(preferTerminalTabs).render(() -> new CustomToggleControl());
// Password manager
// ================
private final StringProperty passwordManagerCommand = typed(new SimpleStringProperty(""), String.class);
// Start behaviour
// ===============
private final SimpleListProperty<StartupBehaviour> startupBehaviourList = new SimpleListProperty<>(
@@ -163,9 +177,9 @@ public class AppPrefs {
private final ObjectProperty<StartupBehaviour> startupBehaviour =
typed(new SimpleObjectProperty<>(StartupBehaviour.GUI), StartupBehaviour.class);
private final SingleSelectionField<StartupBehaviour> startupBehaviourControl =
Field.ofSingleSelectionType(startupBehaviourList, startupBehaviour)
.render(() -> new TranslatableComboBoxControl<>());
private final SingleSelectionField<StartupBehaviour> startupBehaviourControl = Field.ofSingleSelectionType(
startupBehaviourList, startupBehaviour)
.render(() -> new TranslatableComboBoxControl<>());
// Close behaviour
// ===============
@@ -209,10 +223,8 @@ public class AppPrefs {
// =======
private final ObjectProperty<Path> storageDirectory =
typed(new SimpleObjectProperty<>(DEFAULT_STORAGE_DIR), Path.class);
private final StringField storageDirectoryControl = PrefFields.ofPath(storageDirectory)
.validate(
CustomValidators.absolutePath(),
CustomValidators.directory());
private final StringField storageDirectoryControl =
PrefFields.ofPath(storageDirectory).validate(CustomValidators.absolutePath(), CustomValidators.directory());
// Log level
// =========
@@ -240,30 +252,35 @@ public class AppPrefs {
typed(new SimpleBooleanProperty(false), Boolean.class);
private final BooleanField developerDisableUpdateVersionCheckField =
BooleanField.ofBooleanType(developerDisableUpdateVersionCheck).render(() -> new CustomToggleControl());
private final ObservableBooleanValue developerDisableUpdateVersionCheckEffective = bindDeveloperTrue(developerDisableUpdateVersionCheck);
private final ObservableBooleanValue developerDisableUpdateVersionCheckEffective =
bindDeveloperTrue(developerDisableUpdateVersionCheck);
private final BooleanProperty developerDisableGuiRestrictions =
typed(new SimpleBooleanProperty(false), Boolean.class);
private final BooleanField developerDisableGuiRestrictionsField =
BooleanField.ofBooleanType(developerDisableGuiRestrictions).render(() -> new CustomToggleControl());
private final ObservableBooleanValue developerDisableGuiRestrictionsEffective = bindDeveloperTrue(developerDisableGuiRestrictions);
private final ObservableBooleanValue developerDisableGuiRestrictionsEffective =
bindDeveloperTrue(developerDisableGuiRestrictions);
private final BooleanProperty developerShowHiddenProviders = typed(new SimpleBooleanProperty(false), Boolean.class);
private final BooleanField developerShowHiddenProvidersField =
BooleanField.ofBooleanType(developerShowHiddenProviders).render(() -> new CustomToggleControl());
private final ObservableBooleanValue developerShowHiddenProvidersEffective = bindDeveloperTrue(developerShowHiddenProviders);
private final ObservableBooleanValue developerShowHiddenProvidersEffective =
bindDeveloperTrue(developerShowHiddenProviders);
private final BooleanProperty developerShowHiddenEntries = typed(new SimpleBooleanProperty(false), Boolean.class);
private final BooleanField developerShowHiddenEntriesField =
BooleanField.ofBooleanType(developerShowHiddenEntries).render(() -> new CustomToggleControl());
private final ObservableBooleanValue developerShowHiddenEntriesEffective = bindDeveloperTrue(developerShowHiddenEntries);
private final ObservableBooleanValue developerShowHiddenEntriesEffective =
bindDeveloperTrue(developerShowHiddenEntries);
private final BooleanProperty developerDisableConnectorInstallationVersionCheck =
typed(new SimpleBooleanProperty(false), Boolean.class);
private final BooleanField developerDisableConnectorInstallationVersionCheckField = BooleanField.ofBooleanType(
developerDisableConnectorInstallationVersionCheck)
.render(() -> new CustomToggleControl());
private final ObservableBooleanValue developerDisableConnectorInstallationVersionCheckEffective = bindDeveloperTrue(developerDisableConnectorInstallationVersionCheck);
private final ObservableBooleanValue developerDisableConnectorInstallationVersionCheckEffective =
bindDeveloperTrue(developerDisableConnectorInstallationVersionCheck);
public ReadOnlyProperty<CloseBehaviour> closeBehaviour() {
return closeBehaviour;
@@ -510,12 +527,70 @@ public class AppPrefs {
return null;
}
public void selectCategory(int index) {
AppLayoutModel.get().selectSettings();
preferencesFx
.getNavigationPresenter()
.setSelectedCategory(preferencesFx.getCategories().get(index));
}
public String passwordManagersString(String key) {
if (passwordManagerCommand.get() == null
|| passwordManagerCommand
.get()
.isEmpty()
|| key == null
|| key
.isEmpty()) {
return null;
}
return ApplicationHelper.replaceFileArgument(
passwordManagerCommand.get(),
"KEY",
key);
}
@SneakyThrows
private AppPreferencesFx createPreferences() {
var ctr = Setting.class.getDeclaredConstructor(String.class, Element.class, Property.class);
ctr.setAccessible(true);
var testPasswordManagerValue = new SimpleStringProperty();
var testPasswordManager = ctr.newInstance(
"passwordManagerCommandTest",
new LazyNodeElement<>(() -> new HorizontalComp(List.of(
new TextFieldComp(testPasswordManagerValue)
.apply(struc -> struc.get().setPromptText("Test password key"))
.styleClass(Styles.LEFT_PILL),
new ButtonComp(null, new FontIcon("mdi2p-play"), () -> {
var cmd = passwordManagersString(testPasswordManagerValue.get());
if (cmd == null) {
return;
}
try {
TerminalHelper.open(
"Password test",
new LocalStore()
.control()
.command(cmd)
.terminalExitMode(
CommandControl.TerminalExitMode
.KEEP_OPEN)
+ ShellDialects.getPlatformDefault()
.getEchoCommand(
"Is this your password?", false));
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
})
.styleClass(Styles.RIGHT_PILL)))
.createRegion()),
null);
var about = ctr.newInstance(null, new LazyNodeElement<>(() -> new AboutComp().createRegion()), null);
var troubleshoot = ctr.newInstance(null, new LazyNodeElement<>(() -> new TroubleshootComp().createRegion()), null);
var troubleshoot =
ctr.newInstance(null, new LazyNodeElement<>(() -> new TroubleshootComp().createRegion()), null);
var categories = new ArrayList<>(List.of(
Category.of("about", Group.of(about)),
@@ -523,11 +598,7 @@ public class AppPrefs {
"system",
Group.of(
"appBehaviour",
Setting.of(
"startupBehaviour",
startupBehaviourControl,
startupBehaviour
),
Setting.of("startupBehaviour", startupBehaviourControl, startupBehaviour),
Setting.of("closeBehaviour", closeBehaviourControl, closeBehaviour)),
Group.of("security", Setting.of("workspaceLock", lockCryptControl, lockCrypt)),
Group.of(
@@ -539,7 +610,9 @@ public class AppPrefs {
Setting.of("updateToPrereleases", checkForPrereleasesField, checkForPrereleases)),
group(
"advanced",
STORAGE_DIR_FIXED ? null : Setting.of("storageDirectory", storageDirectoryControl, storageDirectory),
STORAGE_DIR_FIXED
? null
: Setting.of("storageDirectory", storageDirectoryControl, storageDirectory),
Setting.of("logLevel", logLevelField, internalLogLevel),
Setting.of("developerMode", developerModeField, internalDeveloperMode))),
Category.of(
@@ -551,6 +624,9 @@ public class AppPrefs {
Setting.of("tooltipDelay", tooltipDelayInternal, tooltipDelayMin, tooltipDelayMax),
Setting.of("language", languageControl, languageInternal)),
Group.of("windowOptions", Setting.of("saveWindowLocation", saveWindowLocationInternal))),
Category.of(
"passwordManager",
Group.of(Setting.of("passwordManagerCommand", passwordManagerCommand), testPasswordManager)),
Category.of(
"editor",
Group.of(
@@ -564,13 +640,14 @@ public class AppPrefs {
editorReloadTimeoutMin,
editorReloadTimeoutMax),
Setting.of("preferEditorTabs", preferEditorTabsField, preferEditorTabs))),
Category.of("terminal",
Group.of(
Setting.of("terminalProgram", terminalTypeControl, terminalType),
Setting.of("customTerminalCommand", customTerminalCommandControl, customTerminalCommand)
.applyVisibility(VisibilityProperty.of(
terminalType.isEqualTo(ExternalTerminalType.CUSTOM))),
Setting.of("preferTerminalTabs", preferTerminalTabsField, preferTerminalTabs))),
Category.of(
"terminal",
Group.of(
Setting.of("terminalProgram", terminalTypeControl, terminalType),
Setting.of("customTerminalCommand", customTerminalCommandControl, customTerminalCommand)
.applyVisibility(VisibilityProperty.of(
terminalType.isEqualTo(ExternalTerminalType.CUSTOM))),
Setting.of("preferTerminalTabs", preferTerminalTabsField, preferTerminalTabs))),
Category.of(
"developer",
Setting.of(
@@ -604,8 +681,9 @@ public class AppPrefs {
return AppPreferencesFx.of(cats);
}
private Group group(String name, Setting<?,?>... settings) {
return Group.of(name, Arrays.stream(settings).filter(setting -> setting != null).toArray(Setting[]::new));
private Group group(String name, Setting<?, ?>... settings) {
return Group.of(
name, Arrays.stream(settings).filter(setting -> setting != null).toArray(Setting[]::new));
}
private class PrefsHandlerImpl implements PrefsHandler {
@@ -12,6 +12,7 @@ import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Supplier;
@@ -137,8 +138,8 @@ public interface ExternalEditorType extends PrefsChoiceValue {
throw new IllegalStateException("No custom editor command specified");
}
var format = customCommand.contains("$file") ? customCommand : customCommand + " $file";
ApplicationHelper.executeLocalApplication(sc -> ApplicationHelper.replaceFileArgument(format, "file", file.toString()), true);
var format = customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE";
ApplicationHelper.executeLocalApplication(sc -> ApplicationHelper.replaceFileArgument(format, "FILE", file.toString()), true);
}
@Override
@@ -447,9 +447,9 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
throw new IllegalStateException("No custom terminal command specified");
}
var format = custom.toLowerCase(Locale.ROOT).contains("$cmd") ? custom : custom + " $cmd";
var format = custom.toLowerCase(Locale.ROOT).contains("$cmd") ? custom : custom + " $CMD";
try (var pc = LocalStore.getShell()) {
var toExecute = ApplicationHelper.replaceFileArgument(format, "cmd", file);
var toExecute = ApplicationHelper.replaceFileArgument(format, "CMD", file);
if (pc.getOsType().equals(OsType.WINDOWS)) {
toExecute = "start \"" + name + "\" " + toExecute;
} else {
@@ -12,7 +12,9 @@ public class ApplicationHelper {
public static String replaceFileArgument(String format, String variable, String file) {
// Support for legacy variables that were not upper case
variable = variable.toUpperCase(Locale.ROOT);
format = format.replace("$" + variable.toLowerCase(Locale.ROOT), "$" + variable.toUpperCase(Locale.ROOT));
var fileString = file.contains(" ") ? "\"" + file + "\"" : file;
// Check if the variable is already quoted
var replaced = format.replace("\"$" + variable + "\"", fileString).replace("$" + variable, fileString);
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.SecretValue;
@@ -21,8 +22,8 @@ import java.util.function.Supplier;
@JsonSubTypes.Type(value = SecretRetrievalStrategy.Reference.class),
@JsonSubTypes.Type(value = SecretRetrievalStrategy.InPlace.class),
@JsonSubTypes.Type(value = SecretRetrievalStrategy.Prompt.class),
@JsonSubTypes.Type(value = SecretRetrievalStrategy.Command.class),
@JsonSubTypes.Type(value = SecretRetrievalStrategy.KeePass.class)
@JsonSubTypes.Type(value = SecretRetrievalStrategy.CustomCommand.class),
@JsonSubTypes.Type(value = SecretRetrievalStrategy.PasswordManager.class)
})
public interface SecretRetrievalStrategy {
@@ -90,11 +91,33 @@ public interface SecretRetrievalStrategy {
}
}
@JsonTypeName("command")
@JsonTypeName("passwordManager")
@Builder
@Jacksonized
@Value
public static class Command implements SecretRetrievalStrategy {
public static class PasswordManager implements SecretRetrievalStrategy {
String key;
@Override
public SecretValue retrieve(String displayName, DataStore store) throws Exception {
var cmd = AppPrefs.get().passwordManagersString(key);
if (cmd == null) {
return null;
}
try (var cc = new LocalStore().createBasicControl().command(cmd).start()) {
var read = cc.readStdoutDiscardErr();
return SecretHelper.encrypt(read);
}
}
}
@JsonTypeName("customCommand")
@Builder
@Jacksonized
@Value
public static class CustomCommand implements SecretRetrievalStrategy {
String command;
@@ -106,21 +129,4 @@ public interface SecretRetrievalStrategy {
}
}
}
@JsonTypeName("keepass")
@Builder
@Jacksonized
@Value
public static class KeePass implements SecretRetrievalStrategy {
String entry;
@Override
public SecretValue retrieve(String displayName, DataStore store) throws Exception {
try (var cc = new LocalStore().createBasicControl().command(entry).start()) {
var read = cc.readStdoutDiscardErr();
return SecretHelper.encrypt(read);
}
}
}
}
@@ -1,17 +1,25 @@
package io.xpipe.app.util;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.App;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.SecretFieldComp;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.prefs.AppPrefs;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.LinkedHashMap;
import java.util.List;
public class SecretRetrievalStrategyHelper {
private static OptionsBuilder inPlace(Property<SecretRetrievalStrategy.InPlace> p) {
var secretProperty = new SimpleObjectProperty<>(
p.getValue() != null ? p.getValue().getValue() : null);
var secretProperty =
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getValue() : null);
return new OptionsBuilder()
.name("password")
.addComp(new SecretFieldComp(secretProperty), secretProperty)
@@ -22,16 +30,63 @@ public class SecretRetrievalStrategyHelper {
p);
}
private static OptionsBuilder passwordManager(Property<SecretRetrievalStrategy.PasswordManager> p) {
var keyProperty =
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getKey() : null);
var content = new HorizontalComp(List.<Comp<?>>of(
new TextFieldComp(keyProperty).hgrow(),
new ButtonComp(null, new FontIcon("mdomz-settings"), () -> {
AppPrefs.get().selectCategory(3);
App.getApp().getStage().requestFocus();
})
.grow(false, true)))
.apply(struc -> struc.get().setSpacing(10));
return new OptionsBuilder()
.name("command")
.addComp(content, keyProperty)
.bind(
() -> {
return new SecretRetrievalStrategy.PasswordManager(keyProperty.getValue());
},
p);
}
private static OptionsBuilder customCommand(Property<SecretRetrievalStrategy.CustomCommand> p) {
var cmdProperty =
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getCommand() : null);
var content = new TextFieldComp(cmdProperty).apply(struc -> struc.get().setPromptText("Password key"));
return new OptionsBuilder()
.name("key")
.addComp(content, cmdProperty)
.bind(
() -> {
return new SecretRetrievalStrategy.CustomCommand(cmdProperty.getValue());
},
p);
}
public static OptionsBuilder comp(Property<SecretRetrievalStrategy> s) {
var inPlace = new SimpleObjectProperty<>(s.getValue() instanceof SecretRetrievalStrategy.InPlace i ? i : null);
var command = new SimpleObjectProperty<>(s.getValue() instanceof SecretRetrievalStrategy.Command c ? c : null);
SecretRetrievalStrategy strat = s.getValue();
var inPlace = new SimpleObjectProperty<>(strat instanceof SecretRetrievalStrategy.InPlace i ? i : null);
var passwordManager =
new SimpleObjectProperty<>(strat instanceof SecretRetrievalStrategy.PasswordManager i ? i : null);
var customCommand =
new SimpleObjectProperty<>(strat instanceof SecretRetrievalStrategy.CustomCommand i ? i : null);
var command = new SimpleObjectProperty<>(strat instanceof SecretRetrievalStrategy.CustomCommand c ? c : null);
var map = new LinkedHashMap<String, OptionsBuilder>();
map.put("none", new OptionsBuilder());
map.put("password", inPlace(inPlace));
map.put("passwordManager", passwordManager(passwordManager));
map.put("customCommand", customCommand(customCommand));
map.put("prompt", new OptionsBuilder());
// map.put("command", new OptionsBuilder());
map.put("keepass", new OptionsBuilder());
var selected = new SimpleIntegerProperty();
var selected = new SimpleIntegerProperty(
strat instanceof SecretRetrievalStrategy.None
? 0
: strat instanceof SecretRetrievalStrategy.InPlace
? 1
: strat instanceof SecretRetrievalStrategy.PasswordManager
? 2
: strat instanceof SecretRetrievalStrategy.CustomCommand ? 3 : strat instanceof SecretRetrievalStrategy.Prompt ? 4 : 0);
return new OptionsBuilder()
.choice(selected, map)
.bindChoice(
@@ -39,9 +94,9 @@ public class SecretRetrievalStrategyHelper {
return switch (selected.get()) {
case 0 -> new SimpleObjectProperty<>(new SecretRetrievalStrategy.None());
case 1 -> inPlace;
case 2 -> new SimpleObjectProperty<>(new SecretRetrievalStrategy.Prompt());
// case 3 -> command;
case 3 -> new SimpleObjectProperty<>(new SecretRetrievalStrategy.KeePass("a"));
case 2 -> passwordManager;
case 3 -> customCommand;
case 4 -> new SimpleObjectProperty<>(new SecretRetrievalStrategy.Prompt());
default -> new SimpleObjectProperty<>();
};
},
@@ -64,10 +64,12 @@ developerMode=Developer mode
developerModeDescription=When enabled, you will have access to a variety of additional options that are useful for development.
editor=Editor
custom=Custom
passwordManagerCommand=Password manager command
passwordManagerCommandDescription=The command to execute to fetch passwords. The placeholder string $KEY will be replaced by the quoted password key when called. This should call your password manager CLI to print the password to stdout, e.g. mypassmgr get $KEY.\n\nYou can check here whether the output is correct. The command should only output the password itself, no other formatting should be included in the output.
preferEditorTabs=Prefer to open new tabs
preferEditorTabsDescription=Controls whether XPipe will try to open new tabs in your chosen editor instead of new windows.
customEditorCommand=Custom editor command
customEditorCommandDescription=The command to execute to open the custom editor. The placeholder string $file will be replaced by the quoted absolute file name when called. Remember to quote your editor executable path if it contains spaces.
customEditorCommandDescription=The command to execute to open the custom editor. The placeholder string $FILE will be replaced by the quoted absolute file name when called. Remember to quote your editor executable path if it contains spaces.
editorReloadTimeout=Editor reload timeout
editorReloadTimeoutDescription=The amount of milliseconds to wait before reading a file after it has been updated. This avoids issues in cases where your editor is slow at writing or releasing file locks.
notepad++=Notepad++
@@ -102,7 +104,7 @@ terminalProgram=Default program
terminalProgramDescription=The default terminal to use when opening any kind of shell connection. This application is only used for display purposes, the started shell program depends on the shell connection itself.
program=Program
customTerminalCommand=Custom terminal command
customTerminalCommandDescription=The command to execute to open the custom terminal. The placeholder string $cmd will be replaced by the quoted shell script file name when called. Remember to quote your terminal executable path if it contains spaces.
customTerminalCommandDescription=The command to execute to open the custom terminal. The placeholder string $CMD will be replaced by the quoted shell script file name when called. Remember to quote your terminal executable path if it contains spaces.
preferTerminalTabs=Prefer to open new tabs
preferTerminalTabsDescription=Controls whether XPipe will try to open new tabs in your chosen terminal instead of new windows.
cmd=cmd.exe
@@ -4,6 +4,10 @@ crlf=CRLF (Windows)
lf=LF (Linux)
none=None
common=Common
key=Key
passwordManager=Password manager
prompt=Prompt
customCommand=Custom command
other=Other
setLock=Set lock
changeLock=Change lock