Skip to content

Commit 35c79ac

Browse files
gave92Marco Gavelli
authored andcommitted
Multiple terminals
1 parent c68c356 commit 35c79ac

File tree

5 files changed

+198
-38
lines changed

5 files changed

+198
-38
lines changed

src/Files.App/UserControls/StatusBar.xaml

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,5 +341,105 @@
341341
</Button.Content>
342342
</Button>
343343

344+
<SplitButton
345+
x:Name="ToggleTerminal"
346+
Height="24"
347+
Padding="8,0,8,0"
348+
VerticalAlignment="Center"
349+
AutomationProperties.Name="Toggle terminal"
350+
Background="Transparent"
351+
BorderBrush="Transparent"
352+
Command="{x:Bind MainPageViewModel.TerminalToggleCommand}">
353+
<SplitButton.Content>
354+
<StackPanel Orientation="Horizontal" Spacing="8">
355+
<FontIcon FontSize="12" Glyph="&#xE756;" />
356+
<TextBlock Text="Terminal" />
357+
</StackPanel>
358+
</SplitButton.Content>
359+
<SplitButton.Flyout>
360+
<Flyout Placement="Top">
361+
<Grid
362+
Width="200"
363+
Height="160"
364+
Margin="-16">
365+
<Grid.RowDefinitions>
366+
<RowDefinition Height="Auto" />
367+
<RowDefinition Height="*" />
368+
</Grid.RowDefinitions>
369+
370+
<!-- Header -->
371+
<Grid
372+
Grid.Row="0"
373+
Padding="4,8,8,8"
374+
Background="{ThemeResource AcrylicBackgroundFillColorDefaultBrush}"
375+
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
376+
BorderThickness="0,0,0,1">
377+
378+
<SplitButton
379+
Height="24"
380+
Padding="8,0"
381+
HorizontalAlignment="Center"
382+
Command="{x:Bind MainPageViewModel.TerminalAddCommand, Mode=OneWay}">
383+
<SplitButton.Content>
384+
<TextBlock FontSize="12">
385+
<Run Text="Add" />
386+
<Run Text="{x:Bind MainPageViewModel.TerminalSelectedProfile.Name, Mode=OneWay}" />
387+
</TextBlock>
388+
</SplitButton.Content>
389+
<SplitButton.Flyout>
390+
<Flyout>
391+
<ListView
392+
x:Name="ShellProfileList"
393+
Margin="-16"
394+
Padding="4"
395+
DisplayMemberPath="Name"
396+
IsItemClickEnabled="True"
397+
ItemClick="ShellProfileList_ItemClick"
398+
ItemsSource="{x:Bind MainPageViewModel.TerminalProfiles, Mode=OneWay}"
399+
SelectionMode="None" />
400+
</Flyout>
401+
</SplitButton.Flyout>
402+
</SplitButton>
403+
</Grid>
404+
405+
<ListView
406+
Grid.Row="1"
407+
Padding="4"
408+
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
409+
IsItemClickEnabled="True"
410+
ItemsSource="{x:Bind MainPageViewModel.TerminalNames, Mode=OneWay}"
411+
SelectedIndex="{x:Bind MainPageViewModel.SelectedTerminal, Mode=TwoWay}"
412+
SelectionMode="Single">
413+
<ListView.ItemTemplate>
414+
<DataTemplate x:DataType="x:String">
415+
<Grid>
416+
<Grid.ColumnDefinitions>
417+
<ColumnDefinition Width="*" />
418+
<ColumnDefinition Width="Auto" />
419+
</Grid.ColumnDefinitions>
420+
<TextBlock
421+
VerticalAlignment="Center"
422+
Text="{x:Bind}"
423+
TextTrimming="CharacterEllipsis" />
424+
<Button
425+
Grid.Column="1"
426+
AutomationProperties.Name="{helpers:ResourceString Name=Close}"
427+
Background="Transparent"
428+
BorderBrush="Transparent"
429+
Click="Button_Click"
430+
Tag="{x:Bind}"
431+
ToolTipService.ToolTip="{helpers:ResourceString Name=Close}">
432+
<FontIcon FontSize="12" Glyph="&#xE74D;" />
433+
</Button>
434+
</Grid>
435+
</DataTemplate>
436+
</ListView.ItemTemplate>
437+
</ListView>
438+
</Grid>
439+
</Flyout>
440+
</SplitButton.Flyout>
441+
</SplitButton>
442+
</StackPanel>
443+
344444
</Grid>
345445
</UserControl>

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Files Community
22
// Licensed under the MIT License.
33

4+
using Files.App.Data.Commands;
5+
using Files.App.Utils.Terminal;
46
using Microsoft.UI.Xaml;
57
using Microsoft.UI.Xaml.Controls;
68

@@ -76,5 +78,15 @@ private async void DeleteBranch_Click(object sender, RoutedEventArgs e)
7678
BranchesFlyout.Hide();
7779
await StatusBarViewModel.ExecuteDeleteBranch(((BranchItem)((Button)sender).DataContext).Name);
7880
}
81+
82+
private void Button_Click(object sender, RoutedEventArgs e)
83+
{
84+
MainPageViewModel.TerminalCloseCommand.Execute(((Button)sender).Tag.ToString());
85+
}
86+
87+
private void ShellProfileList_ItemClick(object sender, ItemClickEventArgs e)
88+
{
89+
MainPageViewModel.TerminalAddCommand.Execute((ShellProfile)e.ClickedItem);
90+
}
7991
}
8092
}

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: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,22 @@ public MainPageViewModel()
172172
DismissReviewPromptCommand = new RelayCommand(ExecuteDismissReviewPromptCommand);
173173
SponsorCommand = new RelayCommand(ExecuteSponsorCommand);
174174
DismissSponsorPromptCommand = new RelayCommand(ExecuteDismissSponsorPromptCommand);
175-
TerminalToggleCommand = new RelayCommand(() => IsTerminalViewOpen = !IsTerminalViewOpen);
175+
TerminalAddCommand = new RelayCommand<ShellProfile>((e) =>
176+
{
177+
Terminals.Add(new TerminalView(e ?? TerminalSelectedProfile)
178+
{
179+
Tag = $"Terminal {Terminals.Count}"
180+
});
181+
OnPropertyChanged(nameof(SelectedTerminal));
182+
OnPropertyChanged(nameof(ActiveTerminal));
183+
OnPropertyChanged(nameof(TerminalNames));
184+
});
185+
TerminalToggleCommand = new RelayCommand(() =>
186+
{
187+
IsTerminalViewOpen = !IsTerminalViewOpen;
188+
if (IsTerminalViewOpen && Terminals.IsEmpty())
189+
TerminalAddCommand.Execute(TerminalSelectedProfile);
190+
});
176191
TerminalSyncUpCommand = new AsyncRelayCommand(async () =>
177192
{
178193
var context = Ioc.Default.GetRequiredService<IContentPageContext>();
@@ -185,6 +200,15 @@ public MainPageViewModel()
185200
if (context.Folder?.ItemPath is string currentFolder)
186201
SetTerminalFolder?.Invoke(currentFolder);
187202
});
203+
TerminalCloseCommand = new RelayCommand<string>((name) =>
204+
{
205+
var terminal = Terminals.First(x => x.Tag.ToString() == name);
206+
(terminal as IDisposable)?.Dispose();
207+
Terminals.Remove(terminal);
208+
SelectedTerminal = int.Min(SelectedTerminal, Terminals.Count - 1);
209+
OnPropertyChanged(nameof(ActiveTerminal));
210+
OnPropertyChanged(nameof(TerminalNames));
211+
});
188212
TerminalSelectedProfile = TerminalProfiles[0];
189213

190214
AppearanceSettingsService.PropertyChanged += (s, e) =>
@@ -417,6 +441,8 @@ private async void ExecuteNavigateToNumberedTabKeyboardAcceleratorCommand(Keyboa
417441
public ICommand TerminalToggleCommand { get; init; }
418442
public ICommand TerminalSyncUpCommand { get; init; }
419443
public ICommand TerminalSyncDownCommand { get; init; }
444+
public IRelayCommand<string> TerminalCloseCommand { get; init; }
445+
public IRelayCommand<ShellProfile> TerminalAddCommand { get; init; }
420446

421447
public Func<Task<string?>>? GetTerminalFolder { get; set; }
422448
public Action<string>? SetTerminalFolder { get; set; }
@@ -432,6 +458,23 @@ public bool IsTerminalViewOpen
432458
set => SetProperty(ref _isTerminalViewOpen, value);
433459
}
434460

461+
public Control? ActiveTerminal => SelectedTerminal >= 0 && SelectedTerminal < Terminals.Count ? Terminals[SelectedTerminal] : null;
462+
463+
public List<Control> Terminals { get; } = new();
464+
public List<string> TerminalNames => Terminals.Select(x => x.Tag.ToString()!).ToList();
465+
466+
private int _selectedTerminal;
467+
public int SelectedTerminal
468+
{
469+
get => _selectedTerminal;
470+
set
471+
{
472+
if (value != -1)
473+
if (SetProperty(ref _selectedTerminal, value))
474+
OnPropertyChanged(nameof(ActiveTerminal));
475+
}
476+
}
477+
435478
private ShellProfile _terminalSelectedProfile;
436479
public ShellProfile TerminalSelectedProfile
437480
{

src/Files.App/Views/MainPage.xaml

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

259259
<!-- Terminal -->
260-
<uc:TerminalView
260+
<ContentPresenter
261261
x:Name="TerminalControl"
262262
Grid.Row="4"
263263
Grid.ColumnSpan="3"
264-
x:Load="{x:Bind ViewModel.IsTerminalViewOpen, Mode=OneWay}" />
264+
Content="{x:Bind ViewModel.ActiveTerminal, Mode=OneWay}"
265+
Visibility="{x:Bind ViewModel.IsTerminalViewOpen, Mode=OneWay}" />
265266

266267
<!-- Status Bar -->
267268
<uc:StatusBarControl

0 commit comments

Comments
 (0)