Skip to content

Commit d2f437d

Browse files
committed
Multiple terminals
1 parent 57376d0 commit d2f437d

File tree

5 files changed

+199
-48
lines changed

5 files changed

+199
-48
lines changed

src/Files.App/UserControls/StatusBar.xaml

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -429,27 +429,96 @@
429429
Height="24"
430430
Padding="8,0,8,0"
431431
VerticalAlignment="Center"
432-
x:Load="True"
433432
AutomationProperties.Name="Toggle terminal"
434433
Background="Transparent"
435434
BorderBrush="Transparent"
436435
Command="{x:Bind MainPageViewModel.TerminalToggleCommand}">
437436
<SplitButton.Content>
438437
<StackPanel Orientation="Horizontal" Spacing="8">
439438
<FontIcon FontSize="12" Glyph="&#xE756;" />
440-
<TextBlock Text="{x:Bind MainPageViewModel.TerminalSelectedProfile.Name, Mode=OneWay}" />
439+
<TextBlock Text="Terminal" />
441440
</StackPanel>
442441
</SplitButton.Content>
443442
<SplitButton.Flyout>
444443
<Flyout Placement="Top">
445-
<ListView
446-
x:Name="ShellProfileList"
447-
Margin="-16"
448-
Padding="4"
449-
DisplayMemberPath="Name"
450-
ItemsSource="{x:Bind MainPageViewModel.TerminalProfiles, Mode=OneWay}"
451-
SelectedItem="{x:Bind MainPageViewModel.TerminalSelectedProfile, Mode=TwoWay}"
452-
SelectionMode="Single" />
444+
<Grid
445+
Width="200"
446+
Height="160"
447+
Margin="-16">
448+
<Grid.RowDefinitions>
449+
<RowDefinition Height="Auto" />
450+
<RowDefinition Height="*" />
451+
</Grid.RowDefinitions>
452+
453+
<!-- Header -->
454+
<Grid
455+
Grid.Row="0"
456+
Padding="4,8,8,8"
457+
Background="{ThemeResource AcrylicBackgroundFillColorDefaultBrush}"
458+
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
459+
BorderThickness="0,0,0,1">
460+
461+
<SplitButton
462+
Height="24"
463+
Padding="8,0"
464+
HorizontalAlignment="Center"
465+
Command="{x:Bind MainPageViewModel.TerminalAddCommand, Mode=OneWay}">
466+
<SplitButton.Content>
467+
<TextBlock FontSize="12">
468+
<Run Text="Add" />
469+
<Run Text="{x:Bind MainPageViewModel.TerminalSelectedProfile.Name, Mode=OneWay}" />
470+
</TextBlock>
471+
</SplitButton.Content>
472+
<SplitButton.Flyout>
473+
<Flyout>
474+
<ListView
475+
x:Name="ShellProfileList"
476+
Margin="-16"
477+
Padding="4"
478+
DisplayMemberPath="Name"
479+
IsItemClickEnabled="True"
480+
ItemClick="ShellProfileList_ItemClick"
481+
ItemsSource="{x:Bind MainPageViewModel.TerminalProfiles, Mode=OneWay}"
482+
SelectionMode="None" />
483+
</Flyout>
484+
</SplitButton.Flyout>
485+
</SplitButton>
486+
</Grid>
487+
488+
<ListView
489+
Grid.Row="1"
490+
Padding="4"
491+
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
492+
IsItemClickEnabled="True"
493+
ItemsSource="{x:Bind MainPageViewModel.TerminalNames, Mode=OneWay}"
494+
SelectedIndex="{x:Bind MainPageViewModel.SelectedTerminal, Mode=TwoWay}"
495+
SelectionMode="Single">
496+
<ListView.ItemTemplate>
497+
<DataTemplate x:DataType="x:String">
498+
<Grid>
499+
<Grid.ColumnDefinitions>
500+
<ColumnDefinition Width="*" />
501+
<ColumnDefinition Width="Auto" />
502+
</Grid.ColumnDefinitions>
503+
<TextBlock
504+
VerticalAlignment="Center"
505+
Text="{x:Bind}"
506+
TextTrimming="CharacterEllipsis" />
507+
<Button
508+
Grid.Column="1"
509+
AutomationProperties.Name="{helpers:ResourceString Name=Close}"
510+
Background="Transparent"
511+
BorderBrush="Transparent"
512+
Click="Button_Click"
513+
Tag="{x:Bind}"
514+
ToolTipService.ToolTip="{helpers:ResourceString Name=Close}">
515+
<FontIcon FontSize="12" Glyph="&#xE74D;" />
516+
</Button>
517+
</Grid>
518+
</DataTemplate>
519+
</ListView.ItemTemplate>
520+
</ListView>
521+
</Grid>
453522
</Flyout>
454523
</SplitButton.Flyout>
455524
</SplitButton>

src/Files.App/UserControls/StatusBar.xaml.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License. See the LICENSE.
33

44
using Files.App.Data.Commands;
5+
using Files.App.Utils.Terminal;
56
using Microsoft.UI.Xaml;
67
using Microsoft.UI.Xaml.Controls;
78

@@ -79,5 +80,15 @@ private async void DeleteBranch_Click(object sender, RoutedEventArgs e)
7980
BranchesFlyout.Hide();
8081
await DirectoryPropertiesViewModel.ExecuteDeleteBranch(((BranchItem)((Button)sender).DataContext).Name);
8182
}
83+
84+
private void Button_Click(object sender, RoutedEventArgs e)
85+
{
86+
MainPageViewModel.TerminalCloseCommand.Execute(((Button)sender).Tag.ToString());
87+
}
88+
89+
private void ShellProfileList_ItemClick(object sender, ItemClickEventArgs e)
90+
{
91+
MainPageViewModel.TerminalAddCommand.Execute((ShellProfile)e.ClickedItem);
92+
}
8293
}
8394
}

src/Files.App/UserControls/TerminalView.xaml.cs

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ private async Task ResizeTask()
113113

114114
private Terminal _terminal;
115115
private BufferedReader _reader;
116+
private ShellProfile _profile;
117+
118+
public TerminalView(ShellProfile profile) : this()
119+
=> _profile = profile;
116120

117121
public TerminalView()
118122
{
@@ -121,6 +125,9 @@ public TerminalView()
121125

122126
private async void WebViewControl_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
123127
{
128+
if (WebViewControl.Source is not null)
129+
return;
130+
124131
var envOptions = new CoreWebView2EnvironmentOptions()
125132
{
126133
// TODO: switch to "ScrollBarStyle" when available
@@ -143,8 +150,7 @@ private async void WebViewControl_Loaded(object sender, Microsoft.UI.Xaml.Routed
143150
var provider = new DefaultValueProvider();
144151
var options = provider.GetDefaultTerminalOptions();
145152
var keyBindings = provider.GetCommandKeyBindings();
146-
var profile = _mainPageModel.TerminalSelectedProfile;
147-
var theme = provider.GetPreInstalledThemes().First(x => x.Id == profile.TerminalThemeId);
153+
var theme = provider.GetPreInstalledThemes().First(x => x.Id == _profile.TerminalThemeId);
148154

149155
WebViewControl.CoreWebView2.Profile.PreferredColorScheme = (ActualTheme == Microsoft.UI.Xaml.ElementTheme.Dark) ? CoreWebView2PreferredColorScheme.Dark : CoreWebView2PreferredColorScheme.Light;
150156

@@ -167,7 +173,7 @@ private async void WebViewControl_Loaded(object sender, Microsoft.UI.Xaml.Routed
167173
}
168174
}
169175

170-
StartShellProcess(size, profile);
176+
StartShellProcess(size, _profile);
171177

172178
lock (_resizeLock)
173179
{
@@ -329,8 +335,6 @@ private async Task<string> ExecuteScriptAsync(string script)
329335

330336
public void Dispose()
331337
{
332-
_mainPageModel.GetTerminalFolder = null;
333-
_mainPageModel.SetTerminalFolder = null;
334338
WebViewControl.Close();
335339
_outputBlockedBuffer?.Dispose();
336340
_reader?.Dispose();
@@ -339,6 +343,34 @@ public void Dispose()
339343

340344
public void Paste(string text) => OnPaste?.Invoke(this, text);
341345

346+
public async Task<string?> GetTerminalFolder()
347+
{
348+
var tcs = new TaskCompletionSource<string>();
349+
EventHandler<object> getResponse = (s, e) =>
350+
{
351+
var pwd = Encoding.UTF8.GetString((byte[])e);
352+
var match = Regex.Match(pwd, @"[a-zA-Z]:\\(((?![<>:""\r/\\|?*]).)+((?<![ .])\\)?)*");
353+
if (match.Success)
354+
tcs.TrySetResult(match.Value);
355+
};
356+
OnOutput += getResponse;
357+
if (_profile.Location.Contains("wsl.exe"))
358+
_terminal.WriteToPseudoConsole(Encoding.UTF8.GetBytes($"wslpath -w \"$(pwd)\"\r"));
359+
else
360+
_terminal.WriteToPseudoConsole(Encoding.UTF8.GetBytes($"cd .\r"));
361+
var pwd = await tcs.Task.WithTimeoutAsync(TimeSpan.FromSeconds(1));
362+
OnOutput -= getResponse;
363+
return pwd;
364+
}
365+
366+
public void SetTerminalFolder(string folder)
367+
{
368+
if (_profile.Location.Contains("wsl.exe"))
369+
_terminal.WriteToPseudoConsole(Encoding.UTF8.GetBytes($"cd \"$(wslpath \"{folder}\")\"\r"));
370+
else
371+
_terminal.WriteToPseudoConsole(Encoding.UTF8.GetBytes($"cd \"{folder}\"\r"));
372+
}
373+
342374
private void StartShellProcess(TerminalSize size, ShellProfile profile)
343375
{
344376
var ShellExecutableName = Path.GetFileNameWithoutExtension(profile.Location);
@@ -352,38 +384,12 @@ private void StartShellProcess(TerminalSize size, ShellProfile profile)
352384
_terminal.OutputReady += (s, e) =>
353385
{
354386
_reader = new BufferedReader(_terminal.ConsoleOutStream, OutputReceivedCallback, true);
355-
_mainPageModel.GetTerminalFolder = async () =>
356-
{
357-
var tcs = new TaskCompletionSource<string>();
358-
EventHandler<object> getResponse = (s, e) =>
359-
{
360-
var pwd = Encoding.UTF8.GetString((byte[])e);
361-
var match = Regex.Match(pwd, @"[a-zA-Z]:\\(((?![<>:""\r/\\|?*]).)+((?<![ .])\\)?)*");
362-
if (match.Success)
363-
tcs.TrySetResult(match.Value);
364-
};
365-
OnOutput += getResponse;
366-
if (profile.Location.Contains("wsl.exe"))
367-
_terminal.WriteToPseudoConsole(Encoding.UTF8.GetBytes($"wslpath -w \"$(pwd)\"\r"));
368-
else
369-
_terminal.WriteToPseudoConsole(Encoding.UTF8.GetBytes($"cd .\r"));
370-
var pwd = await tcs.Task.WithTimeoutAsync(TimeSpan.FromSeconds(1));
371-
OnOutput -= getResponse;
372-
return pwd;
373-
};
374-
_mainPageModel.SetTerminalFolder = (folder) =>
375-
{
376-
if (profile.Location.Contains("wsl.exe"))
377-
_terminal.WriteToPseudoConsole(Encoding.UTF8.GetBytes($"cd \"$(wslpath \"{folder}\")\"\r"));
378-
else
379-
_terminal.WriteToPseudoConsole(Encoding.UTF8.GetBytes($"cd \"{folder}\"\r"));
380-
};
381387
};
382388
_terminal.Exited += (s, e) =>
383389
{
384390
DispatcherQueue.EnqueueAsync(() =>
385391
{
386-
_mainPageModel.IsTerminalViewOpen = false;
392+
_mainPageModel.TerminalCloseCommand.Execute(Tag);
387393
});
388394
};
389395

@@ -424,7 +430,6 @@ Task<string> GetTextAsync()
424430

425431
private void TerminalView_Unloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
426432
{
427-
Dispose();
428433
}
429434

430435
private async void TerminalView_ActualThemeChanged(Microsoft.UI.Xaml.FrameworkElement sender, object args)
@@ -434,8 +439,7 @@ private async void TerminalView_ActualThemeChanged(Microsoft.UI.Xaml.FrameworkEl
434439

435440
var serializerSettings = new JsonSerializerOptions();
436441
serializerSettings.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
437-
var profile = _mainPageModel.TerminalSelectedProfile;
438-
var theme = new DefaultValueProvider().GetPreInstalledThemes().First(x => x.Id == profile.TerminalThemeId);
442+
var theme = new DefaultValueProvider().GetPreInstalledThemes().First(x => x.Id == _profile.TerminalThemeId);
439443

440444
WebViewControl.CoreWebView2.Profile.PreferredColorScheme = (ActualTheme == Microsoft.UI.Xaml.ElementTheme.Dark) ? CoreWebView2PreferredColorScheme.Dark : CoreWebView2PreferredColorScheme.Light;
441445

src/Files.App/ViewModels/MainPageViewModel.cs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,22 @@ public bool ShouldPreviewPaneBeDisplayed
6969
public MainPageViewModel()
7070
{
7171
NavigateToNumberedTabKeyboardAcceleratorCommand = new RelayCommand<KeyboardAcceleratorInvokedEventArgs>(ExecuteNavigateToNumberedTabKeyboardAcceleratorCommand);
72-
TerminalToggleCommand = new RelayCommand(() => IsTerminalViewOpen = !IsTerminalViewOpen);
72+
TerminalAddCommand = new RelayCommand<ShellProfile>((e) =>
73+
{
74+
Terminals.Add(new TerminalView(e ?? TerminalSelectedProfile)
75+
{
76+
Tag = $"Terminal {Terminals.Count}"
77+
});
78+
OnPropertyChanged(nameof(SelectedTerminal));
79+
OnPropertyChanged(nameof(ActiveTerminal));
80+
OnPropertyChanged(nameof(TerminalNames));
81+
});
82+
TerminalToggleCommand = new RelayCommand(() =>
83+
{
84+
IsTerminalViewOpen = !IsTerminalViewOpen;
85+
if (IsTerminalViewOpen && Terminals.IsEmpty())
86+
TerminalAddCommand.Execute(TerminalSelectedProfile);
87+
});
7388
TerminalSyncUpCommand = new AsyncRelayCommand(async () =>
7489
{
7590
var context = Ioc.Default.GetRequiredService<IContentPageContext>();
@@ -82,8 +97,40 @@ public MainPageViewModel()
8297
if (context.Folder?.ItemPath is string currentFolder)
8398
SetTerminalFolder?.Invoke(currentFolder);
8499
});
100+
TerminalCloseCommand = new RelayCommand<string>((name) =>
101+
{
102+
var terminal = Terminals.First(x => x.Tag.ToString() == name);
103+
(terminal as IDisposable)?.Dispose();
104+
Terminals.Remove(terminal);
105+
SelectedTerminal = int.Min(SelectedTerminal, Terminals.Count - 1);
106+
OnPropertyChanged(nameof(ActiveTerminal));
107+
OnPropertyChanged(nameof(TerminalNames));
108+
});
85109
TerminalSelectedProfile = TerminalProfiles[0];
86110
GeneralSettingsService.PropertyChanged += GeneralSettingsService_PropertyChanged;
111+
PropertyChanged += MainPageViewModel_PropertyChanged;
112+
}
113+
114+
private void MainPageViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
115+
{
116+
if (e.PropertyName == nameof(ActiveTerminal))
117+
{
118+
if (ActiveTerminal is TerminalView termView)
119+
{
120+
GetTerminalFolder = termView.GetTerminalFolder;
121+
SetTerminalFolder = termView.SetTerminalFolder;
122+
}
123+
else
124+
{
125+
GetTerminalFolder = null;
126+
SetTerminalFolder = null;
127+
}
128+
}
129+
else if (e.PropertyName == nameof(SelectedTerminal))
130+
{
131+
if (Terminals.IsEmpty())
132+
IsTerminalViewOpen = false;
133+
}
87134
}
88135

89136
private void GeneralSettingsService_PropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -239,6 +286,8 @@ private void ExecuteNavigateToNumberedTabKeyboardAcceleratorCommand(KeyboardAcce
239286
public ICommand TerminalToggleCommand { get; init; }
240287
public ICommand TerminalSyncUpCommand { get; init; }
241288
public ICommand TerminalSyncDownCommand { get; init; }
289+
public IRelayCommand<string> TerminalCloseCommand { get; init; }
290+
public IRelayCommand<ShellProfile> TerminalAddCommand { get; init; }
242291

243292
public Func<Task<string?>>? GetTerminalFolder { get; set; }
244293
public Action<string>? SetTerminalFolder { get; set; }
@@ -254,6 +303,23 @@ public bool IsTerminalViewOpen
254303
set => SetProperty(ref _isTerminalViewOpen, value);
255304
}
256305

306+
public Control? ActiveTerminal => SelectedTerminal >= 0 && SelectedTerminal < Terminals.Count ? Terminals[SelectedTerminal] : null;
307+
308+
public List<Control> Terminals { get; } = new();
309+
public List<string> TerminalNames => Terminals.Select(x => x.Tag.ToString()!).ToList();
310+
311+
private int _selectedTerminal;
312+
public int SelectedTerminal
313+
{
314+
get => _selectedTerminal;
315+
set
316+
{
317+
if (value != -1)
318+
if (SetProperty(ref _selectedTerminal, value))
319+
OnPropertyChanged(nameof(ActiveTerminal));
320+
}
321+
}
322+
257323
private ShellProfile _terminalSelectedProfile;
258324
public ShellProfile TerminalSelectedProfile
259325
{

src/Files.App/Views/MainPage.xaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,11 +270,12 @@
270270
Unloaded="PreviewPane_Unloaded" />
271271

272272
<!-- Terminal -->
273-
<uc:TerminalView
273+
<ContentPresenter
274274
x:Name="TerminalControl"
275275
Grid.Row="4"
276276
Grid.ColumnSpan="3"
277-
x:Load="{x:Bind ViewModel.IsTerminalViewOpen, Mode=OneWay}" />
277+
Content="{x:Bind ViewModel.ActiveTerminal, Mode=OneWay}"
278+
Visibility="{x:Bind ViewModel.IsTerminalViewOpen, Mode=OneWay}" />
278279

279280
<!-- Status Bar -->
280281
<uc:StatusBar

0 commit comments

Comments
 (0)