feat: cmux.json for custom commands (#2011)
* Pre-launch app for browser UI test on headless CI runners
XCUIApplication.launch() blocks ~60s then fails on headless WarpBuild
runners because foreground activation requires a GUI login session.
Apply the same pre-launch strategy used for the display resolution test:
- CI shell launches the app with env vars before running xcodebuild
- Test detects pre-launched app via manifest, uses activate() instead of
launch() to avoid killing and relaunching the app
- Falls back to clicking the window for focus via accessibility framework
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Revert "Pre-launch app for browser UI test on headless CI runners"
This reverts commit a540e2fd99aaa1395b91a8d50caa797cdd7551b8.
* feat: cmux.json for custom commands
* tests: add cmux json tests
* fix: pr review feedback: validation, translations, input handling, and palette improvements
- Fix Danish ("Overfladedef inition") and Norwegian ("rotmapp") translation typos
- Add empty-string check for baseCwd fallback in command palette handlers
- Coalesce \r\n into single Return keypress in sendInput
- Redact command text from timeout log to prevent secret leakage
- Add decode-time validation: reject hybrid/empty commands, ambiguous layout
nodes, wrong split children count, and empty pane surfaces
- Namespace custom command IDs with "cmux.config.command." prefix
- Forward command description to palette subtitle when available
- Update tests for new validation rules and ID prefix
* fix: address PR review feedback — per-window config isolation, blank validation, ancestor walk,
palette sanitization
* fix: fallback to current dir cmux.json watching if no any cmux.json found in full acesor walk
* ci: trigger CI for fork PR
* Add directory trust for cmux.json command confirmation
The confirm dialog now shows the actual command text and has an "Always
trust commands from this folder" checkbox. When checked, future confirm
commands from that directory skip the dialog.
Trust is scoped to the git repo root if the cmux.json is inside a repo,
so trusting once covers all subdirectories. Non-git directories are
trusted by exact path. Global config is always trusted.
Trusted directories are persisted in ~/Library/Application Support/cmux/
trusted-directories.json.
* Add trusted directories section to Settings
Shows all trusted directories with per-directory revoke buttons and a
Clear All option. Placed in a "Custom Commands" section between
Automation and Browser in Settings.
* Replace trusted directories list with editable textarea
One path per line, with a Save button that activates on changes.
Users can add, remove, or edit paths directly.
* Auto-save trusted directories on edit, remove Save button
Matches the behavior of other textarea settings (browser host
whitelist, external URL patterns) which auto-save via @AppStorage.
* Sanitize command text in confirm dialog against BiDi attacks
Strip zero-width and BiDi override characters from the command preview
so the dialog shows exactly what will be executed.
---------
Co-authored-by: austinpower1258 <austinwang115@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
This commit is contained in:
parent
09e31448c9
commit
b9c656b90c
33 changed files with 4588 additions and 1 deletions
|
|
@ -68,6 +68,9 @@
|
||||||
A5001240 /* WindowDecorationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001241 /* WindowDecorationsController.swift */; };
|
A5001240 /* WindowDecorationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001241 /* WindowDecorationsController.swift */; };
|
||||||
A5001610 /* SessionPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001611 /* SessionPersistence.swift */; };
|
A5001610 /* SessionPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001611 /* SessionPersistence.swift */; };
|
||||||
A5001640 /* RemoteRelayZshBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001641 /* RemoteRelayZshBootstrap.swift */; };
|
A5001640 /* RemoteRelayZshBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001641 /* RemoteRelayZshBootstrap.swift */; };
|
||||||
|
A5001650 /* CmuxConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001651 /* CmuxConfig.swift */; };
|
||||||
|
A5001652 /* CmuxConfigExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001653 /* CmuxConfigExecutor.swift */; };
|
||||||
|
A5001654 /* CmuxDirectoryTrust.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001655 /* CmuxDirectoryTrust.swift */; };
|
||||||
A5001100 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5001101 /* Assets.xcassets */; };
|
A5001100 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5001101 /* Assets.xcassets */; };
|
||||||
A5001230 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A5001231 /* Sparkle */; };
|
A5001230 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A5001231 /* Sparkle */; };
|
||||||
B9000002A1B2C3D4E5F60719 /* cmux.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000001A1B2C3D4E5F60719 /* cmux.swift */; };
|
B9000002A1B2C3D4E5F60719 /* cmux.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000001A1B2C3D4E5F60719 /* cmux.swift */; };
|
||||||
|
|
@ -123,6 +126,7 @@
|
||||||
CA39C0304FE351A21C372429 /* SidebarWidthPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */; };
|
CA39C0304FE351A21C372429 /* SidebarWidthPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */; };
|
||||||
8C4BBF2DEF6DF93F395A9EE7 /* TerminalControllerSocketSecurityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */; };
|
8C4BBF2DEF6DF93F395A9EE7 /* TerminalControllerSocketSecurityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */; };
|
||||||
2BB56A710BB1FC50367E5BCF /* TabManagerSessionSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */; };
|
2BB56A710BB1FC50367E5BCF /* TabManagerSessionSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */; };
|
||||||
|
C1A2B3C4D5E6F70800000001 /* CmuxConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A2B3C4D5E6F70800000002 /* CmuxConfigTests.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
|
@ -237,6 +241,9 @@
|
||||||
A5001223 /* UpdateLogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateLogStore.swift; sourceTree = "<group>"; };
|
A5001223 /* UpdateLogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateLogStore.swift; sourceTree = "<group>"; };
|
||||||
A5001241 /* WindowDecorationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDecorationsController.swift; sourceTree = "<group>"; };
|
A5001241 /* WindowDecorationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDecorationsController.swift; sourceTree = "<group>"; };
|
||||||
A5001611 /* SessionPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPersistence.swift; sourceTree = "<group>"; };
|
A5001611 /* SessionPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPersistence.swift; sourceTree = "<group>"; };
|
||||||
|
A5001651 /* CmuxConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxConfig.swift; sourceTree = "<group>"; };
|
||||||
|
A5001653 /* CmuxConfigExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxConfigExecutor.swift; sourceTree = "<group>"; };
|
||||||
|
A5001655 /* CmuxDirectoryTrust.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxDirectoryTrust.swift; sourceTree = "<group>"; };
|
||||||
A5001641 /* RemoteRelayZshBootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteRelayZshBootstrap.swift; sourceTree = "<group>"; };
|
A5001641 /* RemoteRelayZshBootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteRelayZshBootstrap.swift; sourceTree = "<group>"; };
|
||||||
818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarResizeUITests.swift; sourceTree = "<group>"; };
|
818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarResizeUITests.swift; sourceTree = "<group>"; };
|
||||||
B8F266256A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarHelpMenuUITests.swift; sourceTree = "<group>"; };
|
B8F266256A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarHelpMenuUITests.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -292,6 +299,7 @@
|
||||||
EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWidthPolicyTests.swift; sourceTree = "<group>"; };
|
EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWidthPolicyTests.swift; sourceTree = "<group>"; };
|
||||||
491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalControllerSocketSecurityTests.swift; sourceTree = "<group>"; };
|
491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalControllerSocketSecurityTests.swift; sourceTree = "<group>"; };
|
||||||
10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerSessionSnapshotTests.swift; sourceTree = "<group>"; };
|
10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerSessionSnapshotTests.swift; sourceTree = "<group>"; };
|
||||||
|
C1A2B3C4D5E6F70800000002 /* CmuxConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxConfigTests.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
|
@ -458,6 +466,9 @@
|
||||||
A5001222 /* WindowAccessor.swift */,
|
A5001222 /* WindowAccessor.swift */,
|
||||||
A5001611 /* SessionPersistence.swift */,
|
A5001611 /* SessionPersistence.swift */,
|
||||||
A5001641 /* RemoteRelayZshBootstrap.swift */,
|
A5001641 /* RemoteRelayZshBootstrap.swift */,
|
||||||
|
A5001651 /* CmuxConfig.swift */,
|
||||||
|
A5001653 /* CmuxConfigExecutor.swift */,
|
||||||
|
A5001655 /* CmuxDirectoryTrust.swift */,
|
||||||
);
|
);
|
||||||
path = Sources;
|
path = Sources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -547,6 +558,7 @@
|
||||||
EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */,
|
EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */,
|
||||||
491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */,
|
491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */,
|
||||||
10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */,
|
10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */,
|
||||||
|
C1A2B3C4D5E6F70800000002 /* CmuxConfigTests.swift */,
|
||||||
);
|
);
|
||||||
path = cmuxTests;
|
path = cmuxTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -753,6 +765,9 @@
|
||||||
A500120C /* WindowAccessor.swift in Sources */,
|
A500120C /* WindowAccessor.swift in Sources */,
|
||||||
A5001610 /* SessionPersistence.swift in Sources */,
|
A5001610 /* SessionPersistence.swift in Sources */,
|
||||||
A5001640 /* RemoteRelayZshBootstrap.swift in Sources */,
|
A5001640 /* RemoteRelayZshBootstrap.swift in Sources */,
|
||||||
|
A5001650 /* CmuxConfig.swift in Sources */,
|
||||||
|
A5001652 /* CmuxConfigExecutor.swift in Sources */,
|
||||||
|
A5001654 /* CmuxDirectoryTrust.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -810,6 +825,7 @@
|
||||||
CA39C0304FE351A21C372429 /* SidebarWidthPolicyTests.swift in Sources */,
|
CA39C0304FE351A21C372429 /* SidebarWidthPolicyTests.swift in Sources */,
|
||||||
8C4BBF2DEF6DF93F395A9EE7 /* TerminalControllerSocketSecurityTests.swift in Sources */,
|
8C4BBF2DEF6DF93F395A9EE7 /* TerminalControllerSocketSecurityTests.swift in Sources */,
|
||||||
2BB56A710BB1FC50367E5BCF /* TabManagerSessionSnapshotTests.swift in Sources */,
|
2BB56A710BB1FC50367E5BCF /* TabManagerSessionSnapshotTests.swift in Sources */,
|
||||||
|
C1A2B3C4D5E6F70800000001 /* CmuxConfigTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -5789,11 +5789,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
||||||
)
|
)
|
||||||
let notificationStore = TerminalNotificationStore.shared
|
let notificationStore = TerminalNotificationStore.shared
|
||||||
|
|
||||||
|
let cmuxConfigStore = CmuxConfigStore()
|
||||||
|
cmuxConfigStore.wireDirectoryTracking(tabManager: tabManager)
|
||||||
|
cmuxConfigStore.loadAll()
|
||||||
|
|
||||||
let root = ContentView(updateViewModel: updateViewModel, windowId: windowId)
|
let root = ContentView(updateViewModel: updateViewModel, windowId: windowId)
|
||||||
.environmentObject(tabManager)
|
.environmentObject(tabManager)
|
||||||
.environmentObject(notificationStore)
|
.environmentObject(notificationStore)
|
||||||
.environmentObject(sidebarState)
|
.environmentObject(sidebarState)
|
||||||
.environmentObject(sidebarSelectionState)
|
.environmentObject(sidebarSelectionState)
|
||||||
|
.environmentObject(cmuxConfigStore)
|
||||||
|
|
||||||
let window = NSWindow(
|
let window = NSWindow(
|
||||||
contentRect: NSRect(x: 0, y: 0, width: 460, height: 360),
|
contentRect: NSRect(x: 0, y: 0, width: 460, height: 360),
|
||||||
|
|
|
||||||
590
Sources/CmuxConfig.swift
Normal file
590
Sources/CmuxConfig.swift
Normal file
|
|
@ -0,0 +1,590 @@
|
||||||
|
import Bonsplit
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct CmuxConfigFile: Codable, Sendable {
|
||||||
|
var commands: [CmuxCommandDefinition]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CmuxCommandDefinition: Codable, Sendable, Identifiable {
|
||||||
|
var name: String
|
||||||
|
var description: String?
|
||||||
|
var keywords: [String]?
|
||||||
|
var restart: CmuxRestartBehavior?
|
||||||
|
var workspace: CmuxWorkspaceDefinition?
|
||||||
|
var command: String?
|
||||||
|
var confirm: Bool?
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
"cmux.config.command." + (name.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? name)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
name: String,
|
||||||
|
description: String? = nil,
|
||||||
|
keywords: [String]? = nil,
|
||||||
|
restart: CmuxRestartBehavior? = nil,
|
||||||
|
workspace: CmuxWorkspaceDefinition? = nil,
|
||||||
|
command: String? = nil,
|
||||||
|
confirm: Bool? = nil
|
||||||
|
) {
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
self.keywords = keywords
|
||||||
|
self.restart = restart
|
||||||
|
self.workspace = workspace
|
||||||
|
self.command = command
|
||||||
|
self.confirm = confirm
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
name = try container.decode(String.self, forKey: .name)
|
||||||
|
description = try container.decodeIfPresent(String.self, forKey: .description)
|
||||||
|
keywords = try container.decodeIfPresent([String].self, forKey: .keywords)
|
||||||
|
restart = try container.decodeIfPresent(CmuxRestartBehavior.self, forKey: .restart)
|
||||||
|
workspace = try container.decodeIfPresent(CmuxWorkspaceDefinition.self, forKey: .workspace)
|
||||||
|
command = try container.decodeIfPresent(String.self, forKey: .command)
|
||||||
|
confirm = try container.decodeIfPresent(Bool.self, forKey: .confirm)
|
||||||
|
|
||||||
|
if name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
throw DecodingError.dataCorrupted(
|
||||||
|
DecodingError.Context(
|
||||||
|
codingPath: decoder.codingPath,
|
||||||
|
debugDescription: "Command name must not be blank"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if let cmd = command,
|
||||||
|
cmd.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
throw DecodingError.dataCorrupted(
|
||||||
|
DecodingError.Context(
|
||||||
|
codingPath: decoder.codingPath,
|
||||||
|
debugDescription: "Command '\(name)' must not define a blank 'command'"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if workspace != nil && command != nil {
|
||||||
|
throw DecodingError.dataCorrupted(
|
||||||
|
DecodingError.Context(
|
||||||
|
codingPath: decoder.codingPath,
|
||||||
|
debugDescription: "Command '\(name)' must not define both 'workspace' and 'command'"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if workspace == nil && command == nil {
|
||||||
|
throw DecodingError.dataCorrupted(
|
||||||
|
DecodingError.Context(
|
||||||
|
codingPath: decoder.codingPath,
|
||||||
|
debugDescription: "Command '\(name)' must define either 'workspace' or 'command'"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CmuxRestartBehavior: String, Codable, Sendable {
|
||||||
|
case recreate
|
||||||
|
case ignore
|
||||||
|
case confirm
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CmuxWorkspaceDefinition: Codable, Sendable {
|
||||||
|
var name: String?
|
||||||
|
var cwd: String?
|
||||||
|
var color: String?
|
||||||
|
var layout: CmuxLayoutNode?
|
||||||
|
}
|
||||||
|
|
||||||
|
indirect enum CmuxLayoutNode: Codable, Sendable {
|
||||||
|
case pane(CmuxPaneDefinition)
|
||||||
|
case split(CmuxSplitDefinition)
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case pane
|
||||||
|
case direction
|
||||||
|
case split
|
||||||
|
case children
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
let hasPane = container.contains(.pane)
|
||||||
|
let hasDirection = container.contains(.direction)
|
||||||
|
|
||||||
|
if hasPane && hasDirection {
|
||||||
|
throw DecodingError.dataCorrupted(
|
||||||
|
DecodingError.Context(
|
||||||
|
codingPath: decoder.codingPath,
|
||||||
|
debugDescription: "CmuxLayoutNode must not contain both 'pane' and 'direction' keys"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasPane {
|
||||||
|
let pane = try container.decode(CmuxPaneDefinition.self, forKey: .pane)
|
||||||
|
self = .pane(pane)
|
||||||
|
} else if hasDirection {
|
||||||
|
let splitDef = try CmuxSplitDefinition(from: decoder)
|
||||||
|
self = .split(splitDef)
|
||||||
|
} else {
|
||||||
|
throw DecodingError.dataCorrupted(
|
||||||
|
DecodingError.Context(
|
||||||
|
codingPath: decoder.codingPath,
|
||||||
|
debugDescription: "CmuxLayoutNode must contain either a 'pane' key or a 'direction' key"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
switch self {
|
||||||
|
case .pane(let pane):
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(pane, forKey: .pane)
|
||||||
|
case .split(let split):
|
||||||
|
try split.encode(to: encoder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CmuxSplitDefinition: Codable, Sendable {
|
||||||
|
var direction: CmuxSplitDirection
|
||||||
|
var split: Double?
|
||||||
|
var children: [CmuxLayoutNode]
|
||||||
|
|
||||||
|
init(direction: CmuxSplitDirection, split: Double? = nil, children: [CmuxLayoutNode]) {
|
||||||
|
self.direction = direction
|
||||||
|
self.split = split
|
||||||
|
self.children = children
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
direction = try container.decode(CmuxSplitDirection.self, forKey: .direction)
|
||||||
|
split = try container.decodeIfPresent(Double.self, forKey: .split)
|
||||||
|
children = try container.decode([CmuxLayoutNode].self, forKey: .children)
|
||||||
|
if children.count != 2 {
|
||||||
|
throw DecodingError.dataCorrupted(
|
||||||
|
DecodingError.Context(
|
||||||
|
codingPath: decoder.codingPath,
|
||||||
|
debugDescription: "Split node requires exactly 2 children, got \(children.count)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var clampedSplitPosition: Double {
|
||||||
|
let value = split ?? 0.5
|
||||||
|
return min(0.9, max(0.1, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
var splitOrientation: SplitOrientation {
|
||||||
|
switch direction {
|
||||||
|
case .horizontal: return .horizontal
|
||||||
|
case .vertical: return .vertical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CmuxSplitDirection: String, Codable, Sendable {
|
||||||
|
case horizontal
|
||||||
|
case vertical
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CmuxPaneDefinition: Codable, Sendable {
|
||||||
|
var surfaces: [CmuxSurfaceDefinition]
|
||||||
|
|
||||||
|
init(surfaces: [CmuxSurfaceDefinition]) {
|
||||||
|
self.surfaces = surfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
surfaces = try container.decode([CmuxSurfaceDefinition].self, forKey: .surfaces)
|
||||||
|
if surfaces.isEmpty {
|
||||||
|
throw DecodingError.dataCorrupted(
|
||||||
|
DecodingError.Context(
|
||||||
|
codingPath: decoder.codingPath,
|
||||||
|
debugDescription: "Pane node must contain at least one surface"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CmuxSurfaceDefinition: Codable, Sendable {
|
||||||
|
var type: CmuxSurfaceType
|
||||||
|
var name: String?
|
||||||
|
var command: String?
|
||||||
|
var cwd: String?
|
||||||
|
var env: [String: String]?
|
||||||
|
var url: String?
|
||||||
|
var focus: Bool?
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CmuxSurfaceType: String, Codable, Sendable {
|
||||||
|
case terminal
|
||||||
|
case browser
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class CmuxConfigStore: ObservableObject {
|
||||||
|
@Published private(set) var loadedCommands: [CmuxCommandDefinition] = []
|
||||||
|
@Published private(set) var configRevision: UInt64 = 0
|
||||||
|
|
||||||
|
/// Which config file each command came from, keyed by command id.
|
||||||
|
private(set) var commandSourcePaths: [String: String] = [:]
|
||||||
|
|
||||||
|
private(set) var localConfigPath: String?
|
||||||
|
let globalConfigPath: String = {
|
||||||
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||||
|
return (home as NSString).appendingPathComponent(".config/cmux/cmux.json")
|
||||||
|
}()
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private var localFileWatchSource: DispatchSourceFileSystemObject?
|
||||||
|
private var localFileDescriptor: Int32 = -1
|
||||||
|
private var globalFileWatchSource: DispatchSourceFileSystemObject?
|
||||||
|
private var globalFileDescriptor: Int32 = -1
|
||||||
|
private let watchQueue = DispatchQueue(label: "com.cmux.config-file-watch")
|
||||||
|
|
||||||
|
private static let maxReattachAttempts = 5
|
||||||
|
private static let reattachDelay: TimeInterval = 0.5
|
||||||
|
|
||||||
|
init() {
|
||||||
|
startGlobalFileWatcher()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
localFileWatchSource?.cancel()
|
||||||
|
globalFileWatchSource?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public API
|
||||||
|
|
||||||
|
func wireDirectoryTracking(tabManager: TabManager) {
|
||||||
|
cancellables.removeAll()
|
||||||
|
|
||||||
|
tabManager.$selectedTabId
|
||||||
|
.compactMap { [weak tabManager] tabId -> Workspace? in
|
||||||
|
guard let tabId, let tabManager else { return nil }
|
||||||
|
return tabManager.tabs.first(where: { $0.id == tabId })
|
||||||
|
}
|
||||||
|
.removeDuplicates(by: { $0.id == $1.id })
|
||||||
|
.map { workspace -> AnyPublisher<String, Never> in
|
||||||
|
workspace.$currentDirectory.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.switchToLatest()
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] directory in
|
||||||
|
self?.updateLocalConfigPath(directory)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
if let directory = tabManager.selectedWorkspace?.currentDirectory {
|
||||||
|
updateLocalConfigPath(directory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateLocalConfigPath(_ directory: String?) {
|
||||||
|
let newPath: String?
|
||||||
|
if let directory, !directory.isEmpty {
|
||||||
|
newPath = findCmuxConfig(startingFrom: directory)
|
||||||
|
?? (directory as NSString).appendingPathComponent("cmux.json")
|
||||||
|
} else {
|
||||||
|
newPath = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard newPath != localConfigPath else { return }
|
||||||
|
stopLocalFileWatcher()
|
||||||
|
localConfigPath = newPath
|
||||||
|
if newPath != nil {
|
||||||
|
startLocalFileWatcher()
|
||||||
|
}
|
||||||
|
loadAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findCmuxConfig(startingFrom directory: String) -> String? {
|
||||||
|
var current = directory
|
||||||
|
let fs = FileManager.default
|
||||||
|
while true {
|
||||||
|
let candidate = (current as NSString).appendingPathComponent("cmux.json")
|
||||||
|
if fs.fileExists(atPath: candidate) {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
let parent = (current as NSString).deletingLastPathComponent
|
||||||
|
if parent == current { break }
|
||||||
|
current = parent
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAll() {
|
||||||
|
var commands: [CmuxCommandDefinition] = []
|
||||||
|
var seenNames = Set<String>()
|
||||||
|
var sourcePaths: [String: String] = [:]
|
||||||
|
|
||||||
|
// Local config takes precedence
|
||||||
|
if let localPath = localConfigPath {
|
||||||
|
if let localConfig = parseConfig(at: localPath) {
|
||||||
|
for command in localConfig.commands {
|
||||||
|
if !seenNames.contains(command.name) {
|
||||||
|
commands.append(command)
|
||||||
|
seenNames.insert(command.name)
|
||||||
|
sourcePaths[command.id] = localPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global config fills in the rest
|
||||||
|
if let globalConfig = parseConfig(at: globalConfigPath) {
|
||||||
|
for command in globalConfig.commands {
|
||||||
|
if !seenNames.contains(command.name) {
|
||||||
|
commands.append(command)
|
||||||
|
seenNames.insert(command.name)
|
||||||
|
sourcePaths[command.id] = globalConfigPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedCommands = commands
|
||||||
|
commandSourcePaths = sourcePaths
|
||||||
|
configRevision &+= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Parsing
|
||||||
|
|
||||||
|
private func parseConfig(at path: String) -> CmuxConfigFile? {
|
||||||
|
guard FileManager.default.fileExists(atPath: path),
|
||||||
|
let data = FileManager.default.contents(atPath: path),
|
||||||
|
!data.isEmpty else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
return try JSONDecoder().decode(CmuxConfigFile.self, from: data)
|
||||||
|
} catch {
|
||||||
|
NSLog("[CmuxConfig] parse error at %@: %@", path, String(describing: error))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - File watching (local)
|
||||||
|
|
||||||
|
private func startLocalFileWatcher() {
|
||||||
|
guard let path = localConfigPath else { return }
|
||||||
|
let fd = open(path, O_EVTONLY)
|
||||||
|
guard fd >= 0 else {
|
||||||
|
// File doesn't exist yet — watch the directory instead
|
||||||
|
startLocalDirectoryWatcher()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
localFileDescriptor = fd
|
||||||
|
|
||||||
|
let source = DispatchSource.makeFileSystemObjectSource(
|
||||||
|
fileDescriptor: fd,
|
||||||
|
eventMask: [.write, .delete, .rename, .extend],
|
||||||
|
queue: watchQueue
|
||||||
|
)
|
||||||
|
|
||||||
|
source.setEventHandler { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
let flags = source.data
|
||||||
|
if flags.contains(.delete) || flags.contains(.rename) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.stopLocalFileWatcher()
|
||||||
|
self.loadAll()
|
||||||
|
self.scheduleLocalReattach(attempt: 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.loadAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
source.setCancelHandler {
|
||||||
|
Darwin.close(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
source.resume()
|
||||||
|
localFileWatchSource = source
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startLocalDirectoryWatcher() {
|
||||||
|
guard let path = localConfigPath else { return }
|
||||||
|
let dirPath = (path as NSString).deletingLastPathComponent
|
||||||
|
let fd = open(dirPath, O_EVTONLY)
|
||||||
|
guard fd >= 0 else { return }
|
||||||
|
localFileDescriptor = fd
|
||||||
|
|
||||||
|
let source = DispatchSource.makeFileSystemObjectSource(
|
||||||
|
fileDescriptor: fd,
|
||||||
|
eventMask: [.write, .link, .rename],
|
||||||
|
queue: watchQueue
|
||||||
|
)
|
||||||
|
|
||||||
|
source.setEventHandler { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let configPath = self.localConfigPath,
|
||||||
|
FileManager.default.fileExists(atPath: configPath) else { return }
|
||||||
|
// File appeared — switch to file-level watching
|
||||||
|
self.stopLocalFileWatcher()
|
||||||
|
self.loadAll()
|
||||||
|
self.startLocalFileWatcher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
source.setCancelHandler {
|
||||||
|
Darwin.close(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
source.resume()
|
||||||
|
localFileWatchSource = source
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleLocalReattach(attempt: Int) {
|
||||||
|
guard attempt <= Self.maxReattachAttempts else { return }
|
||||||
|
watchQueue.asyncAfter(deadline: .now() + Self.reattachDelay) { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let path = self.localConfigPath else { return }
|
||||||
|
if FileManager.default.fileExists(atPath: path) {
|
||||||
|
self.loadAll()
|
||||||
|
self.startLocalFileWatcher()
|
||||||
|
} else {
|
||||||
|
self.startLocalDirectoryWatcher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopLocalFileWatcher() {
|
||||||
|
if let source = localFileWatchSource {
|
||||||
|
source.cancel()
|
||||||
|
localFileWatchSource = nil
|
||||||
|
}
|
||||||
|
localFileDescriptor = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - File watching (global)
|
||||||
|
|
||||||
|
private func startGlobalFileWatcher() {
|
||||||
|
let fd = open(globalConfigPath, O_EVTONLY)
|
||||||
|
guard fd >= 0 else {
|
||||||
|
startGlobalDirectoryWatcher()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
globalFileDescriptor = fd
|
||||||
|
|
||||||
|
let source = DispatchSource.makeFileSystemObjectSource(
|
||||||
|
fileDescriptor: fd,
|
||||||
|
eventMask: [.write, .delete, .rename, .extend],
|
||||||
|
queue: watchQueue
|
||||||
|
)
|
||||||
|
|
||||||
|
source.setEventHandler { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
let flags = source.data
|
||||||
|
if flags.contains(.delete) || flags.contains(.rename) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.stopGlobalFileWatcher()
|
||||||
|
self.loadAll()
|
||||||
|
self.scheduleGlobalReattach(attempt: 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.loadAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
source.setCancelHandler {
|
||||||
|
Darwin.close(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
source.resume()
|
||||||
|
globalFileWatchSource = source
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleGlobalReattach(attempt: Int) {
|
||||||
|
guard attempt <= Self.maxReattachAttempts else {
|
||||||
|
startGlobalDirectoryWatcher()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
watchQueue.asyncAfter(deadline: .now() + Self.reattachDelay) { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if FileManager.default.fileExists(atPath: self.globalConfigPath) {
|
||||||
|
self.loadAll()
|
||||||
|
self.startGlobalFileWatcher()
|
||||||
|
} else {
|
||||||
|
self.scheduleGlobalReattach(attempt: attempt + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startGlobalDirectoryWatcher() {
|
||||||
|
let dirPath = (globalConfigPath as NSString).deletingLastPathComponent
|
||||||
|
let fm = FileManager.default
|
||||||
|
if !fm.fileExists(atPath: dirPath) {
|
||||||
|
try? fm.createDirectory(atPath: dirPath, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
let fd = open(dirPath, O_EVTONLY)
|
||||||
|
guard fd >= 0 else { return }
|
||||||
|
globalFileDescriptor = fd
|
||||||
|
|
||||||
|
let source = DispatchSource.makeFileSystemObjectSource(
|
||||||
|
fileDescriptor: fd,
|
||||||
|
eventMask: [.write, .link, .rename],
|
||||||
|
queue: watchQueue
|
||||||
|
)
|
||||||
|
|
||||||
|
source.setEventHandler { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard FileManager.default.fileExists(atPath: self.globalConfigPath) else { return }
|
||||||
|
self.stopGlobalFileWatcher()
|
||||||
|
self.loadAll()
|
||||||
|
self.startGlobalFileWatcher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
source.setCancelHandler {
|
||||||
|
Darwin.close(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
source.resume()
|
||||||
|
globalFileWatchSource = source
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopGlobalFileWatcher() {
|
||||||
|
if let source = globalFileWatchSource {
|
||||||
|
source.cancel()
|
||||||
|
globalFileWatchSource = nil
|
||||||
|
}
|
||||||
|
globalFileDescriptor = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CmuxConfigStore {
|
||||||
|
static func resolveCwd(_ cwd: String?, relativeTo baseCwd: String) -> String {
|
||||||
|
guard let cwd, !cwd.isEmpty, cwd != "." else {
|
||||||
|
return baseCwd
|
||||||
|
}
|
||||||
|
if cwd.hasPrefix("~/") || cwd == "~" {
|
||||||
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||||
|
if cwd == "~" { return home }
|
||||||
|
return (home as NSString).appendingPathComponent(String(cwd.dropFirst(2)))
|
||||||
|
}
|
||||||
|
if cwd.hasPrefix("/") {
|
||||||
|
return cwd
|
||||||
|
}
|
||||||
|
return (baseCwd as NSString).appendingPathComponent(cwd)
|
||||||
|
}
|
||||||
|
}
|
||||||
130
Sources/CmuxConfigExecutor.swift
Normal file
130
Sources/CmuxConfigExecutor.swift
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import AppKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct CmuxConfigExecutor {
|
||||||
|
|
||||||
|
static func execute(
|
||||||
|
command: CmuxCommandDefinition,
|
||||||
|
tabManager: TabManager,
|
||||||
|
baseCwd: String,
|
||||||
|
configSourcePath: String?,
|
||||||
|
globalConfigPath: String
|
||||||
|
) {
|
||||||
|
if let workspace = command.workspace {
|
||||||
|
executeWorkspaceCommand(command: command, workspace: workspace, tabManager: tabManager, baseCwd: baseCwd)
|
||||||
|
} else if let shellCommand = command.command {
|
||||||
|
let needsConfirm = command.confirm ?? false
|
||||||
|
if needsConfirm, let sourcePath = configSourcePath {
|
||||||
|
let trusted = CmuxDirectoryTrust.shared.isTrusted(
|
||||||
|
configPath: sourcePath,
|
||||||
|
globalConfigPath: globalConfigPath
|
||||||
|
)
|
||||||
|
if !trusted {
|
||||||
|
guard showConfirmDialog(command: shellCommand, configPath: sourcePath) else { return }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard let terminal = tabManager.selectedWorkspace?.focusedTerminalPanel else { return }
|
||||||
|
terminal.sendInput(shellCommand + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a confirmation dialog with the command text and a "trust this directory" checkbox.
|
||||||
|
/// Returns true if the user chose to run, false if cancelled.
|
||||||
|
private static func showConfirmDialog(command: String, configPath: String) -> Bool {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = String(
|
||||||
|
localized: "dialog.cmuxConfig.confirmCommand.title",
|
||||||
|
defaultValue: "Run Command"
|
||||||
|
)
|
||||||
|
let messageFormat = String(
|
||||||
|
localized: "dialog.cmuxConfig.confirmCommand.messageWithCommand",
|
||||||
|
defaultValue: "This will run the following command:\n\n%@"
|
||||||
|
)
|
||||||
|
alert.informativeText = String(format: messageFormat, sanitizeForDisplay(command))
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
alert.addButton(withTitle: String(
|
||||||
|
localized: "dialog.cmuxConfig.confirmCommand.run",
|
||||||
|
defaultValue: "Run"
|
||||||
|
))
|
||||||
|
alert.addButton(withTitle: String(
|
||||||
|
localized: "dialog.cmuxConfig.confirmCommand.cancel",
|
||||||
|
defaultValue: "Cancel"
|
||||||
|
))
|
||||||
|
|
||||||
|
let checkbox = NSButton(checkboxWithTitle: String(
|
||||||
|
localized: "dialog.cmuxConfig.confirmCommand.trustDirectory",
|
||||||
|
defaultValue: "Always trust commands from this folder"
|
||||||
|
), target: nil, action: nil)
|
||||||
|
checkbox.state = .off
|
||||||
|
alert.accessoryView = checkbox
|
||||||
|
|
||||||
|
let response = alert.runModal()
|
||||||
|
guard response == .alertFirstButtonReturn else { return false }
|
||||||
|
|
||||||
|
if checkbox.state == .on {
|
||||||
|
CmuxDirectoryTrust.shared.trust(configPath: configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func sanitizeForDisplay(_ text: String) -> String {
|
||||||
|
let dangerous: Set<Unicode.Scalar> = [
|
||||||
|
"\u{200B}", "\u{200C}", "\u{200D}", "\u{200E}", "\u{200F}",
|
||||||
|
"\u{202A}", "\u{202B}", "\u{202C}", "\u{202D}", "\u{202E}",
|
||||||
|
"\u{2066}", "\u{2067}", "\u{2068}", "\u{2069}",
|
||||||
|
"\u{FEFF}",
|
||||||
|
]
|
||||||
|
let filtered = String(text.unicodeScalars.filter { !dangerous.contains($0) })
|
||||||
|
return filtered.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func executeWorkspaceCommand(
|
||||||
|
command: CmuxCommandDefinition,
|
||||||
|
workspace wsDef: CmuxWorkspaceDefinition,
|
||||||
|
tabManager: TabManager,
|
||||||
|
baseCwd: String
|
||||||
|
) {
|
||||||
|
let workspaceName = wsDef.name ?? command.name
|
||||||
|
let restart = command.restart ?? .ignore
|
||||||
|
|
||||||
|
if let existing = tabManager.tabs.first(where: { $0.customTitle == workspaceName }) {
|
||||||
|
switch restart {
|
||||||
|
case .ignore:
|
||||||
|
tabManager.selectWorkspace(existing)
|
||||||
|
return
|
||||||
|
case .recreate:
|
||||||
|
tabManager.closeWorkspace(existing)
|
||||||
|
case .confirm:
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = String(
|
||||||
|
localized: "dialog.cmuxConfig.confirmRestart.title",
|
||||||
|
defaultValue: "Workspace Already Exists"
|
||||||
|
)
|
||||||
|
alert.informativeText = String(
|
||||||
|
localized: "dialog.cmuxConfig.confirmRestart.message",
|
||||||
|
defaultValue: "A workspace with this name already exists. Close it and create a new one?"
|
||||||
|
)
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
alert.addButton(withTitle: String(localized: "dialog.cmuxConfig.confirmRestart.recreate", defaultValue: "Recreate"))
|
||||||
|
alert.addButton(withTitle: String(localized: "dialog.cmuxConfig.confirmRestart.cancel", defaultValue: "Cancel"))
|
||||||
|
guard alert.runModal() == .alertFirstButtonReturn else {
|
||||||
|
tabManager.selectWorkspace(existing)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tabManager.closeWorkspace(existing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedCwd = CmuxConfigStore.resolveCwd(wsDef.cwd, relativeTo: baseCwd)
|
||||||
|
let newWorkspace = tabManager.addWorkspace(workingDirectory: resolvedCwd)
|
||||||
|
newWorkspace.setCustomTitle(workspaceName)
|
||||||
|
if let color = wsDef.color {
|
||||||
|
newWorkspace.setCustomColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let layout = wsDef.layout else { return }
|
||||||
|
newWorkspace.applyCustomLayout(layout, baseCwd: resolvedCwd)
|
||||||
|
}
|
||||||
|
}
|
||||||
112
Sources/CmuxDirectoryTrust.swift
Normal file
112
Sources/CmuxDirectoryTrust.swift
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Manages trusted directories for cmux.json command execution.
|
||||||
|
/// When a directory (or its git repo root) is trusted, `confirm: true` commands
|
||||||
|
/// from that directory's cmux.json skip the confirmation dialog.
|
||||||
|
/// Global config (~/.config/cmux/cmux.json) is always trusted.
|
||||||
|
final class CmuxDirectoryTrust {
|
||||||
|
static let shared = CmuxDirectoryTrust()
|
||||||
|
|
||||||
|
private let storePath: String
|
||||||
|
private var trustedPaths: Set<String>
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
let appSupport = FileManager.default.urls(
|
||||||
|
for: .applicationSupportDirectory, in: .userDomainMask
|
||||||
|
).first!.appendingPathComponent("cmux")
|
||||||
|
storePath = appSupport.appendingPathComponent("trusted-directories.json").path
|
||||||
|
|
||||||
|
let fm = FileManager.default
|
||||||
|
if !fm.fileExists(atPath: appSupport.path) {
|
||||||
|
try? fm.createDirectory(atPath: appSupport.path, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let data = fm.contents(atPath: storePath),
|
||||||
|
let paths = try? JSONDecoder().decode([String].self, from: data) {
|
||||||
|
trustedPaths = Set(paths)
|
||||||
|
} else {
|
||||||
|
trustedPaths = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a cmux.json path is trusted.
|
||||||
|
/// Global config is always trusted. For local configs, check the git repo root
|
||||||
|
/// (or the cmux.json parent directory if not in a git repo).
|
||||||
|
func isTrusted(configPath: String, globalConfigPath: String) -> Bool {
|
||||||
|
if configPath == globalConfigPath { return true }
|
||||||
|
let trustKey = Self.trustKey(for: configPath)
|
||||||
|
return trustedPaths.contains(trustKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trust the directory containing a cmux.json. If the cmux.json is inside a git
|
||||||
|
/// repo, trusts the repo root (covering all subdirectories).
|
||||||
|
func trust(configPath: String) {
|
||||||
|
let trustKey = Self.trustKey(for: configPath)
|
||||||
|
trustedPaths.insert(trustKey)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove trust for a directory.
|
||||||
|
func revokeTrust(configPath: String) {
|
||||||
|
let trustKey = Self.trustKey(for: configPath)
|
||||||
|
trustedPaths.remove(trustKey)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove trust by the trust key directly (as stored/displayed in settings).
|
||||||
|
func revokeTrustByPath(_ path: String) {
|
||||||
|
trustedPaths.remove(path)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All currently trusted paths.
|
||||||
|
var allTrustedPaths: [String] {
|
||||||
|
Array(trustedPaths).sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace all trusted paths (used by Settings textarea save).
|
||||||
|
func replaceAll(with paths: [String]) {
|
||||||
|
trustedPaths = Set(paths)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all trusted directories.
|
||||||
|
func clearAll() {
|
||||||
|
trustedPaths.removeAll()
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
/// Resolve the trust key for a cmux.json path: git repo root if inside a repo,
|
||||||
|
/// otherwise the cmux.json's parent directory.
|
||||||
|
static func trustKey(for configPath: String) -> String {
|
||||||
|
let configDir = (configPath as NSString).deletingLastPathComponent
|
||||||
|
if let gitRoot = findGitRoot(from: configDir) {
|
||||||
|
return gitRoot
|
||||||
|
}
|
||||||
|
return configDir
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk up from `directory` looking for a `.git` directory or file.
|
||||||
|
private static func findGitRoot(from directory: String) -> String? {
|
||||||
|
let fm = FileManager.default
|
||||||
|
var current = directory
|
||||||
|
while true {
|
||||||
|
let gitPath = (current as NSString).appendingPathComponent(".git")
|
||||||
|
if fm.fileExists(atPath: gitPath) {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
let parent = (current as NSString).deletingLastPathComponent
|
||||||
|
if parent == current { break }
|
||||||
|
current = parent
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func save() {
|
||||||
|
let sorted = trustedPaths.sorted()
|
||||||
|
guard let data = try? JSONEncoder().encode(sorted) else { return }
|
||||||
|
FileManager.default.createFile(atPath: storePath, contents: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1553,6 +1553,7 @@ struct ContentView: View {
|
||||||
@EnvironmentObject var notificationStore: TerminalNotificationStore
|
@EnvironmentObject var notificationStore: TerminalNotificationStore
|
||||||
@EnvironmentObject var sidebarState: SidebarState
|
@EnvironmentObject var sidebarState: SidebarState
|
||||||
@EnvironmentObject var sidebarSelectionState: SidebarSelectionState
|
@EnvironmentObject var sidebarSelectionState: SidebarSelectionState
|
||||||
|
@EnvironmentObject var cmuxConfigStore: CmuxConfigStore
|
||||||
@State private var sidebarWidth: CGFloat = 200
|
@State private var sidebarWidth: CGFloat = 200
|
||||||
@State private var hoveredResizerHandles: Set<SidebarResizerHandle> = []
|
@State private var hoveredResizerHandles: Set<SidebarResizerHandle> = []
|
||||||
@State private var isResizerDragging = false
|
@State private var isResizerDragging = false
|
||||||
|
|
@ -4659,7 +4660,10 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func commandPaletteCommandsFingerprint(commandsContext: CommandPaletteCommandsContext) -> Int {
|
private func commandPaletteCommandsFingerprint(commandsContext: CommandPaletteCommandsContext) -> Int {
|
||||||
commandsContext.snapshot.fingerprint()
|
var hasher = Hasher()
|
||||||
|
hasher.combine(commandsContext.snapshot.fingerprint())
|
||||||
|
hasher.combine(cmuxConfigStore.configRevision)
|
||||||
|
return hasher.finalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func commandPaletteSwitcherEntriesFingerprint(includeSurfaces: Bool) -> Int {
|
private func commandPaletteSwitcherEntriesFingerprint(includeSurfaces: Bool) -> Int {
|
||||||
|
|
@ -5963,9 +5967,37 @@ struct ContentView: View {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let cmuxConfigDefaultSubtitle = constant(String(localized: "command.cmuxConfig.subtitle", defaultValue: "cmux.json"))
|
||||||
|
for command in cmuxConfigStore.loadedCommands {
|
||||||
|
let commandName = sanitizeCmuxConfigPaletteText(command.name)
|
||||||
|
let subtitle = command.description
|
||||||
|
.map { sanitizeCmuxConfigPaletteText($0) }
|
||||||
|
.flatMap { $0.isEmpty ? nil : constant($0) }
|
||||||
|
?? cmuxConfigDefaultSubtitle
|
||||||
|
contributions.append(
|
||||||
|
CommandPaletteCommandContribution(
|
||||||
|
commandId: command.id,
|
||||||
|
title: constant(String(localized: "command.cmuxConfig.customTitle", defaultValue: "Custom: \(commandName)")),
|
||||||
|
subtitle: subtitle,
|
||||||
|
keywords: command.keywords ?? []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return contributions
|
return contributions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func sanitizeCmuxConfigPaletteText(_ text: String) -> String {
|
||||||
|
let dangerous: Set<Unicode.Scalar> = [
|
||||||
|
"\u{200B}", "\u{200C}", "\u{200D}", "\u{200E}", "\u{200F}",
|
||||||
|
"\u{202A}", "\u{202B}", "\u{202C}", "\u{202D}", "\u{202E}",
|
||||||
|
"\u{2066}", "\u{2067}", "\u{2068}", "\u{2069}",
|
||||||
|
"\u{FEFF}",
|
||||||
|
]
|
||||||
|
let filtered = String(text.unicodeScalars.filter { !dangerous.contains($0) })
|
||||||
|
return filtered.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
|
||||||
private func registerCommandPaletteHandlers(_ registry: inout CommandPaletteHandlerRegistry) {
|
private func registerCommandPaletteHandlers(_ registry: inout CommandPaletteHandlerRegistry) {
|
||||||
registry.register(commandId: "palette.newWorkspace") {
|
registry.register(commandId: "palette.newWorkspace") {
|
||||||
tabManager.addWorkspace()
|
tabManager.addWorkspace()
|
||||||
|
|
@ -6294,6 +6326,24 @@ struct ContentView: View {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for command in cmuxConfigStore.loadedCommands {
|
||||||
|
let captured = command
|
||||||
|
let sourcePath = cmuxConfigStore.commandSourcePaths[command.id]
|
||||||
|
let globalPath = cmuxConfigStore.globalConfigPath
|
||||||
|
registry.register(commandId: command.id) {
|
||||||
|
let rawCwd = tabManager.selectedWorkspace?.currentDirectory
|
||||||
|
let baseCwd = (rawCwd?.isEmpty == false) ? rawCwd!
|
||||||
|
: FileManager.default.homeDirectoryForCurrentUser.path
|
||||||
|
CmuxConfigExecutor.execute(
|
||||||
|
command: captured,
|
||||||
|
tabManager: tabManager,
|
||||||
|
baseCwd: baseCwd,
|
||||||
|
configSourcePath: sourcePath,
|
||||||
|
globalConfigPath: globalPath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var focusedPanelContext: (workspace: Workspace, panelId: UUID, panel: any Panel)? {
|
private var focusedPanelContext: (workspace: Workspace, panelId: UUID, panel: any Panel)? {
|
||||||
|
|
|
||||||
|
|
@ -3882,6 +3882,69 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
||||||
writeTextData(data, to: surface)
|
writeTextData(data, to: surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send text with control characters (Return, Tab, etc.) delivered as key
|
||||||
|
/// events so the shell processes them, while regular text is sent via the
|
||||||
|
/// normal key-text path. Mirrors `TerminalController.sendSocketText`.
|
||||||
|
func sendInput(_ text: String) {
|
||||||
|
guard let surface = surface else { return }
|
||||||
|
var bufferedText = ""
|
||||||
|
var previousWasCR = false
|
||||||
|
for scalar in text.unicodeScalars {
|
||||||
|
switch scalar.value {
|
||||||
|
case 0x0A: // \n — skip if preceded by \r (already sent Return)
|
||||||
|
if !previousWasCR {
|
||||||
|
flushText(&bufferedText, surface: surface)
|
||||||
|
sendKeyEvent(surface: surface, keycode: 0x24) // kVK_Return
|
||||||
|
}
|
||||||
|
previousWasCR = false
|
||||||
|
case 0x0D:
|
||||||
|
flushText(&bufferedText, surface: surface)
|
||||||
|
sendKeyEvent(surface: surface, keycode: 0x24) // kVK_Return
|
||||||
|
previousWasCR = true
|
||||||
|
case 0x09:
|
||||||
|
flushText(&bufferedText, surface: surface)
|
||||||
|
sendKeyEvent(surface: surface, keycode: 0x30) // kVK_Tab
|
||||||
|
previousWasCR = false
|
||||||
|
case 0x1B:
|
||||||
|
flushText(&bufferedText, surface: surface)
|
||||||
|
sendKeyEvent(surface: surface, keycode: 0x35) // kVK_Escape
|
||||||
|
previousWasCR = false
|
||||||
|
default:
|
||||||
|
bufferedText.unicodeScalars.append(scalar)
|
||||||
|
previousWasCR = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushText(&bufferedText, surface: surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func flushText(_ buffer: inout String, surface: ghostty_surface_t) {
|
||||||
|
guard !buffer.isEmpty else { return }
|
||||||
|
var keyEvent = ghostty_input_key_s()
|
||||||
|
keyEvent.action = GHOSTTY_ACTION_PRESS
|
||||||
|
keyEvent.keycode = 0
|
||||||
|
keyEvent.mods = GHOSTTY_MODS_NONE
|
||||||
|
keyEvent.consumed_mods = GHOSTTY_MODS_NONE
|
||||||
|
keyEvent.unshifted_codepoint = 0
|
||||||
|
keyEvent.composing = false
|
||||||
|
buffer.withCString { ptr in
|
||||||
|
keyEvent.text = ptr
|
||||||
|
_ = ghostty_surface_key(surface, keyEvent)
|
||||||
|
}
|
||||||
|
buffer.removeAll(keepingCapacity: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendKeyEvent(surface: ghostty_surface_t, keycode: UInt32) {
|
||||||
|
var keyEvent = ghostty_input_key_s()
|
||||||
|
keyEvent.action = GHOSTTY_ACTION_PRESS
|
||||||
|
keyEvent.keycode = keycode
|
||||||
|
keyEvent.mods = GHOSTTY_MODS_NONE
|
||||||
|
keyEvent.consumed_mods = GHOSTTY_MODS_NONE
|
||||||
|
keyEvent.unshifted_codepoint = 0
|
||||||
|
keyEvent.composing = false
|
||||||
|
keyEvent.text = nil
|
||||||
|
_ = ghostty_surface_key(surface, keyEvent)
|
||||||
|
}
|
||||||
|
|
||||||
func requestBackgroundSurfaceStartIfNeeded() {
|
func requestBackgroundSurfaceStartIfNeeded() {
|
||||||
if !Thread.isMainThread {
|
if !Thread.isMainThread {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,10 @@ final class TerminalPanel: Panel, ObservableObject {
|
||||||
surface.sendText(text)
|
surface.sendText(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendInput(_ text: String) {
|
||||||
|
surface.sendInput(text)
|
||||||
|
}
|
||||||
|
|
||||||
func performBindingAction(_ action: String) -> Bool {
|
func performBindingAction(_ action: String) -> Bool {
|
||||||
surface.performBindingAction(action)
|
surface.performBindingAction(action)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -696,6 +696,227 @@ extension Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - cmux.json custom layout
|
||||||
|
|
||||||
|
extension Workspace {
|
||||||
|
|
||||||
|
func applyCustomLayout(_ layout: CmuxLayoutNode, baseCwd: String) {
|
||||||
|
guard let rootPaneId = bonsplitController.allPaneIds.first else { return }
|
||||||
|
|
||||||
|
var leaves: [(paneId: PaneID, surfaces: [CmuxSurfaceDefinition])] = []
|
||||||
|
buildCustomLayoutTree(layout, inPane: rootPaneId, leaves: &leaves)
|
||||||
|
|
||||||
|
// First leaf reuses the initial terminal created by addWorkspace;
|
||||||
|
// subsequent leaves were created via newTerminalSplit which also seeds
|
||||||
|
// a placeholder terminal.
|
||||||
|
var focusPanelId: UUID?
|
||||||
|
for leaf in leaves {
|
||||||
|
populateCustomPane(leaf.paneId, surfaces: leaf.surfaces, baseCwd: baseCwd, focusPanelId: &focusPanelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
let liveRoot = bonsplitController.treeSnapshot()
|
||||||
|
applyCustomDividerPositions(configNode: layout, liveNode: liveRoot)
|
||||||
|
|
||||||
|
if let focusPanelId {
|
||||||
|
focusPanel(focusPanelId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildCustomLayoutTree(
|
||||||
|
_ node: CmuxLayoutNode,
|
||||||
|
inPane paneId: PaneID,
|
||||||
|
leaves: inout [(paneId: PaneID, surfaces: [CmuxSurfaceDefinition])]
|
||||||
|
) {
|
||||||
|
switch node {
|
||||||
|
case .pane(let pane):
|
||||||
|
leaves.append((paneId: paneId, surfaces: pane.surfaces))
|
||||||
|
|
||||||
|
case .split(let split):
|
||||||
|
guard split.children.count == 2 else {
|
||||||
|
NSLog("[CmuxConfig] split node requires exactly 2 children, got %d", split.children.count)
|
||||||
|
leaves.append((paneId: paneId, surfaces: []))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var anchorPanelId = bonsplitController
|
||||||
|
.tabs(inPane: paneId)
|
||||||
|
.compactMap { panelIdFromSurfaceId($0.id) }
|
||||||
|
.first
|
||||||
|
|
||||||
|
if anchorPanelId == nil {
|
||||||
|
anchorPanelId = newTerminalSurface(inPane: paneId, focus: false)?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let anchorPanelId,
|
||||||
|
let newSplitPanel = newTerminalSplit(
|
||||||
|
from: anchorPanelId,
|
||||||
|
orientation: split.splitOrientation,
|
||||||
|
insertFirst: false,
|
||||||
|
focus: false
|
||||||
|
),
|
||||||
|
let secondPaneId = self.paneId(forPanelId: newSplitPanel.id) else {
|
||||||
|
leaves.append((paneId: paneId, surfaces: []))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCustomLayoutTree(split.children[0], inPane: paneId, leaves: &leaves)
|
||||||
|
buildCustomLayoutTree(split.children[1], inPane: secondPaneId, leaves: &leaves)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func populateCustomPane(
|
||||||
|
_ paneId: PaneID,
|
||||||
|
surfaces: [CmuxSurfaceDefinition],
|
||||||
|
baseCwd: String,
|
||||||
|
focusPanelId: inout UUID?
|
||||||
|
) {
|
||||||
|
let existingPanelIds = bonsplitController
|
||||||
|
.tabs(inPane: paneId)
|
||||||
|
.compactMap { panelIdFromSurfaceId($0.id) }
|
||||||
|
|
||||||
|
guard !surfaces.isEmpty else { return }
|
||||||
|
|
||||||
|
let firstSurface = surfaces[0]
|
||||||
|
if let placeholderPanelId = existingPanelIds.first {
|
||||||
|
configureExistingSurface(
|
||||||
|
panelId: placeholderPanelId,
|
||||||
|
inPane: paneId,
|
||||||
|
surface: firstSurface,
|
||||||
|
baseCwd: baseCwd,
|
||||||
|
focusPanelId: &focusPanelId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for surfaceIndex in 1..<surfaces.count {
|
||||||
|
createNewSurface(
|
||||||
|
inPane: paneId,
|
||||||
|
surface: surfaces[surfaceIndex],
|
||||||
|
baseCwd: baseCwd,
|
||||||
|
focusPanelId: &focusPanelId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureExistingSurface(
|
||||||
|
panelId: UUID,
|
||||||
|
inPane paneId: PaneID,
|
||||||
|
surface: CmuxSurfaceDefinition,
|
||||||
|
baseCwd: String,
|
||||||
|
focusPanelId: inout UUID?
|
||||||
|
) {
|
||||||
|
switch surface.type {
|
||||||
|
case .terminal where surface.cwd != nil || surface.env != nil:
|
||||||
|
// Placeholder can't change cwd/env — replace it
|
||||||
|
let resolvedCwd = CmuxConfigStore.resolveCwd(surface.cwd, relativeTo: baseCwd)
|
||||||
|
if let panel = newTerminalSurface(
|
||||||
|
inPane: paneId,
|
||||||
|
focus: false,
|
||||||
|
workingDirectory: resolvedCwd,
|
||||||
|
startupEnvironment: surface.env ?? [:]
|
||||||
|
) {
|
||||||
|
_ = closePanel(panelId, force: true)
|
||||||
|
if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) }
|
||||||
|
if surface.focus == true { focusPanelId = panel.id }
|
||||||
|
if let command = surface.command { sendInputWhenReady(command + "\n", to: panel) }
|
||||||
|
}
|
||||||
|
|
||||||
|
case .terminal:
|
||||||
|
if let name = surface.name { setPanelCustomTitle(panelId: panelId, title: name) }
|
||||||
|
if surface.focus == true { focusPanelId = panelId }
|
||||||
|
if let command = surface.command, let terminal = terminalPanel(for: panelId) {
|
||||||
|
sendInputWhenReady(command + "\n", to: terminal)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .browser:
|
||||||
|
let url = surface.url.flatMap { URL(string: $0) }
|
||||||
|
if let panel = newBrowserSurface(inPane: paneId, url: url, focus: false) {
|
||||||
|
_ = closePanel(panelId, force: true)
|
||||||
|
if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) }
|
||||||
|
if surface.focus == true { focusPanelId = panel.id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createNewSurface(
|
||||||
|
inPane paneId: PaneID,
|
||||||
|
surface: CmuxSurfaceDefinition,
|
||||||
|
baseCwd: String,
|
||||||
|
focusPanelId: inout UUID?
|
||||||
|
) {
|
||||||
|
switch surface.type {
|
||||||
|
case .terminal:
|
||||||
|
let resolvedCwd = CmuxConfigStore.resolveCwd(surface.cwd, relativeTo: baseCwd)
|
||||||
|
if let panel = newTerminalSurface(
|
||||||
|
inPane: paneId,
|
||||||
|
focus: false,
|
||||||
|
workingDirectory: resolvedCwd,
|
||||||
|
startupEnvironment: surface.env ?? [:]
|
||||||
|
) {
|
||||||
|
if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) }
|
||||||
|
if surface.focus == true { focusPanelId = panel.id }
|
||||||
|
if let command = surface.command { sendInputWhenReady(command + "\n", to: panel) }
|
||||||
|
}
|
||||||
|
|
||||||
|
case .browser:
|
||||||
|
let url = surface.url.flatMap { URL(string: $0) }
|
||||||
|
if let panel = newBrowserSurface(inPane: paneId, url: url, focus: false) {
|
||||||
|
if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) }
|
||||||
|
if surface.focus == true { focusPanelId = panel.id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyCustomDividerPositions(
|
||||||
|
configNode: CmuxLayoutNode,
|
||||||
|
liveNode: ExternalTreeNode
|
||||||
|
) {
|
||||||
|
switch (configNode, liveNode) {
|
||||||
|
case (.split(let configSplit), .split(let liveSplit)):
|
||||||
|
if let splitID = UUID(uuidString: liveSplit.id) {
|
||||||
|
_ = bonsplitController.setDividerPosition(
|
||||||
|
CGFloat(configSplit.clampedSplitPosition),
|
||||||
|
forSplit: splitID,
|
||||||
|
fromExternal: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if configSplit.children.count == 2 {
|
||||||
|
applyCustomDividerPositions(configNode: configSplit.children[0], liveNode: liveSplit.first)
|
||||||
|
applyCustomDividerPositions(configNode: configSplit.children[1], liveNode: liveSplit.second)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendInputWhenReady(_ text: String, to panel: TerminalPanel) {
|
||||||
|
if panel.surface.surface != nil {
|
||||||
|
panel.sendInput(text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolved = false
|
||||||
|
var observer: NSObjectProtocol?
|
||||||
|
|
||||||
|
observer = NotificationCenter.default.addObserver(
|
||||||
|
forName: .terminalSurfaceDidBecomeReady,
|
||||||
|
object: panel.surface,
|
||||||
|
queue: .main
|
||||||
|
) { [weak panel] _ in
|
||||||
|
guard !resolved, let panel else { return }
|
||||||
|
resolved = true
|
||||||
|
if let observer { NotificationCenter.default.removeObserver(observer) }
|
||||||
|
panel.sendInput(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||||
|
guard !resolved else { return }
|
||||||
|
resolved = true
|
||||||
|
if let observer { NotificationCenter.default.removeObserver(observer) }
|
||||||
|
NSLog("[CmuxConfig] surface not ready after 3s, dropping command (%d chars)", text.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class WorkspaceRemoteDaemonPendingCallRegistry {
|
final class WorkspaceRemoteDaemonPendingCallRegistry {
|
||||||
final class PendingCall {
|
final class PendingCall {
|
||||||
let id: Int
|
let id: Int
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,7 @@ struct cmuxApp: App {
|
||||||
@StateObject private var notificationStore = TerminalNotificationStore.shared
|
@StateObject private var notificationStore = TerminalNotificationStore.shared
|
||||||
@StateObject private var sidebarState = SidebarState()
|
@StateObject private var sidebarState = SidebarState()
|
||||||
@StateObject private var sidebarSelectionState = SidebarSelectionState()
|
@StateObject private var sidebarSelectionState = SidebarSelectionState()
|
||||||
|
@StateObject private var cmuxConfigStore = CmuxConfigStore()
|
||||||
private let primaryWindowId = UUID()
|
private let primaryWindowId = UUID()
|
||||||
@AppStorage(AppearanceSettings.appearanceModeKey) private var appearanceMode = AppearanceSettings.defaultMode.rawValue
|
@AppStorage(AppearanceSettings.appearanceModeKey) private var appearanceMode = AppearanceSettings.defaultMode.rawValue
|
||||||
@AppStorage("titlebarControlsStyle") private var titlebarControlsStyle = TitlebarControlsStyle.classic.rawValue
|
@AppStorage("titlebarControlsStyle") private var titlebarControlsStyle = TitlebarControlsStyle.classic.rawValue
|
||||||
|
|
@ -337,6 +338,7 @@ struct cmuxApp: App {
|
||||||
.environmentObject(notificationStore)
|
.environmentObject(notificationStore)
|
||||||
.environmentObject(sidebarState)
|
.environmentObject(sidebarState)
|
||||||
.environmentObject(sidebarSelectionState)
|
.environmentObject(sidebarSelectionState)
|
||||||
|
.environmentObject(cmuxConfigStore)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if ProcessInfo.processInfo.environment["CMUX_UI_TEST_MODE"] == "1" {
|
if ProcessInfo.processInfo.environment["CMUX_UI_TEST_MODE"] == "1" {
|
||||||
|
|
@ -346,6 +348,8 @@ struct cmuxApp: App {
|
||||||
// Start the Unix socket controller for programmatic access
|
// Start the Unix socket controller for programmatic access
|
||||||
updateSocketController()
|
updateSocketController()
|
||||||
appDelegate.configure(tabManager: tabManager, notificationStore: notificationStore, sidebarState: sidebarState)
|
appDelegate.configure(tabManager: tabManager, notificationStore: notificationStore, sidebarState: sidebarState)
|
||||||
|
cmuxConfigStore.wireDirectoryTracking(tabManager: tabManager)
|
||||||
|
cmuxConfigStore.loadAll()
|
||||||
applyAppearance()
|
applyAppearance()
|
||||||
if ProcessInfo.processInfo.environment["CMUX_UI_TEST_SHOW_SETTINGS"] == "1" {
|
if ProcessInfo.processInfo.environment["CMUX_UI_TEST_SHOW_SETTINGS"] == "1" {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
@ -3909,6 +3913,7 @@ struct SettingsView: View {
|
||||||
@State private var isResettingSettings = false
|
@State private var isResettingSettings = false
|
||||||
@State private var workspaceTabDefaultEntries = WorkspaceTabColorSettings.defaultPaletteWithOverrides()
|
@State private var workspaceTabDefaultEntries = WorkspaceTabColorSettings.defaultPaletteWithOverrides()
|
||||||
@State private var workspaceTabCustomColors = WorkspaceTabColorSettings.customColors()
|
@State private var workspaceTabCustomColors = WorkspaceTabColorSettings.customColors()
|
||||||
|
@State private var trustedDirectoriesDraft: String = CmuxDirectoryTrust.shared.allTrustedPaths.joined(separator: "\n")
|
||||||
|
|
||||||
private var selectedWorkspacePlacement: NewWorkspacePlacement {
|
private var selectedWorkspacePlacement: NewWorkspacePlacement {
|
||||||
NewWorkspacePlacement(rawValue: newWorkspacePlacement) ?? WorkspacePlacementSettings.defaultPlacement
|
NewWorkspacePlacement(rawValue: newWorkspacePlacement) ?? WorkspacePlacementSettings.defaultPlacement
|
||||||
|
|
@ -4109,6 +4114,14 @@ struct SettingsView: View {
|
||||||
browserInsecureHTTPAllowlistDraft != browserInsecureHTTPAllowlist
|
browserInsecureHTTPAllowlistDraft != browserInsecureHTTPAllowlist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func saveTrustedDirectories() {
|
||||||
|
let paths = trustedDirectoriesDraft
|
||||||
|
.split(separator: "\n", omittingEmptySubsequences: true)
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
CmuxDirectoryTrust.shared.replaceAll(with: paths)
|
||||||
|
}
|
||||||
|
|
||||||
private var hasCustomNotificationSoundFilePath: Bool {
|
private var hasCustomNotificationSoundFilePath: Bool {
|
||||||
!notificationSoundCustomFilePath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
!notificationSoundCustomFilePath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
}
|
}
|
||||||
|
|
@ -5076,6 +5089,38 @@ struct SettingsView: View {
|
||||||
SettingsCardNote(String(localized: "settings.automation.port.note", defaultValue: "Each workspace gets CMUX_PORT and CMUX_PORT_END env vars with a dedicated port range. New terminals inherit these values."))
|
SettingsCardNote(String(localized: "settings.automation.port.note", defaultValue: "Each workspace gets CMUX_PORT and CMUX_PORT_END env vars with a dedicated port range. New terminals inherit these values."))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsSectionHeader(title: String(localized: "settings.section.customCommands", defaultValue: "Custom Commands"))
|
||||||
|
SettingsCard {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
SettingsCardRow(
|
||||||
|
String(localized: "settings.customCommands.trustedDirectories", defaultValue: "Trusted Directories"),
|
||||||
|
subtitle: String(localized: "settings.customCommands.trustedDirectories.subtitle", defaultValue: "Commands from cmux.json in these directories run without confirmation. One path per line.")
|
||||||
|
) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
TextEditor(text: $trustedDirectoriesDraft)
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.frame(minHeight: 60, maxHeight: 120)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.padding(6)
|
||||||
|
.background(Color(nsColor: .controlBackgroundColor))
|
||||||
|
.cornerRadius(6)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.stroke(Color(nsColor: .separatorColor), lineWidth: 0.5)
|
||||||
|
)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.bottom, 12)
|
||||||
|
.onChange(of: trustedDirectoriesDraft) { _ in
|
||||||
|
saveTrustedDirectories()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCardDivider()
|
||||||
|
SettingsCardNote(String(localized: "settings.customCommands.trustedDirectories.note", defaultValue: "Place a cmux.json in your project root to define custom commands. Trust a directory from the confirmation dialog, or add paths here. For git repos, trusting the root covers all subdirectories."))
|
||||||
|
}
|
||||||
|
|
||||||
SettingsSectionHeader(title: String(localized: "settings.section.browser", defaultValue: "Browser"))
|
SettingsSectionHeader(title: String(localized: "settings.section.browser", defaultValue: "Browser"))
|
||||||
.id(SettingsNavigationTarget.browser)
|
.id(SettingsNavigationTarget.browser)
|
||||||
.accessibilityIdentifier("SettingsBrowserSection")
|
.accessibilityIdentifier("SettingsBrowserSection")
|
||||||
|
|
|
||||||
692
cmuxTests/CmuxConfigTests.swift
Normal file
692
cmuxTests/CmuxConfigTests.swift
Normal file
|
|
@ -0,0 +1,692 @@
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
#if canImport(cmux_DEV)
|
||||||
|
@testable import cmux_DEV
|
||||||
|
#elseif canImport(cmux)
|
||||||
|
@testable import cmux
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// MARK: - JSON Decoding
|
||||||
|
|
||||||
|
final class CmuxConfigDecodingTests: XCTestCase {
|
||||||
|
|
||||||
|
private func decode(_ json: String) throws -> CmuxConfigFile {
|
||||||
|
let data = json.data(using: .utf8)!
|
||||||
|
return try JSONDecoder().decode(CmuxConfigFile.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Simple commands
|
||||||
|
|
||||||
|
func testDecodeSimpleCommand() throws {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "Run tests",
|
||||||
|
"command": "npm test"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let config = try decode(json)
|
||||||
|
XCTAssertEqual(config.commands.count, 1)
|
||||||
|
XCTAssertEqual(config.commands[0].name, "Run tests")
|
||||||
|
XCTAssertEqual(config.commands[0].command, "npm test")
|
||||||
|
XCTAssertNil(config.commands[0].workspace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeSimpleCommandWithAllFields() throws {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "Deploy",
|
||||||
|
"description": "Deploy to production",
|
||||||
|
"keywords": ["ship", "release"],
|
||||||
|
"command": "make deploy",
|
||||||
|
"confirm": true
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let config = try decode(json)
|
||||||
|
let cmd = config.commands[0]
|
||||||
|
XCTAssertEqual(cmd.name, "Deploy")
|
||||||
|
XCTAssertEqual(cmd.description, "Deploy to production")
|
||||||
|
XCTAssertEqual(cmd.keywords, ["ship", "release"])
|
||||||
|
XCTAssertEqual(cmd.command, "make deploy")
|
||||||
|
XCTAssertEqual(cmd.confirm, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeMultipleCommands() throws {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [
|
||||||
|
{ "name": "Build", "command": "make build" },
|
||||||
|
{ "name": "Test", "command": "make test" },
|
||||||
|
{ "name": "Lint", "command": "make lint" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let config = try decode(json)
|
||||||
|
XCTAssertEqual(config.commands.count, 3)
|
||||||
|
XCTAssertEqual(config.commands.map(\.name), ["Build", "Test", "Lint"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeEmptyCommandsArray() throws {
|
||||||
|
let json = """
|
||||||
|
{ "commands": [] }
|
||||||
|
"""
|
||||||
|
let config = try decode(json)
|
||||||
|
XCTAssertTrue(config.commands.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Workspace commands
|
||||||
|
|
||||||
|
func testDecodeWorkspaceCommand() throws {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "Dev env",
|
||||||
|
"workspace": {
|
||||||
|
"name": "Development",
|
||||||
|
"cwd": "~/projects/app",
|
||||||
|
"color": "#FF5733"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let config = try decode(json)
|
||||||
|
let ws = config.commands[0].workspace
|
||||||
|
XCTAssertNotNil(ws)
|
||||||
|
XCTAssertEqual(ws?.name, "Development")
|
||||||
|
XCTAssertEqual(ws?.cwd, "~/projects/app")
|
||||||
|
XCTAssertEqual(ws?.color, "#FF5733")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeRestartBehaviors() throws {
|
||||||
|
for behavior in ["recreate", "ignore", "confirm"] {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "test",
|
||||||
|
"restart": "\(behavior)",
|
||||||
|
"workspace": { "name": "ws" }
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let config = try decode(json)
|
||||||
|
XCTAssertEqual(config.commands[0].restart?.rawValue, behavior)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Layout tree
|
||||||
|
|
||||||
|
func testDecodePaneNode() throws {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "layout",
|
||||||
|
"workspace": {
|
||||||
|
"layout": {
|
||||||
|
"pane": {
|
||||||
|
"surfaces": [
|
||||||
|
{ "type": "terminal", "name": "shell" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let config = try decode(json)
|
||||||
|
let layout = config.commands[0].workspace!.layout!
|
||||||
|
if case .pane(let pane) = layout {
|
||||||
|
XCTAssertEqual(pane.surfaces.count, 1)
|
||||||
|
XCTAssertEqual(pane.surfaces[0].type, .terminal)
|
||||||
|
XCTAssertEqual(pane.surfaces[0].name, "shell")
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected pane node")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeSplitNode() throws {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "layout",
|
||||||
|
"workspace": {
|
||||||
|
"layout": {
|
||||||
|
"direction": "horizontal",
|
||||||
|
"split": 0.3,
|
||||||
|
"children": [
|
||||||
|
{ "pane": { "surfaces": [{ "type": "terminal" }] } },
|
||||||
|
{ "pane": { "surfaces": [{ "type": "terminal" }] } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let config = try decode(json)
|
||||||
|
let layout = config.commands[0].workspace!.layout!
|
||||||
|
if case .split(let split) = layout {
|
||||||
|
XCTAssertEqual(split.direction, .horizontal)
|
||||||
|
XCTAssertEqual(split.split, 0.3)
|
||||||
|
XCTAssertEqual(split.children.count, 2)
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected split node")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeNestedSplits() throws {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "nested",
|
||||||
|
"workspace": {
|
||||||
|
"layout": {
|
||||||
|
"direction": "horizontal",
|
||||||
|
"children": [
|
||||||
|
{ "pane": { "surfaces": [{ "type": "terminal" }] } },
|
||||||
|
{
|
||||||
|
"direction": "vertical",
|
||||||
|
"children": [
|
||||||
|
{ "pane": { "surfaces": [{ "type": "terminal" }] } },
|
||||||
|
{ "pane": { "surfaces": [{ "type": "browser", "url": "http://localhost:3000" }] } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let config = try decode(json)
|
||||||
|
let layout = config.commands[0].workspace!.layout!
|
||||||
|
if case .split(let outer) = layout {
|
||||||
|
XCTAssertEqual(outer.direction, .horizontal)
|
||||||
|
if case .split(let inner) = outer.children[1] {
|
||||||
|
XCTAssertEqual(inner.direction, .vertical)
|
||||||
|
if case .pane(let browserPane) = inner.children[1] {
|
||||||
|
XCTAssertEqual(browserPane.surfaces[0].type, .browser)
|
||||||
|
XCTAssertEqual(browserPane.surfaces[0].url, "http://localhost:3000")
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected pane node for inner second child")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected split node for outer second child")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected split node")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Surface definitions
|
||||||
|
|
||||||
|
func testDecodeTerminalSurfaceAllFields() throws {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "test",
|
||||||
|
"workspace": {
|
||||||
|
"layout": {
|
||||||
|
"pane": {
|
||||||
|
"surfaces": [{
|
||||||
|
"type": "terminal",
|
||||||
|
"name": "server",
|
||||||
|
"command": "npm start",
|
||||||
|
"cwd": "./backend",
|
||||||
|
"env": { "NODE_ENV": "development", "PORT": "3000" },
|
||||||
|
"focus": true
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let config = try decode(json)
|
||||||
|
let surface = config.commands[0].workspace!.layout!
|
||||||
|
if case .pane(let pane) = surface {
|
||||||
|
let s = pane.surfaces[0]
|
||||||
|
XCTAssertEqual(s.type, .terminal)
|
||||||
|
XCTAssertEqual(s.name, "server")
|
||||||
|
XCTAssertEqual(s.command, "npm start")
|
||||||
|
XCTAssertEqual(s.cwd, "./backend")
|
||||||
|
XCTAssertEqual(s.env, ["NODE_ENV": "development", "PORT": "3000"])
|
||||||
|
XCTAssertEqual(s.focus, true)
|
||||||
|
XCTAssertNil(s.url)
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected pane node")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeBrowserSurface() throws {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "test",
|
||||||
|
"workspace": {
|
||||||
|
"layout": {
|
||||||
|
"pane": {
|
||||||
|
"surfaces": [{
|
||||||
|
"type": "browser",
|
||||||
|
"name": "Preview",
|
||||||
|
"url": "http://localhost:8080"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let config = try decode(json)
|
||||||
|
if case .pane(let pane) = config.commands[0].workspace!.layout! {
|
||||||
|
let s = pane.surfaces[0]
|
||||||
|
XCTAssertEqual(s.type, .browser)
|
||||||
|
XCTAssertEqual(s.url, "http://localhost:8080")
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected pane node")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeMultipleSurfacesInPane() throws {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "test",
|
||||||
|
"workspace": {
|
||||||
|
"layout": {
|
||||||
|
"pane": {
|
||||||
|
"surfaces": [
|
||||||
|
{ "type": "terminal", "name": "shell1" },
|
||||||
|
{ "type": "terminal", "name": "shell2" },
|
||||||
|
{ "type": "browser", "name": "web" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
let config = try decode(json)
|
||||||
|
if case .pane(let pane) = config.commands[0].workspace!.layout! {
|
||||||
|
XCTAssertEqual(pane.surfaces.count, 3)
|
||||||
|
XCTAssertEqual(pane.surfaces.map(\.name), ["shell1", "shell2", "web"])
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected pane node")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Decoding errors
|
||||||
|
|
||||||
|
func testDecodeInvalidLayoutNodeThrows() {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "bad",
|
||||||
|
"workspace": {
|
||||||
|
"layout": { "invalid": true }
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
XCTAssertThrowsError(try decode(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeMissingCommandsKeyThrows() {
|
||||||
|
let json = """
|
||||||
|
{ "notCommands": [] }
|
||||||
|
"""
|
||||||
|
XCTAssertThrowsError(try decode(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeInvalidSurfaceTypeThrows() {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "test",
|
||||||
|
"workspace": {
|
||||||
|
"layout": {
|
||||||
|
"pane": {
|
||||||
|
"surfaces": [{ "type": "invalidType" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
XCTAssertThrowsError(try decode(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Command validation
|
||||||
|
|
||||||
|
func testDecodeCommandWithNeitherWorkspaceNorCommandThrows() {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "empty"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
XCTAssertThrowsError(try decode(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeCommandWithBothWorkspaceAndCommandThrows() {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "hybrid",
|
||||||
|
"command": "echo hi",
|
||||||
|
"workspace": { "name": "ws" }
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
XCTAssertThrowsError(try decode(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Layout validation
|
||||||
|
|
||||||
|
func testDecodeLayoutNodeWithBothPaneAndDirectionThrows() {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "ambiguous",
|
||||||
|
"workspace": {
|
||||||
|
"layout": {
|
||||||
|
"pane": { "surfaces": [{ "type": "terminal" }] },
|
||||||
|
"direction": "horizontal",
|
||||||
|
"children": [
|
||||||
|
{ "pane": { "surfaces": [{ "type": "terminal" }] } },
|
||||||
|
{ "pane": { "surfaces": [{ "type": "terminal" }] } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
XCTAssertThrowsError(try decode(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeSplitWithWrongChildrenCountThrows() {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "bad-split",
|
||||||
|
"workspace": {
|
||||||
|
"layout": {
|
||||||
|
"direction": "horizontal",
|
||||||
|
"children": [
|
||||||
|
{ "pane": { "surfaces": [{ "type": "terminal" }] } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
XCTAssertThrowsError(try decode(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeSplitWithThreeChildrenThrows() {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "bad-split",
|
||||||
|
"workspace": {
|
||||||
|
"layout": {
|
||||||
|
"direction": "vertical",
|
||||||
|
"children": [
|
||||||
|
{ "pane": { "surfaces": [{ "type": "terminal" }] } },
|
||||||
|
{ "pane": { "surfaces": [{ "type": "terminal" }] } },
|
||||||
|
{ "pane": { "surfaces": [{ "type": "terminal" }] } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
XCTAssertThrowsError(try decode(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodePaneWithEmptySurfacesThrows() {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "empty-pane",
|
||||||
|
"workspace": {
|
||||||
|
"layout": {
|
||||||
|
"pane": { "surfaces": [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
XCTAssertThrowsError(try decode(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeBlankNameThrows() {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "",
|
||||||
|
"command": "echo hi"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
XCTAssertThrowsError(try decode(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeWhitespaceOnlyNameThrows() {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": " ",
|
||||||
|
"command": "echo hi"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
XCTAssertThrowsError(try decode(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeBlankCommandThrows() {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "test",
|
||||||
|
"command": ""
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
XCTAssertThrowsError(try decode(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeWhitespaceOnlyCommandThrows() {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"commands": [{
|
||||||
|
"name": "test",
|
||||||
|
"command": " "
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
XCTAssertThrowsError(try decode(json))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Command identity
|
||||||
|
|
||||||
|
final class CmuxCommandIdentityTests: XCTestCase {
|
||||||
|
|
||||||
|
func testCommandIdIsDeterministic() {
|
||||||
|
let cmd = CmuxCommandDefinition(name: "Run tests", command: "test")
|
||||||
|
XCTAssertEqual(cmd.id, "cmux.config.command.Run%20tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCommandIdEncodesSpecialCharacters() {
|
||||||
|
let cmd = CmuxCommandDefinition(name: "build & deploy", command: "make")
|
||||||
|
XCTAssertTrue(cmd.id.hasPrefix("cmux.config.command."))
|
||||||
|
XCTAssertFalse(cmd.id.contains("&"))
|
||||||
|
XCTAssertFalse(cmd.id.contains(" "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCommandIdIsUniqueForDifferentNames() {
|
||||||
|
let cmd1 = CmuxCommandDefinition(name: "build", command: "make build")
|
||||||
|
let cmd2 = CmuxCommandDefinition(name: "test", command: "make test")
|
||||||
|
XCTAssertNotEqual(cmd1.id, cmd2.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCommandIdDoesNotCollideWithBuiltinPrefix() {
|
||||||
|
let cmd = CmuxCommandDefinition(name: "palette.newWorkspace", command: "echo")
|
||||||
|
XCTAssertTrue(cmd.id.hasPrefix("cmux.config.command."))
|
||||||
|
XCTAssertNotEqual(cmd.id, "palette.newWorkspace")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Split clamping
|
||||||
|
|
||||||
|
final class CmuxSplitDefinitionTests: XCTestCase {
|
||||||
|
|
||||||
|
func testClampedSplitPositionDefaultsToHalf() {
|
||||||
|
let split = CmuxSplitDefinition(direction: .horizontal, split: nil, children: [])
|
||||||
|
XCTAssertEqual(split.clampedSplitPosition, 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testClampedSplitPositionPassesThroughValidValue() {
|
||||||
|
let split = CmuxSplitDefinition(direction: .vertical, split: 0.3, children: [])
|
||||||
|
XCTAssertEqual(split.clampedSplitPosition, 0.3, accuracy: 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testClampedSplitPositionClampsLow() {
|
||||||
|
let split = CmuxSplitDefinition(direction: .horizontal, split: 0.01, children: [])
|
||||||
|
XCTAssertEqual(split.clampedSplitPosition, 0.1, accuracy: 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testClampedSplitPositionClampsHigh() {
|
||||||
|
let split = CmuxSplitDefinition(direction: .horizontal, split: 0.99, children: [])
|
||||||
|
XCTAssertEqual(split.clampedSplitPosition, 0.9, accuracy: 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testClampedSplitPositionClampsNegative() {
|
||||||
|
let split = CmuxSplitDefinition(direction: .horizontal, split: -1.0, children: [])
|
||||||
|
XCTAssertEqual(split.clampedSplitPosition, 0.1, accuracy: 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testClampedSplitPositionClampsAboveOne() {
|
||||||
|
let split = CmuxSplitDefinition(direction: .horizontal, split: 2.0, children: [])
|
||||||
|
XCTAssertEqual(split.clampedSplitPosition, 0.9, accuracy: 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSplitOrientationHorizontal() {
|
||||||
|
let split = CmuxSplitDefinition(direction: .horizontal, split: nil, children: [])
|
||||||
|
XCTAssertEqual(split.splitOrientation, .horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSplitOrientationVertical() {
|
||||||
|
let split = CmuxSplitDefinition(direction: .vertical, split: nil, children: [])
|
||||||
|
XCTAssertEqual(split.splitOrientation, .vertical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - CWD resolution
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class CmuxConfigCwdResolutionTests: XCTestCase {
|
||||||
|
|
||||||
|
private let baseCwd = "/Users/test/project"
|
||||||
|
|
||||||
|
func testNilCwdReturnsBase() {
|
||||||
|
XCTAssertEqual(
|
||||||
|
CmuxConfigStore.resolveCwd(nil, relativeTo: baseCwd),
|
||||||
|
baseCwd
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEmptyCwdReturnsBase() {
|
||||||
|
XCTAssertEqual(
|
||||||
|
CmuxConfigStore.resolveCwd("", relativeTo: baseCwd),
|
||||||
|
baseCwd
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDotCwdReturnsBase() {
|
||||||
|
XCTAssertEqual(
|
||||||
|
CmuxConfigStore.resolveCwd(".", relativeTo: baseCwd),
|
||||||
|
baseCwd
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAbsolutePathReturnedAsIs() {
|
||||||
|
XCTAssertEqual(
|
||||||
|
CmuxConfigStore.resolveCwd("/tmp/other", relativeTo: baseCwd),
|
||||||
|
"/tmp/other"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRelativePathJoinedToBase() {
|
||||||
|
XCTAssertEqual(
|
||||||
|
CmuxConfigStore.resolveCwd("backend/src", relativeTo: baseCwd),
|
||||||
|
"/Users/test/project/backend/src"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTildeExpandsToHome() {
|
||||||
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||||
|
XCTAssertEqual(
|
||||||
|
CmuxConfigStore.resolveCwd("~", relativeTo: baseCwd),
|
||||||
|
home
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTildeSlashExpandsToHomePlusPath() {
|
||||||
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||||
|
XCTAssertEqual(
|
||||||
|
CmuxConfigStore.resolveCwd("~/Documents/work", relativeTo: baseCwd),
|
||||||
|
(home as NSString).appendingPathComponent("Documents/work")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSingleSubdirectory() {
|
||||||
|
XCTAssertEqual(
|
||||||
|
CmuxConfigStore.resolveCwd("src", relativeTo: baseCwd),
|
||||||
|
"/Users/test/project/src"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Layout encoding round-trip
|
||||||
|
|
||||||
|
final class CmuxLayoutEncodingTests: XCTestCase {
|
||||||
|
|
||||||
|
func testPaneNodeRoundTrips() throws {
|
||||||
|
let original = CmuxLayoutNode.pane(CmuxPaneDefinition(surfaces: [
|
||||||
|
CmuxSurfaceDefinition(type: .terminal, name: "shell")
|
||||||
|
]))
|
||||||
|
let data = try JSONEncoder().encode(original)
|
||||||
|
let decoded = try JSONDecoder().decode(CmuxLayoutNode.self, from: data)
|
||||||
|
|
||||||
|
if case .pane(let pane) = decoded {
|
||||||
|
XCTAssertEqual(pane.surfaces.count, 1)
|
||||||
|
XCTAssertEqual(pane.surfaces[0].name, "shell")
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected pane node after round-trip")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSplitNodeRoundTrips() throws {
|
||||||
|
let original = CmuxLayoutNode.split(CmuxSplitDefinition(
|
||||||
|
direction: .vertical,
|
||||||
|
split: 0.7,
|
||||||
|
children: [
|
||||||
|
.pane(CmuxPaneDefinition(surfaces: [CmuxSurfaceDefinition(type: .terminal)])),
|
||||||
|
.pane(CmuxPaneDefinition(surfaces: [CmuxSurfaceDefinition(type: .browser, url: "http://localhost")]))
|
||||||
|
]
|
||||||
|
))
|
||||||
|
let data = try JSONEncoder().encode(original)
|
||||||
|
let decoded = try JSONDecoder().decode(CmuxLayoutNode.self, from: data)
|
||||||
|
|
||||||
|
if case .split(let split) = decoded {
|
||||||
|
XCTAssertEqual(split.direction, .vertical)
|
||||||
|
XCTAssertEqual(split.split, 0.7)
|
||||||
|
XCTAssertEqual(split.children.count, 2)
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected split node after round-trip")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ export const navItems = [
|
||||||
{ titleKey: "gettingStarted" as const, href: "/docs/getting-started" },
|
{ titleKey: "gettingStarted" as const, href: "/docs/getting-started" },
|
||||||
{ titleKey: "concepts" as const, href: "/docs/concepts" },
|
{ titleKey: "concepts" as const, href: "/docs/concepts" },
|
||||||
{ titleKey: "configuration" as const, href: "/docs/configuration" },
|
{ titleKey: "configuration" as const, href: "/docs/configuration" },
|
||||||
|
{ titleKey: "customCommands" as const, href: "/docs/custom-commands" },
|
||||||
{ titleKey: "keyboardShortcuts" as const, href: "/docs/keyboard-shortcuts" },
|
{ titleKey: "keyboardShortcuts" as const, href: "/docs/keyboard-shortcuts" },
|
||||||
{ titleKey: "apiReference" as const, href: "/docs/api" },
|
{ titleKey: "apiReference" as const, href: "/docs/api" },
|
||||||
{ titleKey: "browserAutomation" as const, href: "/docs/browser-automation" },
|
{ titleKey: "browserAutomation" as const, href: "/docs/browser-automation" },
|
||||||
|
|
|
||||||
293
web/app/[locale]/docs/custom-commands/page.tsx
Normal file
293
web/app/[locale]/docs/custom-commands/page.tsx
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import { CodeBlock } from "../../components/code-block";
|
||||||
|
import { Callout } from "../../components/callout";
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||||
|
const { locale } = await params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "docs.customCommands" });
|
||||||
|
return {
|
||||||
|
title: t("metaTitle"),
|
||||||
|
description: t("metaDescription"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CustomCommandsPage() {
|
||||||
|
const t = useTranslations("docs.customCommands");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>{t("title")}</h1>
|
||||||
|
<p>{t("intro")}</p>
|
||||||
|
|
||||||
|
<h2>{t("fileLocations")}</h2>
|
||||||
|
<p>{t("fileLocationsDesc")}</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>{t("localConfig")}</strong> <code>./cmux.json</code> — {t("localConfigDesc")}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>{t("globalConfig")}</strong> <code>~/.config/cmux/cmux.json</code> — {t("globalConfigDesc")}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Callout type="info">{t("precedenceNote")}</Callout>
|
||||||
|
<p>{t("liveReload")}</p>
|
||||||
|
|
||||||
|
<h2>{t("schema")}</h2>
|
||||||
|
<p>{t("schemaDesc")}</p>
|
||||||
|
<CodeBlock title="cmux.json" lang="json">{`{
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "Start Dev",
|
||||||
|
"keywords": ["dev", "start"],
|
||||||
|
"workspace": { ... }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Run Tests",
|
||||||
|
"command": "npm test",
|
||||||
|
"confirm": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`}</CodeBlock>
|
||||||
|
|
||||||
|
<h2>{t("simpleCommands")}</h2>
|
||||||
|
<p>{t("simpleCommandsDesc")}</p>
|
||||||
|
<CodeBlock title="cmux.json" lang="json">{`{
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "Run Tests",
|
||||||
|
"keywords": ["test", "check"],
|
||||||
|
"command": "npm test",
|
||||||
|
"confirm": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`}</CodeBlock>
|
||||||
|
|
||||||
|
<h3>{t("simpleCommandFields")}</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code>name</code> — {t("fieldName")}</li>
|
||||||
|
<li><code>description</code> — {t("fieldDescription")}</li>
|
||||||
|
<li><code>keywords</code> — {t("fieldKeywords")}</li>
|
||||||
|
<li><code>command</code> — {t("fieldCommand")}</li>
|
||||||
|
<li><code>confirm</code> — {t("fieldConfirm")}</li>
|
||||||
|
</ul>
|
||||||
|
<p>{t("simpleCommandCwdNote")} <code>{"cd \"$(git rev-parse --show-toplevel)\" &&"}</code> {t("simpleCommandCwdRepoRoot")} <code>{"cd /your/path &&"}</code> {t("simpleCommandCwdCustomPath")}</p>
|
||||||
|
|
||||||
|
<h2>{t("workspaceCommands")}</h2>
|
||||||
|
<p>{t("workspaceCommandsDesc")}</p>
|
||||||
|
<CodeBlock title="cmux.json" lang="json">{`{
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "Dev Environment",
|
||||||
|
"keywords": ["dev", "fullstack"],
|
||||||
|
"restart": "confirm",
|
||||||
|
"workspace": {
|
||||||
|
"name": "Dev",
|
||||||
|
"cwd": ".",
|
||||||
|
"layout": {
|
||||||
|
"direction": "horizontal",
|
||||||
|
"split": 0.5,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"pane": {
|
||||||
|
"surfaces": [
|
||||||
|
{
|
||||||
|
"type": "terminal",
|
||||||
|
"name": "Frontend",
|
||||||
|
"command": "npm run dev",
|
||||||
|
"focus": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pane": {
|
||||||
|
"surfaces": [
|
||||||
|
{
|
||||||
|
"type": "terminal",
|
||||||
|
"name": "Backend",
|
||||||
|
"command": "cargo watch -x run",
|
||||||
|
"cwd": "./server",
|
||||||
|
"env": { "RUST_LOG": "debug" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`}</CodeBlock>
|
||||||
|
|
||||||
|
<h3>{t("workspaceFields")}</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code>name</code> — {t("wsFieldName")}</li>
|
||||||
|
<li><code>cwd</code> — {t("wsFieldCwd")}</li>
|
||||||
|
<li><code>color</code> — {t("wsFieldColor")}</li>
|
||||||
|
<li><code>layout</code> — {t("wsFieldLayout")}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>{t("restartBehavior")}</h3>
|
||||||
|
<p>{t("restartBehaviorDesc")}</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>"ignore"</code> — {t("restartIgnore")}</li>
|
||||||
|
<li><code>"recreate"</code> — {t("restartRecreate")}</li>
|
||||||
|
<li><code>"confirm"</code> — {t("restartConfirm")}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>{t("layoutTree")}</h2>
|
||||||
|
<p>{t("layoutTreeDesc")}</p>
|
||||||
|
|
||||||
|
<h3>{t("splitNode")}</h3>
|
||||||
|
<p>{t("splitNodeDesc")}</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>direction</code> — <code>"horizontal"</code> {t("or")} <code>"vertical"</code></li>
|
||||||
|
<li><code>split</code> — {t("splitPosition")}</li>
|
||||||
|
<li><code>children</code> — {t("splitChildren")}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>{t("paneNode")}</h3>
|
||||||
|
<p>{t("paneNodeDesc")}</p>
|
||||||
|
|
||||||
|
<h2>{t("surfaceDefinition")}</h2>
|
||||||
|
<p>{t("surfaceDefinitionDesc")}</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>type</code> — <code>"terminal"</code> {t("or")} <code>"browser"</code></li>
|
||||||
|
<li><code>name</code> — {t("surfaceName")}</li>
|
||||||
|
<li><code>command</code> — {t("surfaceCommand")}</li>
|
||||||
|
<li><code>cwd</code> — {t("surfaceCwd")}</li>
|
||||||
|
<li><code>env</code> — {t("surfaceEnv")}</li>
|
||||||
|
<li><code>url</code> — {t("surfaceUrl")}</li>
|
||||||
|
<li><code>focus</code> — {t("surfaceFocus")}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>{t("cwdResolution")}</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code>.</code> {t("or")} {t("omitted")} — {t("cwdRelative")}</li>
|
||||||
|
<li><code>./subdir</code> — {t("cwdSubdir")}</li>
|
||||||
|
<li><code>~/path</code> — {t("cwdHome")}</li>
|
||||||
|
<li>{t("absolutePath")} — {t("cwdAbsolute")}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>{t("fullExample")}</h2>
|
||||||
|
<CodeBlock title="cmux.json" lang="json">{`{
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "Web Dev",
|
||||||
|
"description": "Docs site with live preview",
|
||||||
|
"keywords": ["web", "docs", "next", "frontend"],
|
||||||
|
"restart": "confirm",
|
||||||
|
"workspace": {
|
||||||
|
"name": "Web Dev",
|
||||||
|
"cwd": "./web",
|
||||||
|
"color": "#3b82f6",
|
||||||
|
"layout": {
|
||||||
|
"direction": "horizontal",
|
||||||
|
"split": 0.5,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"pane": {
|
||||||
|
"surfaces": [
|
||||||
|
{
|
||||||
|
"type": "terminal",
|
||||||
|
"name": "Next.js",
|
||||||
|
"command": "npm run dev",
|
||||||
|
"focus": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"direction": "vertical",
|
||||||
|
"split": 0.6,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"pane": {
|
||||||
|
"surfaces": [
|
||||||
|
{
|
||||||
|
"type": "browser",
|
||||||
|
"name": "Preview",
|
||||||
|
"url": "http://localhost:3777"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pane": {
|
||||||
|
"surfaces": [
|
||||||
|
{
|
||||||
|
"type": "terminal",
|
||||||
|
"name": "Shell",
|
||||||
|
"env": { "NODE_ENV": "development" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug Log",
|
||||||
|
"description": "Tail the debug event log from the running dev app",
|
||||||
|
"keywords": ["log", "debug", "tail", "events"],
|
||||||
|
"restart": "ignore",
|
||||||
|
"workspace": {
|
||||||
|
"name": "Debug Log",
|
||||||
|
"layout": {
|
||||||
|
"direction": "horizontal",
|
||||||
|
"split": 0.5,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"pane": {
|
||||||
|
"surfaces": [
|
||||||
|
{
|
||||||
|
"type": "terminal",
|
||||||
|
"name": "Events",
|
||||||
|
"command": "tail -f /tmp/cmux-debug.log",
|
||||||
|
"focus": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pane": {
|
||||||
|
"surfaces": [
|
||||||
|
{
|
||||||
|
"type": "terminal",
|
||||||
|
"name": "Shell"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Setup",
|
||||||
|
"description": "Initialize submodules and build dependencies",
|
||||||
|
"keywords": ["setup", "init", "install"],
|
||||||
|
"command": "./scripts/setup.sh",
|
||||||
|
"confirm": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Reload",
|
||||||
|
"description": "Build and launch the debug app tagged to the current branch",
|
||||||
|
"keywords": ["reload", "build", "run", "launch"],
|
||||||
|
"command": "./scripts/reload.sh --tag $(git branch --show-current)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Run Unit Tests",
|
||||||
|
"keywords": ["test", "unit"],
|
||||||
|
"command": "./scripts/test-unit.sh",
|
||||||
|
"confirm": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`}</CodeBlock>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "مثال على الإعدادات",
|
"exampleConfig": "مثال على الإعدادات",
|
||||||
"metaTitle": "الإعدادات"
|
"metaTitle": "الإعدادات"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "أوامر مخصصة",
|
||||||
|
"metaTitle": "أوامر مخصصة",
|
||||||
|
"metaDescription": "تعريف الأوامر المخصصة وتخطيطات مساحة العمل في cmux.json. تكوين لكل مشروع وعالمي مع مراقبة الملفات المباشرة.",
|
||||||
|
"intro": "عرّف الأوامر المخصصة وتخطيطات مساحة العمل بإضافة ملف cmux.json إلى جذر مشروعك أو ~/.config/cmux/. تظهر الأوامر في لوحة الأوامر.",
|
||||||
|
"fileLocations": "مواقع الملفات",
|
||||||
|
"fileLocationsDesc": "يبحث cmux عن التكوين في مكانين:",
|
||||||
|
"localConfig": "لكل مشروع:",
|
||||||
|
"localConfigDesc": "يقع في دليل مشروعك، له الأولوية",
|
||||||
|
"globalConfig": "عالمي:",
|
||||||
|
"globalConfigDesc": "ينطبق على جميع المشاريع، يملأ الأوامر غير المعرفة محلياً",
|
||||||
|
"precedenceNote": "الأوامر المحلية تتجاوز الأوامر العالمية بنفس الاسم.",
|
||||||
|
"liveReload": "يتم اكتشاف التغييرات تلقائياً — لا حاجة لإعادة التشغيل.",
|
||||||
|
"schema": "المخطط",
|
||||||
|
"schemaDesc": "يحتوي ملف cmux.json على مصفوفة commands. كل أمر إما أمر shell بسيط أو تعريف كامل لمساحة عمل:",
|
||||||
|
"simpleCommands": "أوامر بسيطة",
|
||||||
|
"simpleCommandsDesc": "الأمر البسيط ينفذ أمر shell في الطرفية المحددة حالياً:",
|
||||||
|
"simpleCommandFields": "الحقول",
|
||||||
|
"fieldName": "يظهر في لوحة الأوامر (مطلوب)",
|
||||||
|
"fieldDescription": "وصف اختياري",
|
||||||
|
"fieldKeywords": "مصطلحات بحث إضافية للوحة الأوامر",
|
||||||
|
"fieldCommand": "أمر shell للتشغيل في الطرفية المحددة",
|
||||||
|
"fieldConfirm": "عرض مربع حوار للتأكيد قبل التشغيل",
|
||||||
|
"simpleCommandCwdNote": "تعمل الأوامر البسيطة في دليل العمل الحالي للطرفية المحددة. إذا كان أمرك يعتمد على مسارات نسبية للمشروع، أضف قبله",
|
||||||
|
"simpleCommandCwdRepoRoot": "للتشغيل من جذر المستودع، أو",
|
||||||
|
"simpleCommandCwdCustomPath": "لأي دليل محدد.",
|
||||||
|
"workspaceCommands": "أوامر مساحة العمل",
|
||||||
|
"workspaceCommandsDesc": "يُنشئ أمر مساحة العمل مساحة عمل جديدة بتخطيط مخصص من الانقسامات والطرفيات وألواح المتصفح:",
|
||||||
|
"workspaceFields": "حقول مساحة العمل",
|
||||||
|
"wsFieldName": "اسم علامة التبويب لمساحة العمل (الافتراضي: اسم الأمر)",
|
||||||
|
"wsFieldCwd": "دليل العمل لمساحة العمل",
|
||||||
|
"wsFieldColor": "لون علامة تبويب مساحة العمل",
|
||||||
|
"wsFieldLayout": "شجرة التخطيط التي تحدد الانقسامات والألواح",
|
||||||
|
"restartBehavior": "سلوك إعادة التشغيل",
|
||||||
|
"restartBehaviorDesc": "يتحكم فيما يحدث عندما توجد مساحة عمل بنفس الاسم:",
|
||||||
|
"restartIgnore": "التبديل إلى مساحة العمل الموجودة (الافتراضي)",
|
||||||
|
"restartRecreate": "الإغلاق وإعادة الإنشاء دون سؤال",
|
||||||
|
"restartConfirm": "السؤال قبل إعادة الإنشاء",
|
||||||
|
"layoutTree": "شجرة التخطيط",
|
||||||
|
"layoutTreeDesc": "تحدد شجرة التخطيط كيفية ترتيب الألواح باستخدام عقد الانقسام المتكررة:",
|
||||||
|
"splitNode": "عقدة الانقسام",
|
||||||
|
"splitNodeDesc": "تقسم المساحة إلى فرعين:",
|
||||||
|
"or": "أو",
|
||||||
|
"splitPosition": "موضع الفاصل من 0.1 إلى 0.9 (الافتراضي 0.5)",
|
||||||
|
"splitChildren": "فرعان بالضبط (انقسام أو لوح)",
|
||||||
|
"paneNode": "عقدة اللوح",
|
||||||
|
"paneNodeDesc": "عقدة طرفية تحتوي على واحد أو أكثر من الأسطح (علامات التبويب داخل اللوح).",
|
||||||
|
"surfaceDefinition": "تعريف السطح",
|
||||||
|
"surfaceDefinitionDesc": "كل سطح في لوح يمكن أن يكون طرفية أو متصفحاً:",
|
||||||
|
"surfaceName": "عنوان علامة تبويب مخصص",
|
||||||
|
"surfaceCommand": "أمر shell للتشغيل التلقائي عند الإنشاء (للطرفية فقط)",
|
||||||
|
"surfaceCwd": "دليل العمل لهذا السطح",
|
||||||
|
"surfaceEnv": "متغيرات البيئة كأزواج مفتاح-قيمة",
|
||||||
|
"surfaceUrl": "رابط للفتح (للمتصفح فقط)",
|
||||||
|
"surfaceFocus": "التركيز على هذا السطح بعد الإنشاء",
|
||||||
|
"cwdResolution": "تحليل دليل العمل",
|
||||||
|
"omitted": "محذوف",
|
||||||
|
"cwdRelative": "دليل عمل مساحة العمل",
|
||||||
|
"cwdSubdir": "نسبي إلى دليل عمل مساحة العمل",
|
||||||
|
"cwdHome": "موسّع إلى الدليل الرئيسي",
|
||||||
|
"absolutePath": "مسار مطلق",
|
||||||
|
"cwdAbsolute": "يُستخدم كما هو",
|
||||||
|
"fullExample": "مثال كامل"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "اختصارات لوحة المفاتيح",
|
"title": "اختصارات لوحة المفاتيح",
|
||||||
"description": "جميع اختصارات لوحة المفاتيح المتاحة في cmux، مجمعة حسب الفئة.",
|
"description": "جميع اختصارات لوحة المفاتيح المتاحة في cmux، مجمعة حسب الفئة.",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "البدء",
|
"gettingStarted": "البدء",
|
||||||
"concepts": "المفاهيم",
|
"concepts": "المفاهيم",
|
||||||
"configuration": "الإعدادات",
|
"configuration": "الإعدادات",
|
||||||
|
"customCommands": "أوامر مخصصة",
|
||||||
"keyboardShortcuts": "اختصارات لوحة المفاتيح",
|
"keyboardShortcuts": "اختصارات لوحة المفاتيح",
|
||||||
"apiReference": "مرجع الواجهة البرمجية",
|
"apiReference": "مرجع الواجهة البرمجية",
|
||||||
"browserAutomation": "أتمتة المتصفح",
|
"browserAutomation": "أتمتة المتصفح",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "Primjer konfiguracije",
|
"exampleConfig": "Primjer konfiguracije",
|
||||||
"metaTitle": "Konfiguracija"
|
"metaTitle": "Konfiguracija"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "Prilagođene komande",
|
||||||
|
"metaTitle": "Prilagođene komande",
|
||||||
|
"metaDescription": "Definirajte prilagođene komande i rasporede radnog prostora u cmux.json. Konfiguracija po projektu i globalna konfiguracija s praćenjem promjena datoteka.",
|
||||||
|
"intro": "Definirajte prilagođene komande i rasporede radnog prostora dodavanjem datoteke cmux.json u korijenski direktorij vašeg projekta ili ~/.config/cmux/. Komande se pojavljuju u paleti komandi.",
|
||||||
|
"fileLocations": "Lokacije datoteka",
|
||||||
|
"fileLocationsDesc": "cmux traži konfiguraciju na dva mjesta:",
|
||||||
|
"localConfig": "Po projektu:",
|
||||||
|
"localConfigDesc": "nalazi se u vašem projektnom direktoriju, ima prednost",
|
||||||
|
"globalConfig": "Globalno:",
|
||||||
|
"globalConfigDesc": "primjenjuje se na sve projekte, dopunjava komande koje nisu definirane lokalno",
|
||||||
|
"precedenceNote": "Lokalne komande nadjačavaju globalne komande istog naziva.",
|
||||||
|
"liveReload": "Promjene se automatski preuzimaju — nije potrebno ponovno pokretanje.",
|
||||||
|
"schema": "Shema",
|
||||||
|
"schemaDesc": "Datoteka cmux.json sadrži niz commands. Svaka komanda je ili jednostavna shell komanda ili potpuna definicija radnog prostora:",
|
||||||
|
"simpleCommands": "Jednostavne komande",
|
||||||
|
"simpleCommandsDesc": "Jednostavna komanda pokreće shell komandu u trenutno fokusiranom terminalu:",
|
||||||
|
"simpleCommandFields": "Polja",
|
||||||
|
"fieldName": "Prikazuje se u paleti komandi (obavezno)",
|
||||||
|
"fieldDescription": "Neobavezan opis",
|
||||||
|
"fieldKeywords": "Dodatni pojmovi za pretraživanje u paleti komandi",
|
||||||
|
"fieldCommand": "Shell komanda za pokretanje u fokusiranom terminalu",
|
||||||
|
"fieldConfirm": "Prikaži dijalog za potvrdu prije pokretanja",
|
||||||
|
"simpleCommandCwdNote": "Jednostavne komande se pokreću u trenutnom radnom direktoriju fokusiranog terminala. Ako vaša komanda zavisi od putanja relativnih projektu, dodajte prefiks",
|
||||||
|
"simpleCommandCwdRepoRoot": "za pokretanje iz korijena repozitorija, ili",
|
||||||
|
"simpleCommandCwdCustomPath": "za bilo koji specifični direktorij.",
|
||||||
|
"workspaceCommands": "Komande radnog prostora",
|
||||||
|
"workspaceCommandsDesc": "Komanda radnog prostora kreira novi radni prostor s prilagođenim rasporedom podjela, terminala i panela preglednika:",
|
||||||
|
"workspaceFields": "Polja radnog prostora",
|
||||||
|
"wsFieldName": "Naziv kartice radnog prostora (zadano je naziv komande)",
|
||||||
|
"wsFieldCwd": "Radni direktorij za radni prostor",
|
||||||
|
"wsFieldColor": "Boja kartice radnog prostora",
|
||||||
|
"wsFieldLayout": "Stablo rasporeda koje definira podjele i panele",
|
||||||
|
"restartBehavior": "Ponašanje pri ponovnom pokretanju",
|
||||||
|
"restartBehaviorDesc": "Kontrolira što se dešava kada radni prostor istog naziva već postoji:",
|
||||||
|
"restartIgnore": "Prebaci na postojeći radni prostor (zadano)",
|
||||||
|
"restartRecreate": "Zatvori i ponovo kreiraj bez pitanja",
|
||||||
|
"restartConfirm": "Pitaj korisnika prije ponovnog kreiranja",
|
||||||
|
"layoutTree": "Stablo rasporeda",
|
||||||
|
"layoutTreeDesc": "Stablo rasporeda definira kako su paneli raspoređeni koristeći rekurzivne čvorove podjele:",
|
||||||
|
"splitNode": "Čvor podjele",
|
||||||
|
"splitNodeDesc": "Dijeli prostor na dva djeteta:",
|
||||||
|
"or": "ili",
|
||||||
|
"splitPosition": "Pozicija razdjeljnika od 0.1 do 0.9 (zadano 0.5)",
|
||||||
|
"splitChildren": "Točno dva dječja čvora (podjela ili panel)",
|
||||||
|
"paneNode": "Čvor panela",
|
||||||
|
"paneNodeDesc": "Listni čvor koji sadrži jednu ili više površina (kartice unutar panela).",
|
||||||
|
"surfaceDefinition": "Definicija površine",
|
||||||
|
"surfaceDefinitionDesc": "Svaka površina u panelu može biti terminal ili preglednik:",
|
||||||
|
"surfaceName": "Prilagođeni naslov kartice",
|
||||||
|
"surfaceCommand": "Shell komanda za automatsko pokretanje pri kreiranju (samo terminal)",
|
||||||
|
"surfaceCwd": "Radni direktorij za ovu površinu",
|
||||||
|
"surfaceEnv": "Varijable okruženja kao parovi ključ-vrijednost",
|
||||||
|
"surfaceUrl": "URL za otvaranje (samo preglednik)",
|
||||||
|
"surfaceFocus": "Fokusiraj ovu površinu nakon kreiranja",
|
||||||
|
"cwdResolution": "Razrješavanje radnog direktorija",
|
||||||
|
"omitted": "izostavljeno",
|
||||||
|
"cwdRelative": "radni direktorij radnog prostora",
|
||||||
|
"cwdSubdir": "relativno u odnosu na radni direktorij radnog prostora",
|
||||||
|
"cwdHome": "prošireno na kućni direktorij",
|
||||||
|
"absolutePath": "Apsolutna putanja",
|
||||||
|
"cwdAbsolute": "koristi se kao takvo",
|
||||||
|
"fullExample": "Potpuni primjer"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Prečice na tastaturi",
|
"title": "Prečice na tastaturi",
|
||||||
"description": "Sve prečice na tastaturi dostupne u cmux-u, grupirane po kategorijama.",
|
"description": "Sve prečice na tastaturi dostupne u cmux-u, grupirane po kategorijama.",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "Početak rada",
|
"gettingStarted": "Početak rada",
|
||||||
"concepts": "Koncepti",
|
"concepts": "Koncepti",
|
||||||
"configuration": "Konfiguracija",
|
"configuration": "Konfiguracija",
|
||||||
|
"customCommands": "Prilagođene komande",
|
||||||
"keyboardShortcuts": "Prečice na tastaturi",
|
"keyboardShortcuts": "Prečice na tastaturi",
|
||||||
"apiReference": "API Referenca",
|
"apiReference": "API Referenca",
|
||||||
"browserAutomation": "Automatizacija preglednika",
|
"browserAutomation": "Automatizacija preglednika",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "Eksempelkonfiguration",
|
"exampleConfig": "Eksempelkonfiguration",
|
||||||
"metaTitle": "Konfiguration"
|
"metaTitle": "Konfiguration"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "Brugerdefinerede kommandoer",
|
||||||
|
"metaTitle": "Brugerdefinerede kommandoer",
|
||||||
|
"metaDescription": "Definer brugerdefinerede kommandoer og workspace-layouts i cmux.json. Per-projekt og global konfiguration med live filovervågning.",
|
||||||
|
"intro": "Definer brugerdefinerede kommandoer og workspace-layouts ved at tilføje en cmux.json-fil til din projektrod eller ~/.config/cmux/. Kommandoer vises i kommandopaletten.",
|
||||||
|
"fileLocations": "Filplaceringer",
|
||||||
|
"fileLocationsDesc": "cmux leder efter konfiguration to steder:",
|
||||||
|
"localConfig": "Per projekt:",
|
||||||
|
"localConfigDesc": "befinder sig i din projektmappe, har forrang",
|
||||||
|
"globalConfig": "Global:",
|
||||||
|
"globalConfigDesc": "gælder for alle projekter, udfylder kommandoer, der ikke er defineret lokalt",
|
||||||
|
"precedenceNote": "Lokale kommandoer tilsidesætter globale kommandoer med samme navn.",
|
||||||
|
"liveReload": "Ændringer hentes automatisk — ingen genstart nødvendig.",
|
||||||
|
"schema": "Skema",
|
||||||
|
"schemaDesc": "En cmux.json-fil indeholder et commands-array. Hver kommando er enten en simpel shell-kommando eller en fuld workspace-definition:",
|
||||||
|
"simpleCommands": "Simple kommandoer",
|
||||||
|
"simpleCommandsDesc": "En simpel kommando kører en shell-kommando i den aktuelt fokuserede terminal:",
|
||||||
|
"simpleCommandFields": "Felter",
|
||||||
|
"fieldName": "Vises i kommandopaletten (påkrævet)",
|
||||||
|
"fieldDescription": "Valgfri beskrivelse",
|
||||||
|
"fieldKeywords": "Ekstra søgetermer til kommandopaletten",
|
||||||
|
"fieldCommand": "Shell-kommando der skal køres i den fokuserede terminal",
|
||||||
|
"fieldConfirm": "Vis en bekræftelsesdialog før kørsel",
|
||||||
|
"simpleCommandCwdNote": "Simple kommandoer køres i den fokuserede terminals aktuelle arbejdsmappe. Hvis din kommando afhænger af projektrelative stier, sæt foran med",
|
||||||
|
"simpleCommandCwdRepoRoot": "for at køre fra repo-roden, eller",
|
||||||
|
"simpleCommandCwdCustomPath": "for enhver specifik mappe.",
|
||||||
|
"workspaceCommands": "Workspace-kommandoer",
|
||||||
|
"workspaceCommandsDesc": "En workspace-kommando opretter et nyt workspace med et brugerdefineret layout af opdelte terminaler og browserpaneler:",
|
||||||
|
"workspaceFields": "Workspace-felter",
|
||||||
|
"wsFieldName": "Workspace-fanenavn (standard er kommandoens navn)",
|
||||||
|
"wsFieldCwd": "Arbejdsmappe for workspacet",
|
||||||
|
"wsFieldColor": "Farve på workspace-fanen",
|
||||||
|
"wsFieldLayout": "Layouttræ der definerer opdelinger og paneler",
|
||||||
|
"restartBehavior": "Genstartsadfærd",
|
||||||
|
"restartBehaviorDesc": "Styrer hvad der sker, når der allerede eksisterer et workspace med samme navn:",
|
||||||
|
"restartIgnore": "Skift til det eksisterende workspace (standard)",
|
||||||
|
"restartRecreate": "Luk og genskab uden at spørge",
|
||||||
|
"restartConfirm": "Spørg brugeren inden genskabelse",
|
||||||
|
"layoutTree": "Layouttræ",
|
||||||
|
"layoutTreeDesc": "Layouttræet definerer, hvordan paneler arrangeres ved hjælp af rekursive opdelingsknuder:",
|
||||||
|
"splitNode": "Opdelingsknude",
|
||||||
|
"splitNodeDesc": "Deler plads i to børn:",
|
||||||
|
"or": "eller",
|
||||||
|
"splitPosition": "Delergitterposition fra 0.1 til 0.9 (standard 0.5)",
|
||||||
|
"splitChildren": "Præcis to underknuder (opdeling eller panel)",
|
||||||
|
"paneNode": "Panelknude",
|
||||||
|
"paneNodeDesc": "En bladknude der indeholder én eller flere overflader (faner inden i panelet).",
|
||||||
|
"surfaceDefinition": "Overfladedefinition",
|
||||||
|
"surfaceDefinitionDesc": "Hver overflade i et panel kan være en terminal eller en browser:",
|
||||||
|
"surfaceName": "Brugerdefineret fanetitel",
|
||||||
|
"surfaceCommand": "Shell-kommando der automatisk køres ved oprettelse (kun terminal)",
|
||||||
|
"surfaceCwd": "Arbejdsmappe for denne overflade",
|
||||||
|
"surfaceEnv": "Miljøvariabler som nøgle-værdi-par",
|
||||||
|
"surfaceUrl": "URL der skal åbnes (kun browser)",
|
||||||
|
"surfaceFocus": "Fokuser på denne overflade efter oprettelse",
|
||||||
|
"cwdResolution": "Opløsning af arbejdsmappe",
|
||||||
|
"omitted": "udeladt",
|
||||||
|
"cwdRelative": "workspace-arbejdsmappe",
|
||||||
|
"cwdSubdir": "relativ til workspace-arbejdsmappe",
|
||||||
|
"cwdHome": "udvidet til hjemmemappen",
|
||||||
|
"absolutePath": "Absolut sti",
|
||||||
|
"cwdAbsolute": "bruges som den er",
|
||||||
|
"fullExample": "Fuldt eksempel"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Tastaturgenveje",
|
"title": "Tastaturgenveje",
|
||||||
"description": "Alle tastaturgenveje tilgængelige i cmux, grupperet efter kategori.",
|
"description": "Alle tastaturgenveje tilgængelige i cmux, grupperet efter kategori.",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "Kom i gang",
|
"gettingStarted": "Kom i gang",
|
||||||
"concepts": "Koncepter",
|
"concepts": "Koncepter",
|
||||||
"configuration": "Konfiguration",
|
"configuration": "Konfiguration",
|
||||||
|
"customCommands": "Brugerdefinerede kommandoer",
|
||||||
"keyboardShortcuts": "Tastaturgenveje",
|
"keyboardShortcuts": "Tastaturgenveje",
|
||||||
"apiReference": "API-reference",
|
"apiReference": "API-reference",
|
||||||
"browserAutomation": "Browserautomatisering",
|
"browserAutomation": "Browserautomatisering",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "Beispielkonfiguration",
|
"exampleConfig": "Beispielkonfiguration",
|
||||||
"metaTitle": "Konfiguration"
|
"metaTitle": "Konfiguration"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "Benutzerdefinierte Befehle",
|
||||||
|
"metaTitle": "Benutzerdefinierte Befehle",
|
||||||
|
"metaDescription": "Benutzerdefinierte Befehle und Workspace-Layouts in cmux.json definieren. Projektspezifische und globale Konfiguration mit Live-Dateiüberwachung.",
|
||||||
|
"intro": "Definieren Sie benutzerdefinierte Befehle und Workspace-Layouts, indem Sie eine cmux.json-Datei in Ihr Projektstammverzeichnis oder ~/.config/cmux/ hinzufügen. Befehle erscheinen in der Befehlspalette.",
|
||||||
|
"fileLocations": "Dateispeicherorte",
|
||||||
|
"fileLocationsDesc": "cmux sucht an zwei Stellen nach Konfiguration:",
|
||||||
|
"localConfig": "Projektspezifisch:",
|
||||||
|
"localConfigDesc": "liegt in Ihrem Projektverzeichnis, hat Vorrang",
|
||||||
|
"globalConfig": "Global:",
|
||||||
|
"globalConfigDesc": "gilt für alle Projekte, ergänzt lokal nicht definierte Befehle",
|
||||||
|
"precedenceNote": "Lokale Befehle überschreiben globale Befehle mit demselben Namen.",
|
||||||
|
"liveReload": "Änderungen werden automatisch übernommen — kein Neustart erforderlich.",
|
||||||
|
"schema": "Schema",
|
||||||
|
"schemaDesc": "Eine cmux.json-Datei enthält ein commands-Array. Jeder Befehl ist entweder ein einfacher Shell-Befehl oder eine vollständige Workspace-Definition:",
|
||||||
|
"simpleCommands": "Einfache Befehle",
|
||||||
|
"simpleCommandsDesc": "Ein einfacher Befehl führt einen Shell-Befehl im aktuell fokussierten Terminal aus:",
|
||||||
|
"simpleCommandFields": "Felder",
|
||||||
|
"fieldName": "Wird in der Befehlspalette angezeigt (erforderlich)",
|
||||||
|
"fieldDescription": "Optionale Beschreibung",
|
||||||
|
"fieldKeywords": "Zusätzliche Suchbegriffe für die Befehlspalette",
|
||||||
|
"fieldCommand": "Shell-Befehl, der im fokussierten Terminal ausgeführt wird",
|
||||||
|
"fieldConfirm": "Bestätigungsdialog vor der Ausführung anzeigen",
|
||||||
|
"simpleCommandCwdNote": "Einfache Befehle werden im aktuellen Arbeitsverzeichnis des fokussierten Terminals ausgeführt. Wenn Ihr Befehl projektrelative Pfade benötigt, stellen Sie",
|
||||||
|
"simpleCommandCwdRepoRoot": "voran, um vom Repository-Stammverzeichnis auszuführen, oder",
|
||||||
|
"simpleCommandCwdCustomPath": "für ein beliebiges Verzeichnis.",
|
||||||
|
"workspaceCommands": "Workspace-Befehle",
|
||||||
|
"workspaceCommandsDesc": "Ein Workspace-Befehl erstellt einen neuen Workspace mit einem benutzerdefinierten Layout aus Aufteilungen, Terminals und Browser-Fenstern:",
|
||||||
|
"workspaceFields": "Workspace-Felder",
|
||||||
|
"wsFieldName": "Name des Workspace-Tabs (Standard ist Befehlsname)",
|
||||||
|
"wsFieldCwd": "Arbeitsverzeichnis für den Workspace",
|
||||||
|
"wsFieldColor": "Farbe des Workspace-Tabs",
|
||||||
|
"wsFieldLayout": "Layout-Baum, der Aufteilungen und Fenster definiert",
|
||||||
|
"restartBehavior": "Neustart-Verhalten",
|
||||||
|
"restartBehaviorDesc": "Steuert, was passiert, wenn bereits ein Workspace mit demselben Namen existiert:",
|
||||||
|
"restartIgnore": "Zum vorhandenen Workspace wechseln (Standard)",
|
||||||
|
"restartRecreate": "Schließen und ohne Rückfrage neu erstellen",
|
||||||
|
"restartConfirm": "Benutzer vor der Neuerstellung fragen",
|
||||||
|
"layoutTree": "Layout-Baum",
|
||||||
|
"layoutTreeDesc": "Der Layout-Baum definiert, wie Fenster mithilfe rekursiver Aufteilungsknoten angeordnet werden:",
|
||||||
|
"splitNode": "Aufteilungsknoten",
|
||||||
|
"splitNodeDesc": "Teilt den Platz in zwei Kindelemente auf:",
|
||||||
|
"or": "oder",
|
||||||
|
"splitPosition": "Teilerposition von 0.1 bis 0.9 (Standard 0.5)",
|
||||||
|
"splitChildren": "Genau zwei Kindknoten (Aufteilung oder Fenster)",
|
||||||
|
"paneNode": "Fensterknoten",
|
||||||
|
"paneNodeDesc": "Ein Blattknoten, der eine oder mehrere Oberflächen enthält (Tabs innerhalb des Fensters).",
|
||||||
|
"surfaceDefinition": "Oberflächendefinition",
|
||||||
|
"surfaceDefinitionDesc": "Jede Oberfläche in einem Fenster kann ein Terminal oder ein Browser sein:",
|
||||||
|
"surfaceName": "Benutzerdefinierter Tab-Titel",
|
||||||
|
"surfaceCommand": "Shell-Befehl, der bei der Erstellung automatisch ausgeführt wird (nur Terminal)",
|
||||||
|
"surfaceCwd": "Arbeitsverzeichnis für diese Oberfläche",
|
||||||
|
"surfaceEnv": "Umgebungsvariablen als Schlüssel-Wert-Paare",
|
||||||
|
"surfaceUrl": "URL zum Öffnen (nur Browser)",
|
||||||
|
"surfaceFocus": "Diese Oberfläche nach der Erstellung fokussieren",
|
||||||
|
"cwdResolution": "Arbeitsverzeichnis-Auflösung",
|
||||||
|
"omitted": "weggelassen",
|
||||||
|
"cwdRelative": "Workspace-Arbeitsverzeichnis",
|
||||||
|
"cwdSubdir": "relativ zum Workspace-Arbeitsverzeichnis",
|
||||||
|
"cwdHome": "auf Home-Verzeichnis erweitert",
|
||||||
|
"absolutePath": "Absoluter Pfad",
|
||||||
|
"cwdAbsolute": "wird unverändert verwendet",
|
||||||
|
"fullExample": "Vollständiges Beispiel"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Tastaturkürzel",
|
"title": "Tastaturkürzel",
|
||||||
"description": "Alle in cmux verfügbaren Tastaturkürzel, nach Kategorie gruppiert.",
|
"description": "Alle in cmux verfügbaren Tastaturkürzel, nach Kategorie gruppiert.",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "Erste Schritte",
|
"gettingStarted": "Erste Schritte",
|
||||||
"concepts": "Konzepte",
|
"concepts": "Konzepte",
|
||||||
"configuration": "Konfiguration",
|
"configuration": "Konfiguration",
|
||||||
|
"customCommands": "Benutzerdefinierte Befehle",
|
||||||
"keyboardShortcuts": "Tastaturkürzel",
|
"keyboardShortcuts": "Tastaturkürzel",
|
||||||
"apiReference": "API-Referenz",
|
"apiReference": "API-Referenz",
|
||||||
"browserAutomation": "Browser-Automatisierung",
|
"browserAutomation": "Browser-Automatisierung",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"browserHostsHttp": "HTTP Hosts Allowed in Embedded Browser: applies only to HTTP (non-HTTPS) URLs. Hosts in this list can open in cmux without a warning prompt. Defaults include localhost, 127.0.0.1, ::1, 0.0.0.0, and *.localtest.me.",
|
"browserHostsHttp": "HTTP Hosts Allowed in Embedded Browser: applies only to HTTP (non-HTTPS) URLs. Hosts in this list can open in cmux without a warning prompt. Defaults include localhost, 127.0.0.1, ::1, 0.0.0.0, and *.localtest.me.",
|
||||||
"exampleConfig": "Example config"
|
"exampleConfig": "Example config"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "Custom Commands",
|
||||||
|
"metaTitle": "Custom Commands",
|
||||||
|
"metaDescription": "Define custom commands and workspace layouts in cmux.json. Per-project and global configuration with live file watching.",
|
||||||
|
"intro": "Define custom commands and workspace layouts by adding a cmux.json file to your project root or ~/.config/cmux/. Commands appear in the command palette.",
|
||||||
|
"fileLocations": "File locations",
|
||||||
|
"fileLocationsDesc": "cmux looks for configuration in two places:",
|
||||||
|
"localConfig": "Per-project:",
|
||||||
|
"localConfigDesc": "lives in your project directory, takes precedence",
|
||||||
|
"globalConfig": "Global:",
|
||||||
|
"globalConfigDesc": "applies to all projects, fills in commands not defined locally",
|
||||||
|
"precedenceNote": "Local commands override global commands with the same name.",
|
||||||
|
"liveReload": "Changes are picked up automatically — no restart needed.",
|
||||||
|
"schema": "Schema",
|
||||||
|
"schemaDesc": "A cmux.json file contains a commands array. Each command is either a simple shell command or a full workspace definition:",
|
||||||
|
"simpleCommands": "Simple commands",
|
||||||
|
"simpleCommandsDesc": "A simple command runs a shell command in the currently focused terminal:",
|
||||||
|
"simpleCommandFields": "Fields",
|
||||||
|
"fieldName": "Displayed in the command palette (required)",
|
||||||
|
"fieldDescription": "Optional description",
|
||||||
|
"fieldKeywords": "Extra search terms for the command palette",
|
||||||
|
"fieldCommand": "Shell command to run in the focused terminal",
|
||||||
|
"fieldConfirm": "Show a confirmation dialog before running",
|
||||||
|
"simpleCommandCwdNote": "Simple commands run in the focused terminal's current working directory. If your command relies on project-relative paths, prefix it with",
|
||||||
|
"simpleCommandCwdRepoRoot": "to run from the repo root, or",
|
||||||
|
"simpleCommandCwdCustomPath": "for any specific directory.",
|
||||||
|
"workspaceCommands": "Workspace commands",
|
||||||
|
"workspaceCommandsDesc": "A workspace command creates a new workspace with a custom layout of splits, terminals, and browser panes:",
|
||||||
|
"workspaceFields": "Workspace fields",
|
||||||
|
"wsFieldName": "Workspace tab name (defaults to command name)",
|
||||||
|
"wsFieldCwd": "Working directory for the workspace",
|
||||||
|
"wsFieldColor": "Workspace tab color",
|
||||||
|
"wsFieldLayout": "Layout tree defining splits and panes",
|
||||||
|
"restartBehavior": "Restart behavior",
|
||||||
|
"restartBehaviorDesc": "Controls what happens when a workspace with the same name already exists:",
|
||||||
|
"restartIgnore": "Switch to the existing workspace (default)",
|
||||||
|
"restartRecreate": "Close and recreate without asking",
|
||||||
|
"restartConfirm": "Ask the user before recreating",
|
||||||
|
"layoutTree": "Layout tree",
|
||||||
|
"layoutTreeDesc": "The layout tree defines how panes are arranged using recursive split nodes:",
|
||||||
|
"splitNode": "Split node",
|
||||||
|
"splitNodeDesc": "Divides space into two children:",
|
||||||
|
"or": "or",
|
||||||
|
"splitPosition": "Divider position from 0.1 to 0.9 (default 0.5)",
|
||||||
|
"splitChildren": "Exactly two child nodes (split or pane)",
|
||||||
|
"paneNode": "Pane node",
|
||||||
|
"paneNodeDesc": "A leaf node containing one or more surfaces (tabs within the pane).",
|
||||||
|
"surfaceDefinition": "Surface definition",
|
||||||
|
"surfaceDefinitionDesc": "Each surface in a pane can be a terminal or a browser:",
|
||||||
|
"surfaceName": "Custom tab title",
|
||||||
|
"surfaceCommand": "Shell command to auto-run on creation (terminal only)",
|
||||||
|
"surfaceCwd": "Working directory for this surface",
|
||||||
|
"surfaceEnv": "Environment variables as key-value pairs",
|
||||||
|
"surfaceUrl": "URL to open (browser only)",
|
||||||
|
"surfaceFocus": "Focus this surface after creation",
|
||||||
|
"cwdResolution": "Working directory resolution",
|
||||||
|
"omitted": "omitted",
|
||||||
|
"cwdRelative": "workspace working directory",
|
||||||
|
"cwdSubdir": "relative to workspace working directory",
|
||||||
|
"cwdHome": "expanded to home directory",
|
||||||
|
"absolutePath": "Absolute path",
|
||||||
|
"cwdAbsolute": "used as-is",
|
||||||
|
"fullExample": "Full example"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Keyboard Shortcuts",
|
"title": "Keyboard Shortcuts",
|
||||||
"description": "All keyboard shortcuts available in cmux, grouped by category.",
|
"description": "All keyboard shortcuts available in cmux, grouped by category.",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "Getting Started",
|
"gettingStarted": "Getting Started",
|
||||||
"concepts": "Concepts",
|
"concepts": "Concepts",
|
||||||
"configuration": "Configuration",
|
"configuration": "Configuration",
|
||||||
|
"customCommands": "Custom Commands",
|
||||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||||
"apiReference": "API Reference",
|
"apiReference": "API Reference",
|
||||||
"browserAutomation": "Browser Automation",
|
"browserAutomation": "Browser Automation",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "Configuración de ejemplo",
|
"exampleConfig": "Configuración de ejemplo",
|
||||||
"metaTitle": "Configuración"
|
"metaTitle": "Configuración"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "Comandos personalizados",
|
||||||
|
"metaTitle": "Comandos personalizados",
|
||||||
|
"metaDescription": "Define comandos personalizados y diseños de workspace en cmux.json. Configuración por proyecto y global con monitoreo en vivo de archivos.",
|
||||||
|
"intro": "Define comandos personalizados y diseños de workspace añadiendo un archivo cmux.json a la raíz de tu proyecto o ~/.config/cmux/. Los comandos aparecen en la paleta de comandos.",
|
||||||
|
"fileLocations": "Ubicaciones de archivos",
|
||||||
|
"fileLocationsDesc": "cmux busca configuración en dos lugares:",
|
||||||
|
"localConfig": "Por proyecto:",
|
||||||
|
"localConfigDesc": "se encuentra en tu directorio de proyecto, tiene prioridad",
|
||||||
|
"globalConfig": "Global:",
|
||||||
|
"globalConfigDesc": "se aplica a todos los proyectos, completa los comandos no definidos localmente",
|
||||||
|
"precedenceNote": "Los comandos locales anulan los comandos globales con el mismo nombre.",
|
||||||
|
"liveReload": "Los cambios se recogen automáticamente — no se necesita reinicio.",
|
||||||
|
"schema": "Esquema",
|
||||||
|
"schemaDesc": "Un archivo cmux.json contiene un array commands. Cada comando es un comando de shell simple o una definición completa de workspace:",
|
||||||
|
"simpleCommands": "Comandos simples",
|
||||||
|
"simpleCommandsDesc": "Un comando simple ejecuta un comando de shell en el terminal actualmente enfocado:",
|
||||||
|
"simpleCommandFields": "Campos",
|
||||||
|
"fieldName": "Se muestra en la paleta de comandos (requerido)",
|
||||||
|
"fieldDescription": "Descripción opcional",
|
||||||
|
"fieldKeywords": "Términos de búsqueda adicionales para la paleta de comandos",
|
||||||
|
"fieldCommand": "Comando de shell para ejecutar en el terminal enfocado",
|
||||||
|
"fieldConfirm": "Mostrar un diálogo de confirmación antes de ejecutar",
|
||||||
|
"simpleCommandCwdNote": "Los comandos simples se ejecutan en el directorio de trabajo actual de la terminal enfocada. Si tu comando depende de rutas relativas al proyecto, prefija con",
|
||||||
|
"simpleCommandCwdRepoRoot": "para ejecutar desde la raíz del repositorio, o",
|
||||||
|
"simpleCommandCwdCustomPath": "para cualquier directorio específico.",
|
||||||
|
"workspaceCommands": "Comandos de workspace",
|
||||||
|
"workspaceCommandsDesc": "Un comando de workspace crea un nuevo workspace con un diseño personalizado de divisiones, terminales y paneles de navegador:",
|
||||||
|
"workspaceFields": "Campos de workspace",
|
||||||
|
"wsFieldName": "Nombre de la pestaña del workspace (por defecto es el nombre del comando)",
|
||||||
|
"wsFieldCwd": "Directorio de trabajo del workspace",
|
||||||
|
"wsFieldColor": "Color de la pestaña del workspace",
|
||||||
|
"wsFieldLayout": "Árbol de diseño que define divisiones y paneles",
|
||||||
|
"restartBehavior": "Comportamiento de reinicio",
|
||||||
|
"restartBehaviorDesc": "Controla qué sucede cuando ya existe un workspace con el mismo nombre:",
|
||||||
|
"restartIgnore": "Cambiar al workspace existente (por defecto)",
|
||||||
|
"restartRecreate": "Cerrar y recrear sin preguntar",
|
||||||
|
"restartConfirm": "Preguntar al usuario antes de recrear",
|
||||||
|
"layoutTree": "Árbol de diseño",
|
||||||
|
"layoutTreeDesc": "El árbol de diseño define cómo se organizan los paneles usando nodos de división recursivos:",
|
||||||
|
"splitNode": "Nodo de división",
|
||||||
|
"splitNodeDesc": "Divide el espacio en dos hijos:",
|
||||||
|
"or": "o",
|
||||||
|
"splitPosition": "Posición del divisor de 0.1 a 0.9 (por defecto 0.5)",
|
||||||
|
"splitChildren": "Exactamente dos nodos hijos (división o panel)",
|
||||||
|
"paneNode": "Nodo de panel",
|
||||||
|
"paneNodeDesc": "Un nodo hoja que contiene una o más superficies (pestañas dentro del panel).",
|
||||||
|
"surfaceDefinition": "Definición de superficie",
|
||||||
|
"surfaceDefinitionDesc": "Cada superficie en un panel puede ser un terminal o un navegador:",
|
||||||
|
"surfaceName": "Título de pestaña personalizado",
|
||||||
|
"surfaceCommand": "Comando de shell para ejecutar automáticamente al crear (solo terminal)",
|
||||||
|
"surfaceCwd": "Directorio de trabajo para esta superficie",
|
||||||
|
"surfaceEnv": "Variables de entorno como pares clave-valor",
|
||||||
|
"surfaceUrl": "URL para abrir (solo navegador)",
|
||||||
|
"surfaceFocus": "Enfocar esta superficie después de crearla",
|
||||||
|
"cwdResolution": "Resolución del directorio de trabajo",
|
||||||
|
"omitted": "omitido",
|
||||||
|
"cwdRelative": "directorio de trabajo del workspace",
|
||||||
|
"cwdSubdir": "relativo al directorio de trabajo del workspace",
|
||||||
|
"cwdHome": "expandido al directorio home",
|
||||||
|
"absolutePath": "Ruta absoluta",
|
||||||
|
"cwdAbsolute": "se usa tal cual",
|
||||||
|
"fullExample": "Ejemplo completo"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Atajos de teclado",
|
"title": "Atajos de teclado",
|
||||||
"description": "Todos los atajos de teclado disponibles en cmux, agrupados por categoría.",
|
"description": "Todos los atajos de teclado disponibles en cmux, agrupados por categoría.",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "Primeros pasos",
|
"gettingStarted": "Primeros pasos",
|
||||||
"concepts": "Conceptos",
|
"concepts": "Conceptos",
|
||||||
"configuration": "Configuración",
|
"configuration": "Configuración",
|
||||||
|
"customCommands": "Comandos personalizados",
|
||||||
"keyboardShortcuts": "Atajos de teclado",
|
"keyboardShortcuts": "Atajos de teclado",
|
||||||
"apiReference": "Referencia de API",
|
"apiReference": "Referencia de API",
|
||||||
"browserAutomation": "Automatización del navegador",
|
"browserAutomation": "Automatización del navegador",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "Exemple de configuration",
|
"exampleConfig": "Exemple de configuration",
|
||||||
"metaTitle": "Configuration"
|
"metaTitle": "Configuration"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "Commandes personnalisées",
|
||||||
|
"metaTitle": "Commandes personnalisées",
|
||||||
|
"metaDescription": "Définissez des commandes personnalisées et des mises en page d'espace de travail dans cmux.json. Configuration par projet et globale avec surveillance en direct des fichiers.",
|
||||||
|
"intro": "Définissez des commandes personnalisées et des mises en page d'espace de travail en ajoutant un fichier cmux.json à la racine de votre projet ou ~/.config/cmux/. Les commandes apparaissent dans la palette de commandes.",
|
||||||
|
"fileLocations": "Emplacements des fichiers",
|
||||||
|
"fileLocationsDesc": "cmux recherche la configuration à deux endroits :",
|
||||||
|
"localConfig": "Par projet :",
|
||||||
|
"localConfigDesc": "se trouve dans votre répertoire de projet, a la priorité",
|
||||||
|
"globalConfig": "Global :",
|
||||||
|
"globalConfigDesc": "s'applique à tous les projets, complète les commandes non définies localement",
|
||||||
|
"precedenceNote": "Les commandes locales remplacent les commandes globales du même nom.",
|
||||||
|
"liveReload": "Les modifications sont prises en compte automatiquement — aucun redémarrage nécessaire.",
|
||||||
|
"schema": "Schéma",
|
||||||
|
"schemaDesc": "Un fichier cmux.json contient un tableau commands. Chaque commande est soit une commande shell simple, soit une définition complète d'espace de travail :",
|
||||||
|
"simpleCommands": "Commandes simples",
|
||||||
|
"simpleCommandsDesc": "Une commande simple exécute une commande shell dans le terminal actuellement focalisé :",
|
||||||
|
"simpleCommandFields": "Champs",
|
||||||
|
"fieldName": "Affiché dans la palette de commandes (requis)",
|
||||||
|
"fieldDescription": "Description optionnelle",
|
||||||
|
"fieldKeywords": "Termes de recherche supplémentaires pour la palette de commandes",
|
||||||
|
"fieldCommand": "Commande shell à exécuter dans le terminal focalisé",
|
||||||
|
"fieldConfirm": "Afficher une boîte de dialogue de confirmation avant l'exécution",
|
||||||
|
"simpleCommandCwdNote": "Les commandes simples s'exécutent dans le répertoire de travail actuel du terminal ciblé. Si votre commande utilise des chemins relatifs au projet, préfixez avec",
|
||||||
|
"simpleCommandCwdRepoRoot": "pour exécuter depuis la racine du dépôt, ou",
|
||||||
|
"simpleCommandCwdCustomPath": "pour n'importe quel répertoire spécifique.",
|
||||||
|
"workspaceCommands": "Commandes d'espace de travail",
|
||||||
|
"workspaceCommandsDesc": "Une commande d'espace de travail crée un nouvel espace de travail avec une mise en page personnalisée de divisions, terminaux et panneaux de navigateur :",
|
||||||
|
"workspaceFields": "Champs de l'espace de travail",
|
||||||
|
"wsFieldName": "Nom de l'onglet de l'espace de travail (par défaut, nom de la commande)",
|
||||||
|
"wsFieldCwd": "Répertoire de travail de l'espace de travail",
|
||||||
|
"wsFieldColor": "Couleur de l'onglet de l'espace de travail",
|
||||||
|
"wsFieldLayout": "Arbre de mise en page définissant les divisions et panneaux",
|
||||||
|
"restartBehavior": "Comportement au redémarrage",
|
||||||
|
"restartBehaviorDesc": "Contrôle ce qui se passe lorsqu'un espace de travail du même nom existe déjà :",
|
||||||
|
"restartIgnore": "Basculer vers l'espace de travail existant (par défaut)",
|
||||||
|
"restartRecreate": "Fermer et recréer sans demander",
|
||||||
|
"restartConfirm": "Demander à l'utilisateur avant de recréer",
|
||||||
|
"layoutTree": "Arbre de mise en page",
|
||||||
|
"layoutTreeDesc": "L'arbre de mise en page définit comment les panneaux sont disposés à l'aide de nœuds de division récursifs :",
|
||||||
|
"splitNode": "Nœud de division",
|
||||||
|
"splitNodeDesc": "Divise l'espace en deux enfants :",
|
||||||
|
"or": "ou",
|
||||||
|
"splitPosition": "Position du séparateur de 0.1 à 0.9 (par défaut 0.5)",
|
||||||
|
"splitChildren": "Exactement deux nœuds enfants (division ou panneau)",
|
||||||
|
"paneNode": "Nœud de panneau",
|
||||||
|
"paneNodeDesc": "Un nœud feuille contenant une ou plusieurs surfaces (onglets dans le panneau).",
|
||||||
|
"surfaceDefinition": "Définition de surface",
|
||||||
|
"surfaceDefinitionDesc": "Chaque surface dans un panneau peut être un terminal ou un navigateur :",
|
||||||
|
"surfaceName": "Titre d'onglet personnalisé",
|
||||||
|
"surfaceCommand": "Commande shell à exécuter automatiquement à la création (terminal uniquement)",
|
||||||
|
"surfaceCwd": "Répertoire de travail pour cette surface",
|
||||||
|
"surfaceEnv": "Variables d'environnement sous forme de paires clé-valeur",
|
||||||
|
"surfaceUrl": "URL à ouvrir (navigateur uniquement)",
|
||||||
|
"surfaceFocus": "Focaliser cette surface après la création",
|
||||||
|
"cwdResolution": "Résolution du répertoire de travail",
|
||||||
|
"omitted": "omis",
|
||||||
|
"cwdRelative": "répertoire de travail de l'espace de travail",
|
||||||
|
"cwdSubdir": "relatif au répertoire de travail de l'espace de travail",
|
||||||
|
"cwdHome": "développé vers le répertoire personnel",
|
||||||
|
"absolutePath": "Chemin absolu",
|
||||||
|
"cwdAbsolute": "utilisé tel quel",
|
||||||
|
"fullExample": "Exemple complet"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Raccourcis clavier",
|
"title": "Raccourcis clavier",
|
||||||
"description": "Tous les raccourcis clavier disponibles dans cmux, classés par catégorie.",
|
"description": "Tous les raccourcis clavier disponibles dans cmux, classés par catégorie.",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "Premiers pas",
|
"gettingStarted": "Premiers pas",
|
||||||
"concepts": "Concepts",
|
"concepts": "Concepts",
|
||||||
"configuration": "Configuration",
|
"configuration": "Configuration",
|
||||||
|
"customCommands": "Commandes personnalisées",
|
||||||
"keyboardShortcuts": "Raccourcis clavier",
|
"keyboardShortcuts": "Raccourcis clavier",
|
||||||
"apiReference": "Référence API",
|
"apiReference": "Référence API",
|
||||||
"browserAutomation": "Automatisation du navigateur",
|
"browserAutomation": "Automatisation du navigateur",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "Esempio di configurazione",
|
"exampleConfig": "Esempio di configurazione",
|
||||||
"metaTitle": "Configurazione"
|
"metaTitle": "Configurazione"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "Comandi personalizzati",
|
||||||
|
"metaTitle": "Comandi personalizzati",
|
||||||
|
"metaDescription": "Definisci comandi personalizzati e layout workspace in cmux.json. Configurazione per progetto e globale con monitoraggio in tempo reale dei file.",
|
||||||
|
"intro": "Definisci comandi personalizzati e layout workspace aggiungendo un file cmux.json alla radice del progetto o ~/.config/cmux/. I comandi appaiono nella palette dei comandi.",
|
||||||
|
"fileLocations": "Posizioni dei file",
|
||||||
|
"fileLocationsDesc": "cmux cerca la configurazione in due posti:",
|
||||||
|
"localConfig": "Per progetto:",
|
||||||
|
"localConfigDesc": "si trova nella directory del progetto, ha la precedenza",
|
||||||
|
"globalConfig": "Globale:",
|
||||||
|
"globalConfigDesc": "si applica a tutti i progetti, integra i comandi non definiti localmente",
|
||||||
|
"precedenceNote": "I comandi locali sovrascrivono i comandi globali con lo stesso nome.",
|
||||||
|
"liveReload": "Le modifiche vengono rilevate automaticamente — nessun riavvio necessario.",
|
||||||
|
"schema": "Schema",
|
||||||
|
"schemaDesc": "Un file cmux.json contiene un array commands. Ogni comando è un semplice comando shell o una definizione completa di workspace:",
|
||||||
|
"simpleCommands": "Comandi semplici",
|
||||||
|
"simpleCommandsDesc": "Un comando semplice esegue un comando shell nel terminale attualmente attivo:",
|
||||||
|
"simpleCommandFields": "Campi",
|
||||||
|
"fieldName": "Visualizzato nella palette dei comandi (obbligatorio)",
|
||||||
|
"fieldDescription": "Descrizione opzionale",
|
||||||
|
"fieldKeywords": "Termini di ricerca aggiuntivi per la palette dei comandi",
|
||||||
|
"fieldCommand": "Comando shell da eseguire nel terminale attivo",
|
||||||
|
"fieldConfirm": "Mostra una finestra di conferma prima dell'esecuzione",
|
||||||
|
"simpleCommandCwdNote": "I comandi semplici vengono eseguiti nella directory di lavoro corrente del terminale focalizzato. Se il comando dipende da percorsi relativi al progetto, aggiungi il prefisso",
|
||||||
|
"simpleCommandCwdRepoRoot": "per eseguire dalla radice del repository, o",
|
||||||
|
"simpleCommandCwdCustomPath": "per qualsiasi directory specifica.",
|
||||||
|
"workspaceCommands": "Comandi workspace",
|
||||||
|
"workspaceCommandsDesc": "Un comando workspace crea un nuovo workspace con un layout personalizzato di divisioni, terminali e pannelli browser:",
|
||||||
|
"workspaceFields": "Campi workspace",
|
||||||
|
"wsFieldName": "Nome della scheda workspace (predefinito: nome del comando)",
|
||||||
|
"wsFieldCwd": "Directory di lavoro del workspace",
|
||||||
|
"wsFieldColor": "Colore della scheda workspace",
|
||||||
|
"wsFieldLayout": "Albero di layout che definisce divisioni e pannelli",
|
||||||
|
"restartBehavior": "Comportamento al riavvio",
|
||||||
|
"restartBehaviorDesc": "Controlla cosa succede quando esiste già un workspace con lo stesso nome:",
|
||||||
|
"restartIgnore": "Passa al workspace esistente (predefinito)",
|
||||||
|
"restartRecreate": "Chiudi e ricrea senza chiedere",
|
||||||
|
"restartConfirm": "Chiedi all'utente prima di ricreare",
|
||||||
|
"layoutTree": "Albero di layout",
|
||||||
|
"layoutTreeDesc": "L'albero di layout definisce come i pannelli sono disposti usando nodi di divisione ricorsivi:",
|
||||||
|
"splitNode": "Nodo di divisione",
|
||||||
|
"splitNodeDesc": "Divide lo spazio in due figli:",
|
||||||
|
"or": "o",
|
||||||
|
"splitPosition": "Posizione del divisore da 0.1 a 0.9 (predefinito 0.5)",
|
||||||
|
"splitChildren": "Esattamente due nodi figli (divisione o pannello)",
|
||||||
|
"paneNode": "Nodo pannello",
|
||||||
|
"paneNodeDesc": "Un nodo foglia contenente una o più superfici (schede all'interno del pannello).",
|
||||||
|
"surfaceDefinition": "Definizione superficie",
|
||||||
|
"surfaceDefinitionDesc": "Ogni superficie in un pannello può essere un terminale o un browser:",
|
||||||
|
"surfaceName": "Titolo scheda personalizzato",
|
||||||
|
"surfaceCommand": "Comando shell da eseguire automaticamente alla creazione (solo terminale)",
|
||||||
|
"surfaceCwd": "Directory di lavoro per questa superficie",
|
||||||
|
"surfaceEnv": "Variabili d'ambiente come coppie chiave-valore",
|
||||||
|
"surfaceUrl": "URL da aprire (solo browser)",
|
||||||
|
"surfaceFocus": "Metti il focus su questa superficie dopo la creazione",
|
||||||
|
"cwdResolution": "Risoluzione della directory di lavoro",
|
||||||
|
"omitted": "omesso",
|
||||||
|
"cwdRelative": "directory di lavoro del workspace",
|
||||||
|
"cwdSubdir": "relativa alla directory di lavoro del workspace",
|
||||||
|
"cwdHome": "espansa alla directory home",
|
||||||
|
"absolutePath": "Percorso assoluto",
|
||||||
|
"cwdAbsolute": "usato così com'è",
|
||||||
|
"fullExample": "Esempio completo"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Scorciatoie da tastiera",
|
"title": "Scorciatoie da tastiera",
|
||||||
"description": "Tutte le scorciatoie da tastiera disponibili in cmux, raggruppate per categoria.",
|
"description": "Tutte le scorciatoie da tastiera disponibili in cmux, raggruppate per categoria.",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "Per iniziare",
|
"gettingStarted": "Per iniziare",
|
||||||
"concepts": "Concetti",
|
"concepts": "Concetti",
|
||||||
"configuration": "Configurazione",
|
"configuration": "Configurazione",
|
||||||
|
"customCommands": "Comandi personalizzati",
|
||||||
"keyboardShortcuts": "Scorciatoie da tastiera",
|
"keyboardShortcuts": "Scorciatoie da tastiera",
|
||||||
"apiReference": "Riferimento API",
|
"apiReference": "Riferimento API",
|
||||||
"browserAutomation": "Automazione del browser",
|
"browserAutomation": "Automazione del browser",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "設定例",
|
"exampleConfig": "設定例",
|
||||||
"metaTitle": "設定"
|
"metaTitle": "設定"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "カスタムコマンド",
|
||||||
|
"metaTitle": "カスタムコマンド",
|
||||||
|
"metaDescription": "cmux.jsonでカスタムコマンドとワークスペースレイアウトを定義します。プロジェクトごとおよびグローバル設定とライブファイル監視に対応。",
|
||||||
|
"intro": "プロジェクトルートまたは ~/.config/cmux/ に cmux.json ファイルを追加してカスタムコマンドとワークスペースレイアウトを定義します。コマンドはコマンドパレットに表示されます。",
|
||||||
|
"fileLocations": "ファイルの場所",
|
||||||
|
"fileLocationsDesc": "cmux は2か所で設定を検索します:",
|
||||||
|
"localConfig": "プロジェクトごと:",
|
||||||
|
"localConfigDesc": "プロジェクトディレクトリに置かれ、優先されます",
|
||||||
|
"globalConfig": "グローバル:",
|
||||||
|
"globalConfigDesc": "すべてのプロジェクトに適用され、ローカルで未定義のコマンドを補完します",
|
||||||
|
"precedenceNote": "ローカルコマンドは同名のグローバルコマンドを上書きします。",
|
||||||
|
"liveReload": "変更は自動的に反映されます — 再起動は不要です。",
|
||||||
|
"schema": "スキーマ",
|
||||||
|
"schemaDesc": "cmux.json ファイルには commands 配列が含まれます。各コマンドはシンプルなシェルコマンドまたは完全なワークスペース定義です:",
|
||||||
|
"simpleCommands": "シンプルコマンド",
|
||||||
|
"simpleCommandsDesc": "シンプルコマンドは現在フォーカスされているターミナルでシェルコマンドを実行します:",
|
||||||
|
"simpleCommandFields": "フィールド",
|
||||||
|
"fieldName": "コマンドパレットに表示されます(必須)",
|
||||||
|
"fieldDescription": "任意の説明",
|
||||||
|
"fieldKeywords": "コマンドパレット用の追加検索キーワード",
|
||||||
|
"fieldCommand": "フォーカスされたターミナルで実行するシェルコマンド",
|
||||||
|
"fieldConfirm": "実行前に確認ダイアログを表示する",
|
||||||
|
"simpleCommandCwdNote": "シンプルコマンドはフォーカスされたターミナルの現在の作業ディレクトリで実行されます。プロジェクト相対パスに依存するコマンドの場合は、先頭に",
|
||||||
|
"simpleCommandCwdRepoRoot": "を付けてリポジトリのルートから実行するか、",
|
||||||
|
"simpleCommandCwdCustomPath": "で任意のディレクトリを指定できます。",
|
||||||
|
"workspaceCommands": "ワークスペースコマンド",
|
||||||
|
"workspaceCommandsDesc": "ワークスペースコマンドは、分割、ターミナル、ブラウザペインのカスタムレイアウトで新しいワークスペースを作成します:",
|
||||||
|
"workspaceFields": "ワークスペースフィールド",
|
||||||
|
"wsFieldName": "ワークスペースのタブ名(デフォルトはコマンド名)",
|
||||||
|
"wsFieldCwd": "ワークスペースの作業ディレクトリ",
|
||||||
|
"wsFieldColor": "ワークスペースのタブカラー",
|
||||||
|
"wsFieldLayout": "分割とペインを定義するレイアウトツリー",
|
||||||
|
"restartBehavior": "再起動の動作",
|
||||||
|
"restartBehaviorDesc": "同名のワークスペースが既に存在する場合の動作を制御します:",
|
||||||
|
"restartIgnore": "既存のワークスペースに切り替える(デフォルト)",
|
||||||
|
"restartRecreate": "確認なしに閉じて再作成する",
|
||||||
|
"restartConfirm": "再作成前にユーザーに確認する",
|
||||||
|
"layoutTree": "レイアウトツリー",
|
||||||
|
"layoutTreeDesc": "レイアウトツリーは、再帰的な分割ノードを使用してペインの配置を定義します:",
|
||||||
|
"splitNode": "分割ノード",
|
||||||
|
"splitNodeDesc": "スペースを2つの子に分割します:",
|
||||||
|
"or": "または",
|
||||||
|
"splitPosition": "分割位置(0.1〜0.9、デフォルト0.5)",
|
||||||
|
"splitChildren": "正確に2つの子ノード(分割またはペイン)",
|
||||||
|
"paneNode": "ペインノード",
|
||||||
|
"paneNodeDesc": "1つ以上のサーフェス(ペイン内のタブ)を含むリーフノード。",
|
||||||
|
"surfaceDefinition": "サーフェス定義",
|
||||||
|
"surfaceDefinitionDesc": "ペイン内の各サーフェスはターミナルまたはブラウザです:",
|
||||||
|
"surfaceName": "カスタムタブタイトル",
|
||||||
|
"surfaceCommand": "作成時に自動実行するシェルコマンド(ターミナルのみ)",
|
||||||
|
"surfaceCwd": "このサーフェスの作業ディレクトリ",
|
||||||
|
"surfaceEnv": "キーと値のペアとしての環境変数",
|
||||||
|
"surfaceUrl": "開くURL(ブラウザのみ)",
|
||||||
|
"surfaceFocus": "作成後にこのサーフェスにフォーカスする",
|
||||||
|
"cwdResolution": "作業ディレクトリの解決",
|
||||||
|
"omitted": "省略",
|
||||||
|
"cwdRelative": "ワークスペースの作業ディレクトリ",
|
||||||
|
"cwdSubdir": "ワークスペースの作業ディレクトリからの相対パス",
|
||||||
|
"cwdHome": "ホームディレクトリに展開",
|
||||||
|
"absolutePath": "絶対パス",
|
||||||
|
"cwdAbsolute": "そのまま使用",
|
||||||
|
"fullExample": "完全な例"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "キーボードショートカット",
|
"title": "キーボードショートカット",
|
||||||
"description": "cmuxで使用可能なすべてのキーボードショートカット(カテゴリ別)。",
|
"description": "cmuxで使用可能なすべてのキーボードショートカット(カテゴリ別)。",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "はじめに",
|
"gettingStarted": "はじめに",
|
||||||
"concepts": "コンセプト",
|
"concepts": "コンセプト",
|
||||||
"configuration": "設定",
|
"configuration": "設定",
|
||||||
|
"customCommands": "カスタムコマンド",
|
||||||
"keyboardShortcuts": "キーボードショートカット",
|
"keyboardShortcuts": "キーボードショートカット",
|
||||||
"apiReference": "APIリファレンス",
|
"apiReference": "APIリファレンス",
|
||||||
"browserAutomation": "ブラウザ自動化",
|
"browserAutomation": "ブラウザ自動化",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "ឧទាហរណ៍កំណត់រចនាសម្ព័ន្ធ",
|
"exampleConfig": "ឧទាហរណ៍កំណត់រចនាសម្ព័ន្ធ",
|
||||||
"metaTitle": "ការកំណត់រចនាសម្ព័ន្ធ"
|
"metaTitle": "ការកំណត់រចនាសម្ព័ន្ធ"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "ពាក្យបញ្ជាផ្ទាល់ខ្លួន",
|
||||||
|
"metaTitle": "ពាក្យបញ្ជាផ្ទាល់ខ្លួន",
|
||||||
|
"metaDescription": "កំណត់ពាក្យបញ្ជាផ្ទាល់ខ្លួននិងប្លង់ workspace ក្នុង cmux.json។ ការកំណត់រចនាសម្ព័ន្ធតាមគម្រោងនិងសាកល ជាមួយការតាមដានឯកសារដោយផ្ទាល់។",
|
||||||
|
"intro": "កំណត់ពាក្យបញ្ជាផ្ទាល់ខ្លួននិងប្លង់ workspace ដោយបន្ថែមឯកសារ cmux.json ទៅឫសគម្រោងរបស់អ្នក ឬ ~/.config/cmux/។ ពាក្យបញ្ជាលេចឡើងក្នុងបន្ទះពាក្យបញ្ជា។",
|
||||||
|
"fileLocations": "ទីតាំងឯកសារ",
|
||||||
|
"fileLocationsDesc": "cmux រកការកំណត់រចនាសម្ព័ន្ធនៅ២កន្លែង:",
|
||||||
|
"localConfig": "តាមគម្រោង:",
|
||||||
|
"localConfigDesc": "ស្ថិតក្នុងថតគម្រោងរបស់អ្នក, មានអាទិភាព",
|
||||||
|
"globalConfig": "សាកល:",
|
||||||
|
"globalConfigDesc": "អនុវត្តចំពោះគម្រោងទាំងអស់, បំពេញពាក្យបញ្ជាដែលមិនបានកំណត់ក្នុងតំបន់",
|
||||||
|
"precedenceNote": "ពាក្យបញ្ជាក្នុងតំបន់បដិសេធពាក្យបញ្ជាសាកលដែលមានឈ្មោះដូចគ្នា។",
|
||||||
|
"liveReload": "ការផ្លាស់ប្ដូរត្រូវបានទទួលស្គាល់ដោយស្វ័យប្រវត្តិ — មិនចាំបាច់ចាប់ផ្ដើមឡើងវិញ។",
|
||||||
|
"schema": "ស្គីម៉ា",
|
||||||
|
"schemaDesc": "ឯកសារ cmux.json មានអារ៉េ commands។ ពាក្យបញ្ជានីមួយៗគឺជាពាក្យបញ្ជា shell សាមញ្ញ ឬនិយាមព workspace ពេញលេញ:",
|
||||||
|
"simpleCommands": "ពាក្យបញ្ជាសាមញ្ញ",
|
||||||
|
"simpleCommandsDesc": "ពាក្យបញ្ជាសាមញ្ញដំណើរការពាក្យបញ្ជា shell ក្នុងទែមីណាលដែលបានផ្ដោតបច្ចុប្បន្ន:",
|
||||||
|
"simpleCommandFields": "វាល",
|
||||||
|
"fieldName": "បង្ហាញក្នុងបន្ទះពាក្យបញ្ជា (ចាំបាច់)",
|
||||||
|
"fieldDescription": "ការពិពណ៌នាជាជម្រើស",
|
||||||
|
"fieldKeywords": "ពាក្យស្វែងរកបន្ថែមសម្រាប់បន្ទះពាក្យបញ្ជា",
|
||||||
|
"fieldCommand": "ពាក្យបញ្ជា shell ដំណើរការក្នុងទែមីណាលដែលបានផ្ដោត",
|
||||||
|
"fieldConfirm": "បង្ហាញប្រអប់បញ្ជាក់មុននឹងដំណើរការ",
|
||||||
|
"simpleCommandCwdNote": "ពាក្យបញ្ជាសាមញ្ញដំណើរការក្នុងថតការងារបច្ចុប្បន្នរបស់ terminal ដែលកំពុងផ្ដោត។ ប្រសិនបើពាក្យបញ្ជារបស់អ្នកពឹងផ្អែកលើផ្លូវទាក់ទងនឹងគម្រោង សូមបន្ថែមពីមុខ",
|
||||||
|
"simpleCommandCwdRepoRoot": "ដើម្បីដំណើរការពីឫសនៃ repo ឬ",
|
||||||
|
"simpleCommandCwdCustomPath": "សម្រាប់ថតណាមួយជាក់លាក់។",
|
||||||
|
"workspaceCommands": "ពាក្យបញ្ជា workspace",
|
||||||
|
"workspaceCommandsDesc": "ពាក្យបញ្ជា workspace បង្កើត workspace ថ្មីជាមួយប្លង់ផ្ទាល់ខ្លួននៃការបំបែក, ទែមីណាល, និងបន្ទះកម្មវិធីរុករក:",
|
||||||
|
"workspaceFields": "វាល workspace",
|
||||||
|
"wsFieldName": "ឈ្មោះផ្ទាំង workspace (លំនាំដើមគឺឈ្មោះពាក្យបញ្ជា)",
|
||||||
|
"wsFieldCwd": "ថតការងារសម្រាប់ workspace",
|
||||||
|
"wsFieldColor": "ពណ៌ផ្ទាំង workspace",
|
||||||
|
"wsFieldLayout": "ដើមប្លង់ដែលកំណត់ការបំបែកនិងបន្ទះ",
|
||||||
|
"restartBehavior": "ឥរិយាបថចាប់ផ្ដើមឡើងវិញ",
|
||||||
|
"restartBehaviorDesc": "គ្រប់គ្រងអ្វីដែលកើតឡើងនៅពេល workspace ដែលមានឈ្មោះដូចគ្នារួចមានស្រាប់:",
|
||||||
|
"restartIgnore": "ប្ដូរទៅ workspace ដែលមានស្រាប់ (លំនាំដើម)",
|
||||||
|
"restartRecreate": "បិទហើយបង្កើតឡើងវិញដោយមិនសួរ",
|
||||||
|
"restartConfirm": "សួរអ្នកប្រើប្រាស់មុននឹងបង្កើតឡើងវិញ",
|
||||||
|
"layoutTree": "ដើមប្លង់",
|
||||||
|
"layoutTreeDesc": "ដើមប្លង់កំណត់របៀបដែលបន្ទះត្រូវបានរៀបចំដោយប្រើថ្នាំងការបំបែករៀងគ្នា:",
|
||||||
|
"splitNode": "ថ្នាំងការបំបែក",
|
||||||
|
"splitNodeDesc": "ចែកចន្លោះទៅជាកូន២:",
|
||||||
|
"or": "ឬ",
|
||||||
|
"splitPosition": "ទីតាំងខ្សែបំបែកពី 0.1 ដល់ 0.9 (លំនាំដើម 0.5)",
|
||||||
|
"splitChildren": "ថ្នាំងកូនពិតប្រាកដ២ (ការបំបែក ឬបន្ទះ)",
|
||||||
|
"paneNode": "ថ្នាំងបន្ទះ",
|
||||||
|
"paneNodeDesc": "ថ្នាំងស្លឹកមួយដែលមាន surface មួយ ឬច្រើន (ផ្ទាំងនៅក្នុងបន្ទះ)។",
|
||||||
|
"surfaceDefinition": "និយាម surface",
|
||||||
|
"surfaceDefinitionDesc": "surface នីមួយៗក្នុងបន្ទះអាចជាទែមីណាល ឬកម្មវិធីរុករក:",
|
||||||
|
"surfaceName": "ចំណងជើងផ្ទាំងផ្ទាល់ខ្លួន",
|
||||||
|
"surfaceCommand": "ពាក្យបញ្ជា shell ដំណើរការដោយស្វ័យប្រវត្តិពេលបង្កើត (ទែមីណាលតែប៉ុណ្ណោះ)",
|
||||||
|
"surfaceCwd": "ថតការងារសម្រាប់ surface នេះ",
|
||||||
|
"surfaceEnv": "អថេរបរិស្ថានជាគូ key-value",
|
||||||
|
"surfaceUrl": "URL ដែលត្រូវបើក (កម្មវិធីរុករកតែប៉ុណ្ណោះ)",
|
||||||
|
"surfaceFocus": "ផ្ដោតលើ surface នេះបន្ទាប់ពីបង្កើត",
|
||||||
|
"cwdResolution": "ការដោះស្រាយថតការងារ",
|
||||||
|
"omitted": "លុបចោល",
|
||||||
|
"cwdRelative": "ថតការងារ workspace",
|
||||||
|
"cwdSubdir": "ទាក់ទងនឹងថតការងារ workspace",
|
||||||
|
"cwdHome": "ពង្រីកទៅថតផ្ទះ",
|
||||||
|
"absolutePath": "ផ្លូវដាច់ខាត",
|
||||||
|
"cwdAbsolute": "ប្រើដូចដែលមាន",
|
||||||
|
"fullExample": "ឧទាហរណ៍ពេញលេញ"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "ផ្លូវកាត់ក្ដារចុច",
|
"title": "ផ្លូវកាត់ក្ដារចុច",
|
||||||
"description": "ផ្លូវកាត់ក្ដារចុចទាំងអស់ដែលមានក្នុង cmux, ដាក់ជាក្រុមតាមប្រភេទ។",
|
"description": "ផ្លូវកាត់ក្ដារចុចទាំងអស់ដែលមានក្នុង cmux, ដាក់ជាក្រុមតាមប្រភេទ។",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "ចាប់ផ្ដើម",
|
"gettingStarted": "ចាប់ផ្ដើម",
|
||||||
"concepts": "គោលគំនិត",
|
"concepts": "គោលគំនិត",
|
||||||
"configuration": "កំណត់រចនាសម្ព័ន្ធ",
|
"configuration": "កំណត់រចនាសម្ព័ន្ធ",
|
||||||
|
"customCommands": "ពាក្យបញ្ជាផ្ទាល់ខ្លួន",
|
||||||
"keyboardShortcuts": "ផ្លូវកាត់ក្ដារចុច",
|
"keyboardShortcuts": "ផ្លូវកាត់ក្ដារចុច",
|
||||||
"apiReference": "ឯកសារយោង API",
|
"apiReference": "ឯកសារយោង API",
|
||||||
"browserAutomation": "ស្វ័យប្រវត្តិកម្មកម្មវិធីរុករក",
|
"browserAutomation": "ស្វ័យប្រវត្តិកម្មកម្មវិធីរុករក",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "예시 설정",
|
"exampleConfig": "예시 설정",
|
||||||
"metaTitle": "설정"
|
"metaTitle": "설정"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "사용자 정의 명령어",
|
||||||
|
"metaTitle": "사용자 정의 명령어",
|
||||||
|
"metaDescription": "cmux.json에서 사용자 정의 명령어와 워크스페이스 레이아웃을 정의합니다. 실시간 파일 감시와 함께 프로젝트별 및 전역 설정을 지원합니다.",
|
||||||
|
"intro": "프로젝트 루트 또는 ~/.config/cmux/에 cmux.json 파일을 추가하여 사용자 정의 명령어와 워크스페이스 레이아웃을 정의합니다. 명령어는 명령어 팔레트에 표시됩니다.",
|
||||||
|
"fileLocations": "파일 위치",
|
||||||
|
"fileLocationsDesc": "cmux는 두 곳에서 설정을 찾습니다:",
|
||||||
|
"localConfig": "프로젝트별:",
|
||||||
|
"localConfigDesc": "프로젝트 디렉터리에 위치하며 우선순위를 가집니다",
|
||||||
|
"globalConfig": "전역:",
|
||||||
|
"globalConfigDesc": "모든 프로젝트에 적용되며 로컬에서 정의되지 않은 명령어를 보완합니다",
|
||||||
|
"precedenceNote": "로컬 명령어는 동일한 이름의 전역 명령어를 덮어씁니다.",
|
||||||
|
"liveReload": "변경 사항은 자동으로 반영됩니다 — 재시작이 필요 없습니다.",
|
||||||
|
"schema": "스키마",
|
||||||
|
"schemaDesc": "cmux.json 파일에는 commands 배열이 포함됩니다. 각 명령어는 단순한 셸 명령어이거나 완전한 워크스페이스 정의입니다:",
|
||||||
|
"simpleCommands": "단순 명령어",
|
||||||
|
"simpleCommandsDesc": "단순 명령어는 현재 포커스된 터미널에서 셸 명령어를 실행합니다:",
|
||||||
|
"simpleCommandFields": "필드",
|
||||||
|
"fieldName": "명령어 팔레트에 표시됩니다 (필수)",
|
||||||
|
"fieldDescription": "선택적 설명",
|
||||||
|
"fieldKeywords": "명령어 팔레트용 추가 검색어",
|
||||||
|
"fieldCommand": "포커스된 터미널에서 실행할 셸 명령어",
|
||||||
|
"fieldConfirm": "실행 전 확인 대화상자 표시",
|
||||||
|
"simpleCommandCwdNote": "단순 명령어는 포커스된 터미널의 현재 작업 디렉토리에서 실행됩니다. 프로젝트 상대 경로에 의존하는 명령어의 경우 앞에",
|
||||||
|
"simpleCommandCwdRepoRoot": "를 붙여 저장소 루트에서 실행하거나",
|
||||||
|
"simpleCommandCwdCustomPath": "로 특정 디렉토리를 지정할 수 있습니다.",
|
||||||
|
"workspaceCommands": "워크스페이스 명령어",
|
||||||
|
"workspaceCommandsDesc": "워크스페이스 명령어는 분할, 터미널, 브라우저 패널의 사용자 정의 레이아웃으로 새 워크스페이스를 만듭니다:",
|
||||||
|
"workspaceFields": "워크스페이스 필드",
|
||||||
|
"wsFieldName": "워크스페이스 탭 이름 (기본값은 명령어 이름)",
|
||||||
|
"wsFieldCwd": "워크스페이스의 작업 디렉터리",
|
||||||
|
"wsFieldColor": "워크스페이스 탭 색상",
|
||||||
|
"wsFieldLayout": "분할과 패널을 정의하는 레이아웃 트리",
|
||||||
|
"restartBehavior": "재시작 동작",
|
||||||
|
"restartBehaviorDesc": "동일한 이름의 워크스페이스가 이미 존재할 때 발생하는 동작을 제어합니다:",
|
||||||
|
"restartIgnore": "기존 워크스페이스로 전환 (기본값)",
|
||||||
|
"restartRecreate": "묻지 않고 닫고 재생성",
|
||||||
|
"restartConfirm": "재생성 전 사용자에게 확인",
|
||||||
|
"layoutTree": "레이아웃 트리",
|
||||||
|
"layoutTreeDesc": "레이아웃 트리는 재귀적인 분할 노드를 사용하여 패널 배치를 정의합니다:",
|
||||||
|
"splitNode": "분할 노드",
|
||||||
|
"splitNodeDesc": "공간을 두 개의 자식으로 나눕니다:",
|
||||||
|
"or": "또는",
|
||||||
|
"splitPosition": "0.1에서 0.9 사이의 분할기 위치 (기본값 0.5)",
|
||||||
|
"splitChildren": "정확히 두 개의 자식 노드 (분할 또는 패널)",
|
||||||
|
"paneNode": "패널 노드",
|
||||||
|
"paneNodeDesc": "하나 이상의 서피스(패널 내 탭)를 포함하는 리프 노드.",
|
||||||
|
"surfaceDefinition": "서피스 정의",
|
||||||
|
"surfaceDefinitionDesc": "패널 내 각 서피스는 터미널 또는 브라우저가 될 수 있습니다:",
|
||||||
|
"surfaceName": "사용자 정의 탭 제목",
|
||||||
|
"surfaceCommand": "생성 시 자동 실행할 셸 명령어 (터미널 전용)",
|
||||||
|
"surfaceCwd": "이 서피스의 작업 디렉터리",
|
||||||
|
"surfaceEnv": "키-값 쌍으로 된 환경 변수",
|
||||||
|
"surfaceUrl": "열 URL (브라우저 전용)",
|
||||||
|
"surfaceFocus": "생성 후 이 서피스에 포커스",
|
||||||
|
"cwdResolution": "작업 디렉터리 해석",
|
||||||
|
"omitted": "생략됨",
|
||||||
|
"cwdRelative": "워크스페이스 작업 디렉터리",
|
||||||
|
"cwdSubdir": "워크스페이스 작업 디렉터리 기준 상대 경로",
|
||||||
|
"cwdHome": "홈 디렉터리로 확장",
|
||||||
|
"absolutePath": "절대 경로",
|
||||||
|
"cwdAbsolute": "있는 그대로 사용",
|
||||||
|
"fullExample": "전체 예시"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "키보드 단축키",
|
"title": "키보드 단축키",
|
||||||
"description": "카테고리별로 정리된 cmux의 모든 키보드 단축키.",
|
"description": "카테고리별로 정리된 cmux의 모든 키보드 단축키.",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "시작하기",
|
"gettingStarted": "시작하기",
|
||||||
"concepts": "개념",
|
"concepts": "개념",
|
||||||
"configuration": "설정",
|
"configuration": "설정",
|
||||||
|
"customCommands": "사용자 정의 명령어",
|
||||||
"keyboardShortcuts": "키보드 단축키",
|
"keyboardShortcuts": "키보드 단축키",
|
||||||
"apiReference": "API 레퍼런스",
|
"apiReference": "API 레퍼런스",
|
||||||
"browserAutomation": "브라우저 자동화",
|
"browserAutomation": "브라우저 자동화",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "Eksempelkonfigurasjon",
|
"exampleConfig": "Eksempelkonfigurasjon",
|
||||||
"metaTitle": "Konfigurasjon"
|
"metaTitle": "Konfigurasjon"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "Egendefinerte kommandoer",
|
||||||
|
"metaTitle": "Egendefinerte kommandoer",
|
||||||
|
"metaDescription": "Definer egendefinerte kommandoer og workspace-oppsett i cmux.json. Per-prosjekt og global konfigurasjon med live filovervåking.",
|
||||||
|
"intro": "Definer egendefinerte kommandoer og workspace-oppsett ved å legge til en cmux.json-fil i prosjektets rotmappe eller ~/.config/cmux/. Kommandoer vises i kommandopaletten.",
|
||||||
|
"fileLocations": "Filplasseringer",
|
||||||
|
"fileLocationsDesc": "cmux søker etter konfigurasjon to steder:",
|
||||||
|
"localConfig": "Per prosjekt:",
|
||||||
|
"localConfigDesc": "ligger i prosjektmappen din, har forrang",
|
||||||
|
"globalConfig": "Global:",
|
||||||
|
"globalConfigDesc": "gjelder alle prosjekter, fyller inn kommandoer som ikke er definert lokalt",
|
||||||
|
"precedenceNote": "Lokale kommandoer overstyrer globale kommandoer med samme navn.",
|
||||||
|
"liveReload": "Endringer hentes automatisk — ingen omstart nødvendig.",
|
||||||
|
"schema": "Skjema",
|
||||||
|
"schemaDesc": "En cmux.json-fil inneholder en commands-array. Hver kommando er enten en enkel shell-kommando eller en fullstendig workspace-definisjon:",
|
||||||
|
"simpleCommands": "Enkle kommandoer",
|
||||||
|
"simpleCommandsDesc": "En enkel kommando kjører en shell-kommando i den for øyeblikket fokuserte terminalen:",
|
||||||
|
"simpleCommandFields": "Felter",
|
||||||
|
"fieldName": "Vises i kommandopaletten (påkrevd)",
|
||||||
|
"fieldDescription": "Valgfri beskrivelse",
|
||||||
|
"fieldKeywords": "Ekstra søkeord for kommandopaletten",
|
||||||
|
"fieldCommand": "Shell-kommando som kjøres i den fokuserte terminalen",
|
||||||
|
"fieldConfirm": "Vis en bekreftelsesdialog før kjøring",
|
||||||
|
"simpleCommandCwdNote": "Enkle kommandoer kjøres i den fokuserte terminalens gjeldende arbeidskatalog. Hvis kommandoen din avhenger av prosjektrelative stier, legg til prefiks",
|
||||||
|
"simpleCommandCwdRepoRoot": "for å kjøre fra repo-roten, eller",
|
||||||
|
"simpleCommandCwdCustomPath": "for en hvilken som helst spesifikk katalog.",
|
||||||
|
"workspaceCommands": "Workspace-kommandoer",
|
||||||
|
"workspaceCommandsDesc": "En workspace-kommando oppretter et nytt workspace med et egendefinert oppsett av delinger, terminaler og nettleserpaneler:",
|
||||||
|
"workspaceFields": "Workspace-felter",
|
||||||
|
"wsFieldName": "Workspace-fanenavn (standard er kommandonavn)",
|
||||||
|
"wsFieldCwd": "Arbeidsmappe for workspacet",
|
||||||
|
"wsFieldColor": "Farge på workspace-fanen",
|
||||||
|
"wsFieldLayout": "Oppsettstre som definerer delinger og paneler",
|
||||||
|
"restartBehavior": "Omstartsatferd",
|
||||||
|
"restartBehaviorDesc": "Styrer hva som skjer når et workspace med samme navn allerede eksisterer:",
|
||||||
|
"restartIgnore": "Bytt til det eksisterende workspacet (standard)",
|
||||||
|
"restartRecreate": "Lukk og gjenopprett uten å spørre",
|
||||||
|
"restartConfirm": "Spør brukeren før gjenoppretting",
|
||||||
|
"layoutTree": "Oppsettstre",
|
||||||
|
"layoutTreeDesc": "Oppsettstreet definerer hvordan paneler er arrangert ved hjelp av rekursive delingsknuter:",
|
||||||
|
"splitNode": "Delingsknute",
|
||||||
|
"splitNodeDesc": "Deler plassen i to barn:",
|
||||||
|
"or": "eller",
|
||||||
|
"splitPosition": "Delerposisjon fra 0.1 til 0.9 (standard 0.5)",
|
||||||
|
"splitChildren": "Nøyaktig to barneknuter (deling eller panel)",
|
||||||
|
"paneNode": "Panelknute",
|
||||||
|
"paneNodeDesc": "En bladknute som inneholder én eller flere overflater (faner innen panelet).",
|
||||||
|
"surfaceDefinition": "Overflatedefinisjon",
|
||||||
|
"surfaceDefinitionDesc": "Hver overflate i et panel kan være en terminal eller en nettleser:",
|
||||||
|
"surfaceName": "Egendefinert fanetittel",
|
||||||
|
"surfaceCommand": "Shell-kommando som kjøres automatisk ved opprettelse (kun terminal)",
|
||||||
|
"surfaceCwd": "Arbeidsmappe for denne overflaten",
|
||||||
|
"surfaceEnv": "Miljøvariabler som nøkkel-verdi-par",
|
||||||
|
"surfaceUrl": "URL som skal åpnes (kun nettleser)",
|
||||||
|
"surfaceFocus": "Fokuser på denne overflaten etter opprettelse",
|
||||||
|
"cwdResolution": "Oppløsning av arbeidsmappe",
|
||||||
|
"omitted": "utelatt",
|
||||||
|
"cwdRelative": "workspace-arbeidsmappe",
|
||||||
|
"cwdSubdir": "relativ til workspace-arbeidsmappen",
|
||||||
|
"cwdHome": "utvidet til hjemmemappen",
|
||||||
|
"absolutePath": "Absolutt sti",
|
||||||
|
"cwdAbsolute": "brukes som den er",
|
||||||
|
"fullExample": "Fullstendig eksempel"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Tastatursnarveier",
|
"title": "Tastatursnarveier",
|
||||||
"description": "Alle tastatursnarveier tilgjengelige i cmux, gruppert etter kategori.",
|
"description": "Alle tastatursnarveier tilgjengelige i cmux, gruppert etter kategori.",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "Kom i gang",
|
"gettingStarted": "Kom i gang",
|
||||||
"concepts": "Konsepter",
|
"concepts": "Konsepter",
|
||||||
"configuration": "Konfigurasjon",
|
"configuration": "Konfigurasjon",
|
||||||
|
"customCommands": "Egendefinerte kommandoer",
|
||||||
"keyboardShortcuts": "Tastatursnarveier",
|
"keyboardShortcuts": "Tastatursnarveier",
|
||||||
"apiReference": "API-referanse",
|
"apiReference": "API-referanse",
|
||||||
"browserAutomation": "Nettleserautomatisering",
|
"browserAutomation": "Nettleserautomatisering",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "Przykładowa konfiguracja",
|
"exampleConfig": "Przykładowa konfiguracja",
|
||||||
"metaTitle": "Konfiguracja"
|
"metaTitle": "Konfiguracja"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "Niestandardowe polecenia",
|
||||||
|
"metaTitle": "Niestandardowe polecenia",
|
||||||
|
"metaDescription": "Definiuj niestandardowe polecenia i układy workspace w cmux.json. Konfiguracja per-projekt i globalna z obserwacją plików na żywo.",
|
||||||
|
"intro": "Definiuj niestandardowe polecenia i układy workspace, dodając plik cmux.json do katalogu głównego projektu lub ~/.config/cmux/. Polecenia pojawiają się w palecie poleceń.",
|
||||||
|
"fileLocations": "Lokalizacje plików",
|
||||||
|
"fileLocationsDesc": "cmux szuka konfiguracji w dwóch miejscach:",
|
||||||
|
"localConfig": "Per projekt:",
|
||||||
|
"localConfigDesc": "znajduje się w katalogu projektu, ma pierwszeństwo",
|
||||||
|
"globalConfig": "Globalnie:",
|
||||||
|
"globalConfigDesc": "stosuje się do wszystkich projektów, uzupełnia polecenia niezdefiniowane lokalnie",
|
||||||
|
"precedenceNote": "Lokalne polecenia zastępują globalne polecenia o tej samej nazwie.",
|
||||||
|
"liveReload": "Zmiany są pobierane automatycznie — nie trzeba restartować.",
|
||||||
|
"schema": "Schemat",
|
||||||
|
"schemaDesc": "Plik cmux.json zawiera tablicę commands. Każde polecenie to albo proste polecenie shell, albo pełna definicja workspace:",
|
||||||
|
"simpleCommands": "Proste polecenia",
|
||||||
|
"simpleCommandsDesc": "Proste polecenie uruchamia polecenie shell w aktualnie sfokusowanym terminalu:",
|
||||||
|
"simpleCommandFields": "Pola",
|
||||||
|
"fieldName": "Wyświetlane w palecie poleceń (wymagane)",
|
||||||
|
"fieldDescription": "Opcjonalny opis",
|
||||||
|
"fieldKeywords": "Dodatkowe terminy wyszukiwania dla palety poleceń",
|
||||||
|
"fieldCommand": "Polecenie shell do uruchomienia w sfokusowanym terminalu",
|
||||||
|
"fieldConfirm": "Pokaż okno dialogowe potwierdzenia przed uruchomieniem",
|
||||||
|
"simpleCommandCwdNote": "Proste polecenia są uruchamiane w bieżącym katalogu roboczym aktywnego terminala. Jeśli polecenie wymaga ścieżek względnych do projektu, dodaj prefiks",
|
||||||
|
"simpleCommandCwdRepoRoot": "aby uruchomić z katalogu głównego repozytorium, lub",
|
||||||
|
"simpleCommandCwdCustomPath": "dla dowolnego konkretnego katalogu.",
|
||||||
|
"workspaceCommands": "Polecenia workspace",
|
||||||
|
"workspaceCommandsDesc": "Polecenie workspace tworzy nowy workspace z niestandardowym układem podziałów, terminali i paneli przeglądarki:",
|
||||||
|
"workspaceFields": "Pola workspace",
|
||||||
|
"wsFieldName": "Nazwa zakładki workspace (domyślnie nazwa polecenia)",
|
||||||
|
"wsFieldCwd": "Katalog roboczy workspace",
|
||||||
|
"wsFieldColor": "Kolor zakładki workspace",
|
||||||
|
"wsFieldLayout": "Drzewo układu definiujące podziały i panele",
|
||||||
|
"restartBehavior": "Zachowanie przy restarcie",
|
||||||
|
"restartBehaviorDesc": "Kontroluje co się dzieje, gdy workspace o tej samej nazwie już istnieje:",
|
||||||
|
"restartIgnore": "Przełącz do istniejącego workspace (domyślnie)",
|
||||||
|
"restartRecreate": "Zamknij i odtwórz bez pytania",
|
||||||
|
"restartConfirm": "Zapytaj użytkownika przed odtworzeniem",
|
||||||
|
"layoutTree": "Drzewo układu",
|
||||||
|
"layoutTreeDesc": "Drzewo układu definiuje jak panele są rozmieszczone za pomocą rekurencyjnych węzłów podziału:",
|
||||||
|
"splitNode": "Węzeł podziału",
|
||||||
|
"splitNodeDesc": "Dzieli przestrzeń na dwoje dzieci:",
|
||||||
|
"or": "lub",
|
||||||
|
"splitPosition": "Pozycja dzielnika od 0.1 do 0.9 (domyślnie 0.5)",
|
||||||
|
"splitChildren": "Dokładnie dwa węzły podrzędne (podział lub panel)",
|
||||||
|
"paneNode": "Węzeł panelu",
|
||||||
|
"paneNodeDesc": "Węzeł liścia zawierający jedną lub więcej powierzchni (zakładki wewnątrz panelu).",
|
||||||
|
"surfaceDefinition": "Definicja powierzchni",
|
||||||
|
"surfaceDefinitionDesc": "Każda powierzchnia w panelu może być terminalem lub przeglądarką:",
|
||||||
|
"surfaceName": "Niestandardowy tytuł zakładki",
|
||||||
|
"surfaceCommand": "Polecenie shell do automatycznego uruchomienia przy tworzeniu (tylko terminal)",
|
||||||
|
"surfaceCwd": "Katalog roboczy dla tej powierzchni",
|
||||||
|
"surfaceEnv": "Zmienne środowiskowe jako pary klucz-wartość",
|
||||||
|
"surfaceUrl": "URL do otwarcia (tylko przeglądarka)",
|
||||||
|
"surfaceFocus": "Sfokusuj tę powierzchnię po utworzeniu",
|
||||||
|
"cwdResolution": "Rozwiązanie katalogu roboczego",
|
||||||
|
"omitted": "pominięty",
|
||||||
|
"cwdRelative": "katalog roboczy workspace",
|
||||||
|
"cwdSubdir": "względem katalogu roboczego workspace",
|
||||||
|
"cwdHome": "rozwinięty do katalogu domowego",
|
||||||
|
"absolutePath": "Ścieżka bezwzględna",
|
||||||
|
"cwdAbsolute": "używana bez zmian",
|
||||||
|
"fullExample": "Pełny przykład"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Skróty klawiszowe",
|
"title": "Skróty klawiszowe",
|
||||||
"description": "Wszystkie skróty klawiszowe dostępne w cmux, pogrupowane według kategorii.",
|
"description": "Wszystkie skróty klawiszowe dostępne w cmux, pogrupowane według kategorii.",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "Szybki start",
|
"gettingStarted": "Szybki start",
|
||||||
"concepts": "Koncepty",
|
"concepts": "Koncepty",
|
||||||
"configuration": "Konfiguracja",
|
"configuration": "Konfiguracja",
|
||||||
|
"customCommands": "Niestandardowe polecenia",
|
||||||
"keyboardShortcuts": "Skróty klawiszowe",
|
"keyboardShortcuts": "Skróty klawiszowe",
|
||||||
"apiReference": "Dokumentacja API",
|
"apiReference": "Dokumentacja API",
|
||||||
"browserAutomation": "Automatyzacja przeglądarki",
|
"browserAutomation": "Automatyzacja przeglądarki",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "Exemplo de configuração",
|
"exampleConfig": "Exemplo de configuração",
|
||||||
"metaTitle": "Configuração"
|
"metaTitle": "Configuração"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "Comandos personalizados",
|
||||||
|
"metaTitle": "Comandos personalizados",
|
||||||
|
"metaDescription": "Defina comandos personalizados e layouts de workspace em cmux.json. Configuração por projeto e global com monitoramento em tempo real de arquivos.",
|
||||||
|
"intro": "Defina comandos personalizados e layouts de workspace adicionando um arquivo cmux.json à raiz do seu projeto ou ~/.config/cmux/. Os comandos aparecem na paleta de comandos.",
|
||||||
|
"fileLocations": "Localizações dos arquivos",
|
||||||
|
"fileLocationsDesc": "O cmux procura configuração em dois lugares:",
|
||||||
|
"localConfig": "Por projeto:",
|
||||||
|
"localConfigDesc": "fica no diretório do seu projeto, tem precedência",
|
||||||
|
"globalConfig": "Global:",
|
||||||
|
"globalConfigDesc": "aplica-se a todos os projetos, preenche comandos não definidos localmente",
|
||||||
|
"precedenceNote": "Comandos locais substituem comandos globais com o mesmo nome.",
|
||||||
|
"liveReload": "As alterações são capturadas automaticamente — nenhum reinício necessário.",
|
||||||
|
"schema": "Esquema",
|
||||||
|
"schemaDesc": "Um arquivo cmux.json contém um array commands. Cada comando é um comando shell simples ou uma definição completa de workspace:",
|
||||||
|
"simpleCommands": "Comandos simples",
|
||||||
|
"simpleCommandsDesc": "Um comando simples executa um comando shell no terminal atualmente focado:",
|
||||||
|
"simpleCommandFields": "Campos",
|
||||||
|
"fieldName": "Exibido na paleta de comandos (obrigatório)",
|
||||||
|
"fieldDescription": "Descrição opcional",
|
||||||
|
"fieldKeywords": "Termos de pesquisa extras para a paleta de comandos",
|
||||||
|
"fieldCommand": "Comando shell para executar no terminal focado",
|
||||||
|
"fieldConfirm": "Mostrar um diálogo de confirmação antes de executar",
|
||||||
|
"simpleCommandCwdNote": "Comandos simples são executados no diretório de trabalho atual do terminal focado. Se seu comando depende de caminhos relativos ao projeto, prefixe com",
|
||||||
|
"simpleCommandCwdRepoRoot": "para executar a partir da raiz do repositório, ou",
|
||||||
|
"simpleCommandCwdCustomPath": "para qualquer diretório específico.",
|
||||||
|
"workspaceCommands": "Comandos de workspace",
|
||||||
|
"workspaceCommandsDesc": "Um comando de workspace cria um novo workspace com um layout personalizado de divisões, terminais e painéis do navegador:",
|
||||||
|
"workspaceFields": "Campos de workspace",
|
||||||
|
"wsFieldName": "Nome da aba do workspace (padrão é o nome do comando)",
|
||||||
|
"wsFieldCwd": "Diretório de trabalho do workspace",
|
||||||
|
"wsFieldColor": "Cor da aba do workspace",
|
||||||
|
"wsFieldLayout": "Árvore de layout definindo divisões e painéis",
|
||||||
|
"restartBehavior": "Comportamento de reinício",
|
||||||
|
"restartBehaviorDesc": "Controla o que acontece quando um workspace com o mesmo nome já existe:",
|
||||||
|
"restartIgnore": "Mudar para o workspace existente (padrão)",
|
||||||
|
"restartRecreate": "Fechar e recriar sem perguntar",
|
||||||
|
"restartConfirm": "Perguntar ao usuário antes de recriar",
|
||||||
|
"layoutTree": "Árvore de layout",
|
||||||
|
"layoutTreeDesc": "A árvore de layout define como os painéis são organizados usando nós de divisão recursivos:",
|
||||||
|
"splitNode": "Nó de divisão",
|
||||||
|
"splitNodeDesc": "Divide o espaço em dois filhos:",
|
||||||
|
"or": "ou",
|
||||||
|
"splitPosition": "Posição do divisor de 0.1 a 0.9 (padrão 0.5)",
|
||||||
|
"splitChildren": "Exatamente dois nós filhos (divisão ou painel)",
|
||||||
|
"paneNode": "Nó de painel",
|
||||||
|
"paneNodeDesc": "Um nó folha contendo uma ou mais superfícies (abas dentro do painel).",
|
||||||
|
"surfaceDefinition": "Definição de superfície",
|
||||||
|
"surfaceDefinitionDesc": "Cada superfície em um painel pode ser um terminal ou um navegador:",
|
||||||
|
"surfaceName": "Título de aba personalizado",
|
||||||
|
"surfaceCommand": "Comando shell para executar automaticamente na criação (apenas terminal)",
|
||||||
|
"surfaceCwd": "Diretório de trabalho para esta superfície",
|
||||||
|
"surfaceEnv": "Variáveis de ambiente como pares chave-valor",
|
||||||
|
"surfaceUrl": "URL para abrir (apenas navegador)",
|
||||||
|
"surfaceFocus": "Focar nesta superfície após a criação",
|
||||||
|
"cwdResolution": "Resolução do diretório de trabalho",
|
||||||
|
"omitted": "omitido",
|
||||||
|
"cwdRelative": "diretório de trabalho do workspace",
|
||||||
|
"cwdSubdir": "relativo ao diretório de trabalho do workspace",
|
||||||
|
"cwdHome": "expandido para o diretório home",
|
||||||
|
"absolutePath": "Caminho absoluto",
|
||||||
|
"cwdAbsolute": "usado como está",
|
||||||
|
"fullExample": "Exemplo completo"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Atalhos de Teclado",
|
"title": "Atalhos de Teclado",
|
||||||
"description": "Todos os atalhos de teclado disponíveis no cmux, agrupados por categoria.",
|
"description": "Todos os atalhos de teclado disponíveis no cmux, agrupados por categoria.",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "Primeiros Passos",
|
"gettingStarted": "Primeiros Passos",
|
||||||
"concepts": "Conceitos",
|
"concepts": "Conceitos",
|
||||||
"configuration": "Configuração",
|
"configuration": "Configuração",
|
||||||
|
"customCommands": "Comandos personalizados",
|
||||||
"keyboardShortcuts": "Atalhos de Teclado",
|
"keyboardShortcuts": "Atalhos de Teclado",
|
||||||
"apiReference": "Referência da API",
|
"apiReference": "Referência da API",
|
||||||
"browserAutomation": "Automação do Navegador",
|
"browserAutomation": "Automação do Navegador",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "Пример конфигурации",
|
"exampleConfig": "Пример конфигурации",
|
||||||
"metaTitle": "Конфигурация"
|
"metaTitle": "Конфигурация"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "Пользовательские команды",
|
||||||
|
"metaTitle": "Пользовательские команды",
|
||||||
|
"metaDescription": "Определяйте пользовательские команды и макеты рабочего пространства в cmux.json. Конфигурация для каждого проекта и глобальная с живым отслеживанием файлов.",
|
||||||
|
"intro": "Определяйте пользовательские команды и макеты рабочего пространства, добавив файл cmux.json в корень вашего проекта или ~/.config/cmux/. Команды отображаются в палитре команд.",
|
||||||
|
"fileLocations": "Расположение файлов",
|
||||||
|
"fileLocationsDesc": "cmux ищет конфигурацию в двух местах:",
|
||||||
|
"localConfig": "Для проекта:",
|
||||||
|
"localConfigDesc": "находится в каталоге проекта, имеет приоритет",
|
||||||
|
"globalConfig": "Глобально:",
|
||||||
|
"globalConfigDesc": "применяется ко всем проектам, дополняет команды, не определённые локально",
|
||||||
|
"precedenceNote": "Локальные команды переопределяют глобальные команды с тем же именем.",
|
||||||
|
"liveReload": "Изменения применяются автоматически — перезапуск не требуется.",
|
||||||
|
"schema": "Схема",
|
||||||
|
"schemaDesc": "Файл cmux.json содержит массив commands. Каждая команда — это либо простая команда shell, либо полное определение рабочего пространства:",
|
||||||
|
"simpleCommands": "Простые команды",
|
||||||
|
"simpleCommandsDesc": "Простая команда выполняет команду shell в текущем сфокусированном терминале:",
|
||||||
|
"simpleCommandFields": "Поля",
|
||||||
|
"fieldName": "Отображается в палитре команд (обязательно)",
|
||||||
|
"fieldDescription": "Необязательное описание",
|
||||||
|
"fieldKeywords": "Дополнительные поисковые термины для палитры команд",
|
||||||
|
"fieldCommand": "Команда shell для выполнения в сфокусированном терминале",
|
||||||
|
"fieldConfirm": "Показать диалог подтверждения перед выполнением",
|
||||||
|
"simpleCommandCwdNote": "Простые команды выполняются в текущей рабочей директории активного терминала. Если команда использует пути относительно проекта, добавьте префикс",
|
||||||
|
"simpleCommandCwdRepoRoot": "для запуска из корня репозитория, или",
|
||||||
|
"simpleCommandCwdCustomPath": "для любой конкретной директории.",
|
||||||
|
"workspaceCommands": "Команды рабочего пространства",
|
||||||
|
"workspaceCommandsDesc": "Команда рабочего пространства создаёт новое рабочее пространство с настраиваемым макетом из разделений, терминалов и панелей браузера:",
|
||||||
|
"workspaceFields": "Поля рабочего пространства",
|
||||||
|
"wsFieldName": "Имя вкладки рабочего пространства (по умолчанию — имя команды)",
|
||||||
|
"wsFieldCwd": "Рабочий каталог для рабочего пространства",
|
||||||
|
"wsFieldColor": "Цвет вкладки рабочего пространства",
|
||||||
|
"wsFieldLayout": "Дерево макета, определяющее разделения и панели",
|
||||||
|
"restartBehavior": "Поведение при перезапуске",
|
||||||
|
"restartBehaviorDesc": "Управляет тем, что происходит, когда рабочее пространство с тем же именем уже существует:",
|
||||||
|
"restartIgnore": "Переключиться на существующее рабочее пространство (по умолчанию)",
|
||||||
|
"restartRecreate": "Закрыть и пересоздать без запроса",
|
||||||
|
"restartConfirm": "Спросить пользователя перед пересозданием",
|
||||||
|
"layoutTree": "Дерево макета",
|
||||||
|
"layoutTreeDesc": "Дерево макета определяет расположение панелей с помощью рекурсивных узлов разделения:",
|
||||||
|
"splitNode": "Узел разделения",
|
||||||
|
"splitNodeDesc": "Делит пространство на двух потомков:",
|
||||||
|
"or": "или",
|
||||||
|
"splitPosition": "Положение разделителя от 0.1 до 0.9 (по умолчанию 0.5)",
|
||||||
|
"splitChildren": "Ровно два дочерних узла (разделение или панель)",
|
||||||
|
"paneNode": "Узел панели",
|
||||||
|
"paneNodeDesc": "Листовой узел, содержащий одну или несколько поверхностей (вкладки внутри панели).",
|
||||||
|
"surfaceDefinition": "Определение поверхности",
|
||||||
|
"surfaceDefinitionDesc": "Каждая поверхность в панели может быть терминалом или браузером:",
|
||||||
|
"surfaceName": "Пользовательский заголовок вкладки",
|
||||||
|
"surfaceCommand": "Команда shell для автоматического запуска при создании (только терминал)",
|
||||||
|
"surfaceCwd": "Рабочий каталог для этой поверхности",
|
||||||
|
"surfaceEnv": "Переменные среды в виде пар ключ-значение",
|
||||||
|
"surfaceUrl": "URL для открытия (только браузер)",
|
||||||
|
"surfaceFocus": "Сфокусироваться на этой поверхности после создания",
|
||||||
|
"cwdResolution": "Определение рабочего каталога",
|
||||||
|
"omitted": "опущено",
|
||||||
|
"cwdRelative": "рабочий каталог рабочего пространства",
|
||||||
|
"cwdSubdir": "относительно рабочего каталога рабочего пространства",
|
||||||
|
"cwdHome": "расширено до домашнего каталога",
|
||||||
|
"absolutePath": "Абсолютный путь",
|
||||||
|
"cwdAbsolute": "используется как есть",
|
||||||
|
"fullExample": "Полный пример"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Горячие клавиши",
|
"title": "Горячие клавиши",
|
||||||
"description": "Все горячие клавиши в cmux, сгруппированные по категориям.",
|
"description": "Все горячие клавиши в cmux, сгруппированные по категориям.",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "Начало работы",
|
"gettingStarted": "Начало работы",
|
||||||
"concepts": "Концепции",
|
"concepts": "Концепции",
|
||||||
"configuration": "Конфигурация",
|
"configuration": "Конфигурация",
|
||||||
|
"customCommands": "Пользовательские команды",
|
||||||
"keyboardShortcuts": "Горячие клавиши",
|
"keyboardShortcuts": "Горячие клавиши",
|
||||||
"apiReference": "Справочник API",
|
"apiReference": "Справочник API",
|
||||||
"browserAutomation": "Автоматизация браузера",
|
"browserAutomation": "Автоматизация браузера",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "ตัวอย่างคอนฟิก",
|
"exampleConfig": "ตัวอย่างคอนฟิก",
|
||||||
"metaTitle": "การตั้งค่า"
|
"metaTitle": "การตั้งค่า"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "คำสั่งที่กำหนดเอง",
|
||||||
|
"metaTitle": "คำสั่งที่กำหนดเอง",
|
||||||
|
"metaDescription": "กำหนดคำสั่งที่กำหนดเองและเลย์เอาต์ workspace ใน cmux.json รองรับการตั้งค่าแบบต่อโปรเจกต์และแบบทั่วไปพร้อมการตรวจสอบไฟล์แบบเรียลไทม์",
|
||||||
|
"intro": "กำหนดคำสั่งที่กำหนดเองและเลย์เอาต์ workspace โดยเพิ่มไฟล์ cmux.json ลงในรากโปรเจกต์หรือ ~/.config/cmux/ คำสั่งจะปรากฏในแผงคำสั่ง",
|
||||||
|
"fileLocations": "ตำแหน่งไฟล์",
|
||||||
|
"fileLocationsDesc": "cmux ค้นหาการตั้งค่าจากสองที่:",
|
||||||
|
"localConfig": "ต่อโปรเจกต์:",
|
||||||
|
"localConfigDesc": "อยู่ในไดเรกทอรีโปรเจกต์ของคุณ มีความสำคัญสูงกว่า",
|
||||||
|
"globalConfig": "ทั่วไป:",
|
||||||
|
"globalConfigDesc": "ใช้กับทุกโปรเจกต์ เติมคำสั่งที่ยังไม่ได้กำหนดในท้องถิ่น",
|
||||||
|
"precedenceNote": "คำสั่งท้องถิ่นจะแทนที่คำสั่งทั่วไปที่มีชื่อเดียวกัน",
|
||||||
|
"liveReload": "การเปลี่ยนแปลงจะถูกรับโดยอัตโนมัติ — ไม่จำเป็นต้องรีสตาร์ท",
|
||||||
|
"schema": "สคีมา",
|
||||||
|
"schemaDesc": "ไฟล์ cmux.json มีอาร์เรย์ commands แต่ละคำสั่งเป็นคำสั่ง shell แบบง่ายหรือนิยาม workspace แบบเต็ม:",
|
||||||
|
"simpleCommands": "คำสั่งแบบง่าย",
|
||||||
|
"simpleCommandsDesc": "คำสั่งแบบง่ายรันคำสั่ง shell ในเทอร์มินัลที่กำลังโฟกัสอยู่:",
|
||||||
|
"simpleCommandFields": "ฟิลด์",
|
||||||
|
"fieldName": "แสดงในแผงคำสั่ง (จำเป็น)",
|
||||||
|
"fieldDescription": "คำอธิบายเพิ่มเติม (ไม่บังคับ)",
|
||||||
|
"fieldKeywords": "คำค้นหาเพิ่มเติมสำหรับแผงคำสั่ง",
|
||||||
|
"fieldCommand": "คำสั่ง shell ที่จะรันในเทอร์มินัลที่โฟกัส",
|
||||||
|
"fieldConfirm": "แสดงกล่องโต้ตอบยืนยันก่อนรัน",
|
||||||
|
"simpleCommandCwdNote": "คำสั่งแบบง่ายจะรันในไดเรกทอรีการทำงานปัจจุบันของเทอร์มินัลที่โฟกัสอยู่ หากคำสั่งของคุณใช้พาธสัมพัทธ์กับโปรเจกต์ ให้เพิ่มนำหน้าด้วย",
|
||||||
|
"simpleCommandCwdRepoRoot": "เพื่อรันจากรูทของ repo หรือ",
|
||||||
|
"simpleCommandCwdCustomPath": "สำหรับไดเรกทอรีที่ต้องการ",
|
||||||
|
"workspaceCommands": "คำสั่ง workspace",
|
||||||
|
"workspaceCommandsDesc": "คำสั่ง workspace สร้าง workspace ใหม่ที่มีเลย์เอาต์กำหนดเองของการแบ่ง เทอร์มินัล และแผงเบราว์เซอร์:",
|
||||||
|
"workspaceFields": "ฟิลด์ workspace",
|
||||||
|
"wsFieldName": "ชื่อแท็บ workspace (ค่าเริ่มต้นคือชื่อคำสั่ง)",
|
||||||
|
"wsFieldCwd": "ไดเรกทอรีทำงานสำหรับ workspace",
|
||||||
|
"wsFieldColor": "สีแท็บ workspace",
|
||||||
|
"wsFieldLayout": "ต้นไม้เลย์เอาต์ที่กำหนดการแบ่งและแผง",
|
||||||
|
"restartBehavior": "พฤติกรรมการรีสตาร์ท",
|
||||||
|
"restartBehaviorDesc": "ควบคุมสิ่งที่เกิดขึ้นเมื่อ workspace ที่มีชื่อเดียวกันมีอยู่แล้ว:",
|
||||||
|
"restartIgnore": "สลับไปยัง workspace ที่มีอยู่ (ค่าเริ่มต้น)",
|
||||||
|
"restartRecreate": "ปิดและสร้างใหม่โดยไม่ถาม",
|
||||||
|
"restartConfirm": "ถามผู้ใช้ก่อนสร้างใหม่",
|
||||||
|
"layoutTree": "ต้นไม้เลย์เอาต์",
|
||||||
|
"layoutTreeDesc": "ต้นไม้เลย์เอาต์กำหนดวิธีจัดเรียงแผงโดยใช้โหนดการแบ่งแบบวนซ้ำ:",
|
||||||
|
"splitNode": "โหนดการแบ่ง",
|
||||||
|
"splitNodeDesc": "แบ่งพื้นที่ออกเป็นสองส่วน:",
|
||||||
|
"or": "หรือ",
|
||||||
|
"splitPosition": "ตำแหน่งตัวแบ่งตั้งแต่ 0.1 ถึง 0.9 (ค่าเริ่มต้น 0.5)",
|
||||||
|
"splitChildren": "โหนดลูกสองโหนดพอดี (การแบ่งหรือแผง)",
|
||||||
|
"paneNode": "โหนดแผง",
|
||||||
|
"paneNodeDesc": "โหนดใบที่มี surface หนึ่งอันหรือมากกว่า (แท็บภายในแผง)",
|
||||||
|
"surfaceDefinition": "นิยาม surface",
|
||||||
|
"surfaceDefinitionDesc": "แต่ละ surface ในแผงสามารถเป็นเทอร์มินัลหรือเบราว์เซอร์:",
|
||||||
|
"surfaceName": "ชื่อแท็บที่กำหนดเอง",
|
||||||
|
"surfaceCommand": "คำสั่ง shell ที่รันอัตโนมัติเมื่อสร้าง (เฉพาะเทอร์มินัล)",
|
||||||
|
"surfaceCwd": "ไดเรกทอรีทำงานสำหรับ surface นี้",
|
||||||
|
"surfaceEnv": "ตัวแปรสภาพแวดล้อมในรูปแบบคู่คีย์-ค่า",
|
||||||
|
"surfaceUrl": "URL ที่จะเปิด (เฉพาะเบราว์เซอร์)",
|
||||||
|
"surfaceFocus": "โฟกัสที่ surface นี้หลังสร้าง",
|
||||||
|
"cwdResolution": "การแก้ไขไดเรกทอรีทำงาน",
|
||||||
|
"omitted": "ละไว้",
|
||||||
|
"cwdRelative": "ไดเรกทอรีทำงานของ workspace",
|
||||||
|
"cwdSubdir": "สัมพัทธ์กับไดเรกทอรีทำงานของ workspace",
|
||||||
|
"cwdHome": "ขยายไปยังไดเรกทอรีหลัก",
|
||||||
|
"absolutePath": "พาธสัมบูรณ์",
|
||||||
|
"cwdAbsolute": "ใช้ตามที่เป็น",
|
||||||
|
"fullExample": "ตัวอย่างเต็ม"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "คีย์ลัด",
|
"title": "คีย์ลัด",
|
||||||
"description": "คีย์ลัดทั้งหมดใน cmux จัดกลุ่มตามหมวดหมู่",
|
"description": "คีย์ลัดทั้งหมดใน cmux จัดกลุ่มตามหมวดหมู่",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "เริ่มต้นใช้งาน",
|
"gettingStarted": "เริ่มต้นใช้งาน",
|
||||||
"concepts": "แนวคิด",
|
"concepts": "แนวคิด",
|
||||||
"configuration": "การตั้งค่า",
|
"configuration": "การตั้งค่า",
|
||||||
|
"customCommands": "คำสั่งที่กำหนดเอง",
|
||||||
"keyboardShortcuts": "คีย์ลัด",
|
"keyboardShortcuts": "คีย์ลัด",
|
||||||
"apiReference": "เอกสาร API",
|
"apiReference": "เอกสาร API",
|
||||||
"browserAutomation": "Browser Automation",
|
"browserAutomation": "Browser Automation",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "Örnek yapılandırma",
|
"exampleConfig": "Örnek yapılandırma",
|
||||||
"metaTitle": "Yapılandırma"
|
"metaTitle": "Yapılandırma"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "Özel Komutlar",
|
||||||
|
"metaTitle": "Özel Komutlar",
|
||||||
|
"metaDescription": "cmux.json'da özel komutlar ve workspace düzenleri tanımlayın. Canlı dosya izleme ile proje başına ve genel yapılandırma.",
|
||||||
|
"intro": "Proje köküne veya ~/.config/cmux/ dizinine bir cmux.json dosyası ekleyerek özel komutlar ve workspace düzenleri tanımlayın. Komutlar komut paletinde görünür.",
|
||||||
|
"fileLocations": "Dosya konumları",
|
||||||
|
"fileLocationsDesc": "cmux yapılandırmayı iki yerde arar:",
|
||||||
|
"localConfig": "Proje başına:",
|
||||||
|
"localConfigDesc": "proje dizininde bulunur, önceliğe sahiptir",
|
||||||
|
"globalConfig": "Global:",
|
||||||
|
"globalConfigDesc": "tüm projeler için geçerlidir, yerel olarak tanımlanmayan komutları tamamlar",
|
||||||
|
"precedenceNote": "Yerel komutlar, aynı adlı global komutları geçersiz kılar.",
|
||||||
|
"liveReload": "Değişiklikler otomatik olarak algılanır — yeniden başlatma gerekmez.",
|
||||||
|
"schema": "Şema",
|
||||||
|
"schemaDesc": "Bir cmux.json dosyası bir commands dizisi içerir. Her komut ya basit bir shell komutu ya da tam bir workspace tanımıdır:",
|
||||||
|
"simpleCommands": "Basit komutlar",
|
||||||
|
"simpleCommandsDesc": "Basit bir komut, o an odaklanılan terminalde bir shell komutu çalıştırır:",
|
||||||
|
"simpleCommandFields": "Alanlar",
|
||||||
|
"fieldName": "Komut paletinde gösterilir (zorunlu)",
|
||||||
|
"fieldDescription": "İsteğe bağlı açıklama",
|
||||||
|
"fieldKeywords": "Komut paleti için ekstra arama terimleri",
|
||||||
|
"fieldCommand": "Odaklanılan terminalde çalıştırılacak shell komutu",
|
||||||
|
"fieldConfirm": "Çalıştırmadan önce onay iletişim kutusu göster",
|
||||||
|
"simpleCommandCwdNote": "Basit komutlar, odaklanan terminalin mevcut çalışma dizininde çalıştırılır. Komutunuz projeye göreceli yollar gerektiriyorsa, başına",
|
||||||
|
"simpleCommandCwdRepoRoot": "ekleyerek repo kökünden çalıştırabilir veya",
|
||||||
|
"simpleCommandCwdCustomPath": "ile herhangi bir dizini belirtebilirsiniz.",
|
||||||
|
"workspaceCommands": "Workspace komutları",
|
||||||
|
"workspaceCommandsDesc": "Bir workspace komutu, bölünmeler, terminaller ve tarayıcı panellerinin özel düzeniyle yeni bir workspace oluşturur:",
|
||||||
|
"workspaceFields": "Workspace alanları",
|
||||||
|
"wsFieldName": "Workspace sekme adı (varsayılan komut adıdır)",
|
||||||
|
"wsFieldCwd": "Workspace için çalışma dizini",
|
||||||
|
"wsFieldColor": "Workspace sekme rengi",
|
||||||
|
"wsFieldLayout": "Bölünmeleri ve panelleri tanımlayan düzen ağacı",
|
||||||
|
"restartBehavior": "Yeniden başlatma davranışı",
|
||||||
|
"restartBehaviorDesc": "Aynı adda bir workspace zaten mevcut olduğunda ne olacağını kontrol eder:",
|
||||||
|
"restartIgnore": "Mevcut workspace'e geç (varsayılan)",
|
||||||
|
"restartRecreate": "Sormadan kapat ve yeniden oluştur",
|
||||||
|
"restartConfirm": "Yeniden oluşturmadan önce kullanıcıya sor",
|
||||||
|
"layoutTree": "Düzen ağacı",
|
||||||
|
"layoutTreeDesc": "Düzen ağacı, özyinelemeli bölünme düğümleri kullanarak panellerin nasıl düzenlendiğini tanımlar:",
|
||||||
|
"splitNode": "Bölünme düğümü",
|
||||||
|
"splitNodeDesc": "Alanı iki alt öğeye böler:",
|
||||||
|
"or": "veya",
|
||||||
|
"splitPosition": "0.1'den 0.9'a bölücü konumu (varsayılan 0.5)",
|
||||||
|
"splitChildren": "Tam olarak iki alt düğüm (bölünme veya panel)",
|
||||||
|
"paneNode": "Panel düğümü",
|
||||||
|
"paneNodeDesc": "Bir veya daha fazla yüzey (panel içindeki sekmeler) içeren yaprak düğüm.",
|
||||||
|
"surfaceDefinition": "Yüzey tanımı",
|
||||||
|
"surfaceDefinitionDesc": "Bir paneldeki her yüzey terminal veya tarayıcı olabilir:",
|
||||||
|
"surfaceName": "Özel sekme başlığı",
|
||||||
|
"surfaceCommand": "Oluşturulduğunda otomatik çalıştırılacak shell komutu (yalnızca terminal)",
|
||||||
|
"surfaceCwd": "Bu yüzey için çalışma dizini",
|
||||||
|
"surfaceEnv": "Anahtar-değer çiftleri olarak ortam değişkenleri",
|
||||||
|
"surfaceUrl": "Açılacak URL (yalnızca tarayıcı)",
|
||||||
|
"surfaceFocus": "Oluşturulduktan sonra bu yüzeye odaklan",
|
||||||
|
"cwdResolution": "Çalışma dizini çözümlemesi",
|
||||||
|
"omitted": "atlanmış",
|
||||||
|
"cwdRelative": "workspace çalışma dizini",
|
||||||
|
"cwdSubdir": "workspace çalışma dizinine göreli",
|
||||||
|
"cwdHome": "ana dizine genişletilmiş",
|
||||||
|
"absolutePath": "Mutlak yol",
|
||||||
|
"cwdAbsolute": "olduğu gibi kullanılır",
|
||||||
|
"fullExample": "Tam örnek"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Klavye Kısayolları",
|
"title": "Klavye Kısayolları",
|
||||||
"description": "cmux'ta mevcut tüm klavye kısayolları, kategoriye göre gruplandırılmış.",
|
"description": "cmux'ta mevcut tüm klavye kısayolları, kategoriye göre gruplandırılmış.",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "Başlarken",
|
"gettingStarted": "Başlarken",
|
||||||
"concepts": "Kavramlar",
|
"concepts": "Kavramlar",
|
||||||
"configuration": "Yapılandırma",
|
"configuration": "Yapılandırma",
|
||||||
|
"customCommands": "Özel Komutlar",
|
||||||
"keyboardShortcuts": "Klavye Kısayolları",
|
"keyboardShortcuts": "Klavye Kısayolları",
|
||||||
"apiReference": "API Referansı",
|
"apiReference": "API Referansı",
|
||||||
"browserAutomation": "Tarayıcı Otomasyonu",
|
"browserAutomation": "Tarayıcı Otomasyonu",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "配置示例",
|
"exampleConfig": "配置示例",
|
||||||
"metaTitle": "配置"
|
"metaTitle": "配置"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "自定义命令",
|
||||||
|
"metaTitle": "自定义命令",
|
||||||
|
"metaDescription": "在 cmux.json 中定义自定义命令和工作区布局。支持按项目配置和全局配置,并具有实时文件监视功能。",
|
||||||
|
"intro": "通过在项目根目录或 ~/.config/cmux/ 中添加 cmux.json 文件来定义自定义命令和工作区布局。命令将显示在命令面板中。",
|
||||||
|
"fileLocations": "文件位置",
|
||||||
|
"fileLocationsDesc": "cmux 在两个地方查找配置:",
|
||||||
|
"localConfig": "按项目:",
|
||||||
|
"localConfigDesc": "位于项目目录中,优先级更高",
|
||||||
|
"globalConfig": "全局:",
|
||||||
|
"globalConfigDesc": "适用于所有项目,补充本地未定义的命令",
|
||||||
|
"precedenceNote": "本地命令会覆盖同名的全局命令。",
|
||||||
|
"liveReload": "更改会自动生效 — 无需重启。",
|
||||||
|
"schema": "架构",
|
||||||
|
"schemaDesc": "cmux.json 文件包含一个 commands 数组。每个命令要么是简单的 shell 命令,要么是完整的工作区定义:",
|
||||||
|
"simpleCommands": "简单命令",
|
||||||
|
"simpleCommandsDesc": "简单命令在当前聚焦的终端中运行 shell 命令:",
|
||||||
|
"simpleCommandFields": "字段",
|
||||||
|
"fieldName": "在命令面板中显示(必填)",
|
||||||
|
"fieldDescription": "可选描述",
|
||||||
|
"fieldKeywords": "命令面板的额外搜索词",
|
||||||
|
"fieldCommand": "在聚焦终端中运行的 shell 命令",
|
||||||
|
"fieldConfirm": "运行前显示确认对话框",
|
||||||
|
"simpleCommandCwdNote": "简单命令在当前聚焦终端的工作目录中运行。如果命令依赖于项目相对路径,请在前面加上",
|
||||||
|
"simpleCommandCwdRepoRoot": "以从仓库根目录运行,或使用",
|
||||||
|
"simpleCommandCwdCustomPath": "指定任意目录。",
|
||||||
|
"workspaceCommands": "工作区命令",
|
||||||
|
"workspaceCommandsDesc": "工作区命令会创建一个新工作区,具有自定义的分屏、终端和浏览器面板布局:",
|
||||||
|
"workspaceFields": "工作区字段",
|
||||||
|
"wsFieldName": "工作区标签页名称(默认为命令名称)",
|
||||||
|
"wsFieldCwd": "工作区的工作目录",
|
||||||
|
"wsFieldColor": "工作区标签页颜色",
|
||||||
|
"wsFieldLayout": "定义分屏和面板的布局树",
|
||||||
|
"restartBehavior": "重启行为",
|
||||||
|
"restartBehaviorDesc": "控制当同名工作区已存在时的行为:",
|
||||||
|
"restartIgnore": "切换到现有工作区(默认)",
|
||||||
|
"restartRecreate": "无需询问直接关闭并重新创建",
|
||||||
|
"restartConfirm": "重新创建前询问用户",
|
||||||
|
"layoutTree": "布局树",
|
||||||
|
"layoutTreeDesc": "布局树使用递归分割节点定义面板的排列方式:",
|
||||||
|
"splitNode": "分割节点",
|
||||||
|
"splitNodeDesc": "将空间分割为两个子节点:",
|
||||||
|
"or": "或",
|
||||||
|
"splitPosition": "分割线位置,从 0.1 到 0.9(默认 0.5)",
|
||||||
|
"splitChildren": "恰好两个子节点(分割或面板)",
|
||||||
|
"paneNode": "面板节点",
|
||||||
|
"paneNodeDesc": "包含一个或多个 surface(面板内标签页)的叶节点。",
|
||||||
|
"surfaceDefinition": "Surface 定义",
|
||||||
|
"surfaceDefinitionDesc": "面板中的每个 surface 可以是终端或浏览器:",
|
||||||
|
"surfaceName": "自定义标签页标题",
|
||||||
|
"surfaceCommand": "创建时自动运行的 shell 命令(仅限终端)",
|
||||||
|
"surfaceCwd": "此 surface 的工作目录",
|
||||||
|
"surfaceEnv": "以键值对形式表示的环境变量",
|
||||||
|
"surfaceUrl": "要打开的 URL(仅限浏览器)",
|
||||||
|
"surfaceFocus": "创建后聚焦此 surface",
|
||||||
|
"cwdResolution": "工作目录解析",
|
||||||
|
"omitted": "省略",
|
||||||
|
"cwdRelative": "工作区工作目录",
|
||||||
|
"cwdSubdir": "相对于工作区工作目录",
|
||||||
|
"cwdHome": "展开到主目录",
|
||||||
|
"absolutePath": "绝对路径",
|
||||||
|
"cwdAbsolute": "按原样使用",
|
||||||
|
"fullExample": "完整示例"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "快捷键",
|
"title": "快捷键",
|
||||||
"description": "cmux 中所有可用的快捷键,按类别分组。",
|
"description": "cmux 中所有可用的快捷键,按类别分组。",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "入门指南",
|
"gettingStarted": "入门指南",
|
||||||
"concepts": "核心概念",
|
"concepts": "核心概念",
|
||||||
"configuration": "配置",
|
"configuration": "配置",
|
||||||
|
"customCommands": "自定义命令",
|
||||||
"keyboardShortcuts": "快捷键",
|
"keyboardShortcuts": "快捷键",
|
||||||
"apiReference": "API 参考",
|
"apiReference": "API 参考",
|
||||||
"browserAutomation": "浏览器自动化",
|
"browserAutomation": "浏览器自动化",
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,70 @@
|
||||||
"exampleConfig": "範例設定",
|
"exampleConfig": "範例設定",
|
||||||
"metaTitle": "設定"
|
"metaTitle": "設定"
|
||||||
},
|
},
|
||||||
|
"customCommands": {
|
||||||
|
"title": "自訂指令",
|
||||||
|
"metaTitle": "自訂指令",
|
||||||
|
"metaDescription": "在 cmux.json 中定義自訂指令和工作區版面配置。支援按專案設定和全域設定,並具備即時檔案監視功能。",
|
||||||
|
"intro": "透過在專案根目錄或 ~/.config/cmux/ 中新增 cmux.json 檔案來定義自訂指令和工作區版面配置。指令將顯示於指令面板中。",
|
||||||
|
"fileLocations": "檔案位置",
|
||||||
|
"fileLocationsDesc": "cmux 在兩個地方尋找設定:",
|
||||||
|
"localConfig": "依專案:",
|
||||||
|
"localConfigDesc": "位於專案目錄中,優先採用",
|
||||||
|
"globalConfig": "全域:",
|
||||||
|
"globalConfigDesc": "適用於所有專案,補充本地未定義的指令",
|
||||||
|
"precedenceNote": "本地指令會覆蓋同名的全域指令。",
|
||||||
|
"liveReload": "變更會自動生效 — 無需重新啟動。",
|
||||||
|
"schema": "結構描述",
|
||||||
|
"schemaDesc": "cmux.json 檔案包含一個 commands 陣列。每個指令可以是簡單的 shell 指令或完整的工作區定義:",
|
||||||
|
"simpleCommands": "簡單指令",
|
||||||
|
"simpleCommandsDesc": "簡單指令會在目前聚焦的終端機中執行 shell 指令:",
|
||||||
|
"simpleCommandFields": "欄位",
|
||||||
|
"fieldName": "顯示於指令面板(必填)",
|
||||||
|
"fieldDescription": "選填說明",
|
||||||
|
"fieldKeywords": "指令面板的額外搜尋詞",
|
||||||
|
"fieldCommand": "在聚焦終端機中執行的 shell 指令",
|
||||||
|
"fieldConfirm": "執行前顯示確認對話框",
|
||||||
|
"simpleCommandCwdNote": "簡單指令在當前聚焦終端的工作目錄中執行。如果指令依賴於專案相對路徑,請在前面加上",
|
||||||
|
"simpleCommandCwdRepoRoot": "以從倉庫根目錄執行,或使用",
|
||||||
|
"simpleCommandCwdCustomPath": "指定任意目錄。",
|
||||||
|
"workspaceCommands": "工作區指令",
|
||||||
|
"workspaceCommandsDesc": "工作區指令會建立一個新工作區,具有自訂的分割、終端機和瀏覽器窗格版面配置:",
|
||||||
|
"workspaceFields": "工作區欄位",
|
||||||
|
"wsFieldName": "工作區索引標籤名稱(預設為指令名稱)",
|
||||||
|
"wsFieldCwd": "工作區的工作目錄",
|
||||||
|
"wsFieldColor": "工作區索引標籤顏色",
|
||||||
|
"wsFieldLayout": "定義分割和窗格的版面配置樹",
|
||||||
|
"restartBehavior": "重新啟動行為",
|
||||||
|
"restartBehaviorDesc": "控制當同名工作區已存在時的行為:",
|
||||||
|
"restartIgnore": "切換至現有工作區(預設)",
|
||||||
|
"restartRecreate": "無需詢問直接關閉並重新建立",
|
||||||
|
"restartConfirm": "重新建立前詢問使用者",
|
||||||
|
"layoutTree": "版面配置樹",
|
||||||
|
"layoutTreeDesc": "版面配置樹使用遞迴分割節點定義窗格的排列方式:",
|
||||||
|
"splitNode": "分割節點",
|
||||||
|
"splitNodeDesc": "將空間分割為兩個子節點:",
|
||||||
|
"or": "或",
|
||||||
|
"splitPosition": "分割線位置,從 0.1 到 0.9(預設 0.5)",
|
||||||
|
"splitChildren": "恰好兩個子節點(分割或窗格)",
|
||||||
|
"paneNode": "窗格節點",
|
||||||
|
"paneNodeDesc": "包含一個或多個 surface(窗格內索引標籤)的葉節點。",
|
||||||
|
"surfaceDefinition": "Surface 定義",
|
||||||
|
"surfaceDefinitionDesc": "窗格中的每個 surface 可以是終端機或瀏覽器:",
|
||||||
|
"surfaceName": "自訂索引標籤標題",
|
||||||
|
"surfaceCommand": "建立時自動執行的 shell 指令(僅限終端機)",
|
||||||
|
"surfaceCwd": "此 surface 的工作目錄",
|
||||||
|
"surfaceEnv": "以鍵值對形式表示的環境變數",
|
||||||
|
"surfaceUrl": "要開啟的 URL(僅限瀏覽器)",
|
||||||
|
"surfaceFocus": "建立後聚焦此 surface",
|
||||||
|
"cwdResolution": "工作目錄解析",
|
||||||
|
"omitted": "省略",
|
||||||
|
"cwdRelative": "工作區工作目錄",
|
||||||
|
"cwdSubdir": "相對於工作區工作目錄",
|
||||||
|
"cwdHome": "展開至主目錄",
|
||||||
|
"absolutePath": "絕對路徑",
|
||||||
|
"cwdAbsolute": "按原樣使用",
|
||||||
|
"fullExample": "完整範例"
|
||||||
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "鍵盤快捷鍵",
|
"title": "鍵盤快捷鍵",
|
||||||
"description": "cmux 中所有可用的鍵盤快捷鍵,依類別分組。",
|
"description": "cmux 中所有可用的鍵盤快捷鍵,依類別分組。",
|
||||||
|
|
@ -547,6 +611,7 @@
|
||||||
"gettingStarted": "入門指南",
|
"gettingStarted": "入門指南",
|
||||||
"concepts": "概念",
|
"concepts": "概念",
|
||||||
"configuration": "設定",
|
"configuration": "設定",
|
||||||
|
"customCommands": "自訂指令",
|
||||||
"keyboardShortcuts": "鍵盤快捷鍵",
|
"keyboardShortcuts": "鍵盤快捷鍵",
|
||||||
"apiReference": "API 參考",
|
"apiReference": "API 參考",
|
||||||
"browserAutomation": "瀏覽器自動化",
|
"browserAutomation": "瀏覽器自動化",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue