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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ settings file. On Windows, `WT_PARENT_WINDOW_HANDLE` embeds the window as a
child of that HWND. `alwaysShowTabs: false` hides the tab row when only one
tab is open.

`dt --isolated` (or `Devolutions.Terminal.exe --isolated`) runs the launch in
its own process: it neither forwards to a running instance nor becomes the
broker for later launches, so every window keeps its own environment,
`WT_PARENT_WINDOW_HANDLE`, and settings directory. `-w` targeting of other
windows is unavailable, and persisted layouts are neither restored nor saved.
It is the per-launch equivalent of Windows Terminal's
`compatibility.isolatedMode` setting, which Windows Terminal 1.23 removed.

Set `"experimental.terminalEngine": "ghostty"` to use the pinned
`libghostty-vt` engine globally. A profile can override it with `"builtin"` or
`"ghostty"`. ConPTY remains the Windows process transport for both engines.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ preference is not null &&
preference.Equals("persistedWindowLayout", StringComparison.OrdinalIgnoreCase) ||
preference.Equals("persistedLayoutAndContent", StringComparison.OrdinalIgnoreCase));

// Matches Windows Terminal: an isolated process never restores or saves the shared window layouts.
public static bool ShouldUsePersistedLayout(string? firstWindowPreference, bool isolated) =>
!isolated && IsPersistedLayoutPreference(firstWindowPreference);

public static WindowLayoutState? ReadWindowState(ApplicationStateStore store, int windowId)
{
ArgumentNullException.ThrowIfNull(store);
Expand Down
8 changes: 5 additions & 3 deletions src/Devolutions.Terminal.App/Views/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public partial class MainWindow :
private readonly Func<GlobalSummonArgs, ValueTask<WindowActionResult>>? _summonRequested;
private readonly Action<AppSettings>? _settingsChanged;
private readonly ISystemMenuService _systemMenuService;
private readonly bool _isolated;
private readonly TaskCompletionSource<TerminalWindowActivationResult> _initialActivationCompletion =
new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly DispatcherTimer _notificationTimer;
Expand All @@ -102,10 +103,12 @@ public MainWindow(
Action<string>? workspaceRequested = null,
Func<GlobalSummonArgs, ValueTask<WindowActionResult>>? summonRequested = null,
Action<AppSettings>? settingsChanged = null,
ISystemMenuService? systemMenuService = null)
ISystemMenuService? systemMenuService = null,
bool isolated = false)
{
WindowId = windowId;
WindowName = windowName;
_isolated = isolated;
_initialActivation = initialActivation;
_newWindowRequested = newWindowRequested;
_tabTearOffRequested = tabTearOffRequested;
Expand Down Expand Up @@ -3647,8 +3650,7 @@ private void PersistWorkspace(TerminalWindowLayoutDescriptor layout)
};

private bool UsesPersistedLayout =>
TerminalLayoutStateStore.IsPersistedLayoutPreference(
_settings.FirstWindowPreference);
TerminalLayoutStateStore.ShouldUsePersistedLayout(_settings.FirstWindowPreference, _isolated);

private void TryPersistCurrentLayout(TerminalWindowLayoutDescriptor layout)
{
Expand Down
3 changes: 2 additions & 1 deletion src/Devolutions.Terminal.Cli/CliContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public sealed record CliInvocation(
CliLaunchMode LaunchMode,
int? SavedLayout,
IReadOnlyList<ActionAndArgs> Actions,
CliSaveRequest? SaveRequest = null);
CliSaveRequest? SaveRequest = null,
bool Isolated = false);
Comment thread
irvingoujAtDevolution marked this conversation as resolved.

public sealed record CliParseResult(
int ExitCode,
Expand Down
16 changes: 13 additions & 3 deletions src/Devolutions.Terminal.Cli/CliParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ private static CliParseResult ParseCore(
int? rows = null;
int? savedLayout = null;
var launchMode = CliLaunchMode.Default;
var isolated = false;
CliSaveRequest? save = null;

for (var segmentIndex = 0; segmentIndex < segments.Count; segmentIndex++)
Expand All @@ -135,7 +136,8 @@ private static CliParseResult ParseCore(
ref columns,
ref rows,
ref savedLayout,
ref launchMode);
ref launchMode,
ref isolated);
}

var command = index < segment.Count && Commands.Contains(segment[index])
Expand Down Expand Up @@ -215,7 +217,8 @@ save is null &&
launchMode,
savedLayout,
actions,
save);
save,
isolated);
return new(0, string.Empty, false, invocation);
}

Expand All @@ -228,7 +231,8 @@ private static void ParseRootOptions(
ref int? columns,
ref int? rows,
ref int? savedLayout,
ref CliLaunchMode launchMode)
ref CliLaunchMode launchMode,
ref bool isolated)
{
while (index < args.Count)
{
Expand Down Expand Up @@ -269,6 +273,10 @@ private static void ParseRootOptions(
launchMode |= CliLaunchMode.Focus;
index++;
break;
case "--isolated":
isolated = true;
index++;
break;
Comment thread
irvingoujAtDevolution marked this conversation as resolved.
default:
if (option.StartsWith('-') &&
option.Length > 2 &&
Expand Down Expand Up @@ -696,6 +704,7 @@ internal static class CommandLineSchema
-M, --maximized Launch maximized
-F, --fullscreen Launch fullscreen
-f, --focus Hide the title bar
--isolated Own process: never join or serve other launches

Commands:
new-tab, nt split-pane, sp focus-tab, ft move-focus, mf
Expand All @@ -708,6 +717,7 @@ public static RootCommand Create()
root.Options.Add(new Option<string>("--window") { Description = "Target window." });
root.Options.Add(new Option<string>("--pos") { Description = "Window position." });
root.Options.Add(new Option<string>("--size") { Description = "Terminal size." });
root.Options.Add(new Option<bool>("--isolated") { Description = "Run in its own process." });
foreach (var (name, alias) in new[]
{
("new-tab", "nt"),
Expand Down
8 changes: 5 additions & 3 deletions src/Devolutions.Terminal.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ public static async Task<int> Main(string[] args)
}

var invocation = parsed.Invocation!;
var response = await new BrokerClient().SendAsync(
invocation.TargetWindow,
CliInvocationSerializer.Serialize(invocation)).ConfigureAwait(false);
var response = invocation.Isolated
? BrokerResponse.Unavailable("Isolated launches do not use the broker.")
: await new BrokerClient().SendAsync(
invocation.TargetWindow,
CliInvocationSerializer.Serialize(invocation)).ConfigureAwait(false);
if (response.Status == BrokerStatus.Unavailable)
{
if (RequiresExistingWindow(invocation.TargetWindow))
Expand Down
5 changes: 4 additions & 1 deletion src/Devolutions.Terminal/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ public override void OnFrameworkInitializationCompleted()
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
_desktop = desktop;
_router = new TerminalWindowRouter(desktop, ConfigureWindow);
_router = new TerminalWindowRouter(
desktop,
ConfigureWindow,
isolated: InitialInvocation?.Isolated ?? false);
BrokerHandler?.SetHandler(_router);
if (OperatingSystem.IsMacOS())
{
Expand Down
26 changes: 15 additions & 11 deletions src/Devolutions.Terminal/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,30 +67,34 @@ public static int Main(string[] args)

var invocation = parsed.Invocation!;
var deferredHandler = new DeferredBrokerHandler();
var broker = BrokerHost.TryCreate(deferredHandler);
if (broker is null)
BrokerHost? broker = null;
if (!invocation.Isolated)
{
var response = ForwardToPrimaryAsync(invocation).AsTask().GetAwaiter().GetResult();
if (!response.IsSuccess)
broker = BrokerHost.TryCreate(deferredHandler);
if (broker is null)
{
Console.Error.WriteLine($"dt: {response.Message}");
return response.Status == BrokerStatus.WindowNotFound ? 3 : 1;
}
var response = ForwardToPrimaryAsync(invocation).AsTask().GetAwaiter().GetResult();
if (!response.IsSuccess)
{
Console.Error.WriteLine($"dt: {response.Message}");
return response.Status == BrokerStatus.WindowNotFound ? 3 : 1;
}

return 0;
return 0;
}
}

if (invocation.TargetWindow.Equals("use-existing", StringComparison.OrdinalIgnoreCase) ||
(int.TryParse(invocation.TargetWindow, out var requestedWindowId) && requestedWindowId > 0))
{
broker.DisposeAsync().AsTask().GetAwaiter().GetResult();
broker?.DisposeAsync().AsTask().GetAwaiter().GetResult();
Console.Error.WriteLine($"dt: terminal window '{invocation.TargetWindow}' was not found.");
return 3;
}

if (invocation.SaveRequest is { Commandline.Length: > 0 } saveRequest)
{
broker.DisposeAsync().AsTask().GetAwaiter().GetResult();
broker?.DisposeAsync().AsTask().GetAwaiter().GetResult();
try
{
var settings = Devolutions.Terminal.Settings.SettingsService.Load();
Expand Down Expand Up @@ -135,7 +139,7 @@ IOException or
}
finally
{
broker.DisposeAsync().AsTask().GetAwaiter().GetResult();
broker?.DisposeAsync().AsTask().GetAwaiter().GetResult();
}
}

Expand Down
13 changes: 9 additions & 4 deletions src/Devolutions.Terminal/TerminalWindowRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@ internal sealed class TerminalWindowRouter : IBrokerRequestHandler, IDisposable
private readonly ApplicationStateStore _stateStore;
private readonly GlobalWindowActionRouter _windowActions;
private readonly GlobalHotkeyManager _globalHotkeys;
private readonly bool _isolated;
private int _nextWindowId = 1;

public TerminalWindowRouter(
IClassicDesktopStyleApplicationLifetime desktop,
Action<MainWindow>? windowCreated = null,
ApplicationStateStore? stateStore = null,
IGlobalHotkeyBackend? globalHotkeyBackend = null)
IGlobalHotkeyBackend? globalHotkeyBackend = null,
bool isolated = false)
{
_desktop = desktop;
_windowCreated = windowCreated;
_isolated = isolated;
_stateStore = stateStore ?? SettingsService.LoadApplicationState();
_windowActions = new GlobalWindowActionRouter(CreateSummonWindow);
_globalHotkeys = new GlobalHotkeyManager(
Expand Down Expand Up @@ -146,7 +149,8 @@ private MainWindow CreateWindow(TerminalWindowActivation activation, string name
: $"{window.WindowName} ({window.WindowId})")
.ToArray(),
summonRequested: args => _windowActions.SummonAsync(window, args),
settingsChanged: settings => TraceHotkeyResults(_globalHotkeys.Apply(settings.ActionMap)));
settingsChanged: settings => TraceHotkeyResults(_globalHotkeys.Apply(settings.ActionMap)),
isolated: _isolated);
_windows.Add(window);
_windowActions.Add(window);
_windowCreated?.Invoke(window);
Expand Down Expand Up @@ -225,11 +229,12 @@ parsed.Invocation.PositionX is not null ||
parsed.Invocation.PositionY is not null ||
parsed.Invocation.Columns is not null ||
parsed.Invocation.Rows is not null ||
parsed.Invocation.LaunchMode != CliLaunchMode.Default)
parsed.Invocation.LaunchMode != CliLaunchMode.Default ||
parsed.Invocation.Isolated)
{
return new(
false,
"Window routing, position, size, and launch-mode options are not valid inside the current window's command palette.",
"Window routing, position, size, launch-mode, and isolation options are not valid inside the current window's command palette.",
[]);
}

Expand Down
10 changes: 10 additions & 0 deletions tests/Devolutions.Terminal.App.Tests/LayoutDescriptorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ public void NonPersistedFirstWindowPreferencesDoNotRestoreLayouts(string? prefer
Assert.False(TerminalLayoutStateStore.IsPersistedLayoutPreference(preference));
}

[Theory]
[InlineData("persistedWindowLayout", false, true)]
[InlineData("persistedWindowLayout", true, false)]
[InlineData("defaultProfile", false, false)]
[InlineData("defaultProfile", true, false)]
public void IsolatedModeNeverUsesPersistedLayouts(string preference, bool isolated, bool expected)
{
Assert.Equal(expected, TerminalLayoutStateStore.ShouldUsePersistedLayout(preference, isolated));
}

[Fact]
public void WindowTabPaneLayoutRoundTripsThroughApplicationState()
{
Expand Down
23 changes: 23 additions & 0 deletions tests/Devolutions.Terminal.Cli.Tests/CliParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,29 @@ public void RootLaunchOptionsArePreserved()
Assert.Equal(40, result.Rows);
Assert.True(result.LaunchMode.HasFlag(CliLaunchMode.Maximized));
Assert.True(result.LaunchMode.HasFlag(CliLaunchMode.Focus));
Assert.False(result.Isolated);
}

[Fact]
public void IsolatedIsARootOptionThatKeepsTheCommandLine()
{
var result = Parse("--isolated", "-w", "new", "new-tab", "-p", "cmd", "cmd.exe", "/k", "--isolated");

Assert.True(result.Isolated);
Assert.Equal("new", result.TargetWindow);
var terminal = Assert.IsType<NewTerminalArgs>(
Assert.IsType<NewTabArgs>(Assert.Single(result.Actions).Args).ContentArgs);
Assert.Equal("cmd", terminal.Profile);
Assert.Equal("cmd.exe /k --isolated", terminal.Commandline);
}

[Fact]
public void IsolatedAfterACommandIsRejected()
{
var result = new CliParser().Parse(["new-tab", "--isolated"]);

Assert.Equal(2, result.ExitCode);
Assert.Equal("dt: Unknown command '--isolated'.", result.Message);
}

[Fact]
Expand Down
Loading