Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
b3a1437
[WIP]
gave92 Oct 24, 2023
36e9230
[WIP]
gave92 Oct 24, 2023
b087936
[WIP]
gave92 Oct 25, 2023
07788a2
Initial work
gave92 Oct 25, 2023
d640652
Add build files
gave92 Oct 25, 2023
d33375f
Added credits to source files
gave92 Oct 25, 2023
6fcd398
Add build files for xterm.js
gave92 Oct 26, 2023
c08e973
Implement copy and resize
gave92 Oct 26, 2023
19b24fa
Set background and hide double scrollbars
gave92 Oct 27, 2023
8f2b1bd
Toggle button & cleanup
gave92 Oct 27, 2023
1336b9f
Start from current folder
gave92 Oct 27, 2023
2006eec
Sync folder to cmd
gave92 Oct 28, 2023
4a031f1
Implemented up sync button
gave92 Oct 28, 2023
aed9ed5
Enable profile selection
gave92 Oct 28, 2023
1da9b0e
Updated comments
gave92 Oct 29, 2023
4510555
Test theme change
gave92 Oct 29, 2023
5120ca9
Place classes in project structure
gave92 Nov 1, 2023
e7de9d1
Added missing attribution
gave92 Nov 1, 2023
0e3f24e
Fix webview scrollbars in dark mode
gave92 Nov 1, 2023
40e80d2
Moved Terminal under Utils
gave92 Nov 1, 2023
a1c4341
Removed extra colon
gave92 Nov 1, 2023
d65c409
Fix Release build
gave92 Nov 1, 2023
fa19bc3
Set background color in html
gave92 Nov 1, 2023
b33f483
Fix buttons for wsl
gave92 Nov 5, 2023
73fc955
Fix webview flashing white
gave92 Nov 5, 2023
67c8453
Remove extra commands
gave92 Apr 13, 2024
a5945d1
Improve scrollbar
gave92 Apr 13, 2024
283f134
Use windows style scrollbars
gave92 Apr 13, 2024
e4443eb
Code suggestions
gave92 Apr 17, 2024
93d424e
Switch native methods to CsWin32
gave92 Apr 17, 2024
6f94aca
Added settings under experimental
May 1, 2024
0c42061
WIP
May 1, 2024
0344a51
Call AddWebAllowedObject after navigation
gave92 May 8, 2024
4a59490
Multiple terminals
gave92 May 8, 2024
47fb2d4
Hide pane when no terminals
gave92 May 8, 2024
21e80a6
Switch to CsWin32 for process api
gave92 May 11, 2024
4dfbc07
Add terminal model
gave92 May 11, 2024
f199939
Minor change
gave92 May 11, 2024
4e643dd
Minor change
gave92 May 11, 2024
cb629aa
Added margin
yair100 May 28, 2024
5c80512
Readded Newtonsoft.Json dep
gave92 Jun 18, 2024
099b4fa
Stupid solution n.1
gave92 Jun 19, 2024
a899a7a
Switched WebView2Extensions to System.Text.Json
gave92 Jun 21, 2024
dd371b4
Switch status bar with terminal
yair100 Jun 26, 2024
8ab6c80
Remove duplicates from NativeMethods.txt
gave92 Dec 14, 2024
f18cb8a
Remove duplicate SetWindowLongPtr
Oct 29, 2025
ca4f32d
Fix build errors
Oct 30, 2025
e923d76
Restore functionalities
Oct 31, 2025
c3fafb8
Restore sync path functionality
Oct 31, 2025
12886be
Refactor WebView2 binding and terminal fixes
yair100 May 5, 2026
c5ac362
Updated
yair100 May 5, 2026
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
23 changes: 23 additions & 0 deletions src/Files.App.Controls/GridSplitter/GridSplitter.Events.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,29 @@ protected override void OnManipulationCompleted(ManipulationCompletedRoutedEvent
base.OnManipulationCompleted(e);
}

/// <inheritdoc />
protected override void OnPointerEntered(PointerRoutedEventArgs e)
{
if (_resizeDirection == GridResizeDirection.Auto)
_resizeDirection = GetResizeDirection();

if (PreviousCursor is null)
PreviousCursor = ProtectedCursor;

ProtectedCursor = _resizeDirection == GridResizeDirection.Rows
? RowSplitterCursor
: ColumnsSplitterCursor;

base.OnPointerEntered(e);
}

/// <inheritdoc />
protected override void OnPointerExited(PointerRoutedEventArgs e)
{
ProtectedCursor = PreviousCursor;
base.OnPointerExited(e);
}

/// <inheritdoc />
protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e)
{
Expand Down
28 changes: 23 additions & 5 deletions src/Files.App.CsWin32/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ WNDCLASSEXW
RegisterClassEx
CreateWindowEx
DestroyWindow
ShowWindow
GetModuleHandle
RECT
NOTIFYICONIDENTIFIER
Expand Down Expand Up @@ -115,7 +116,6 @@ WindowsCreateString
WindowsDeleteString
IPreviewHandler
AssocQueryString
GetModuleHandle
SHEmptyRecycleBin
SHFileOperation
SHGetFolderPath
Expand All @@ -137,15 +137,12 @@ COMPRESSION_FORMAT
FILE_ACCESS_RIGHTS
FindFirstFileEx
FindNextFile
CreateFile
GetFileSizeEx
WIN32_FIND_DATAW
FILE_ACCESS_RIGHTS
SHAddToRecentDocs
SHARD
BHID_EnumItems
FOLDERID_RecycleBinFolder
CoTaskMemFree
SHGetIDListFromObject
SHCreateItemFromIDList
BHID_SFUIObject
Expand All @@ -160,7 +157,6 @@ IApplicationDocumentLists
ApplicationDocumentLists
IApplicationActivationManager
MENU_ITEM_TYPE
COMPRESSION_FORMAT
FSCTL_SET_COMPRESSION
FSCTL_DISMOUNT_VOLUME
FSCTL_LOCK_VOLUME
Expand Down Expand Up @@ -266,3 +262,25 @@ HCERTSTORE
CERT_QUERY_ENCODING_TYPE
CertGetNameString
MessageBox
// Console api
AllocConsole
GetConsoleWindow
GetStdHandle
SetConsoleMode
GetConsoleMode
COORD
CreatePseudoConsole
ResizePseudoConsole
ClosePseudoConsole
CreatePipe
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
STARTUPINFOEXW
STARTUPINFOW
PROCESS_CREATION_FLAGS
PROCESS_INFORMATION
SECURITY_ATTRIBUTES
CloseHandle
DeleteProcThreadAttributeList
UpdateProcThreadAttribute
InitializeProcThreadAttributeList
CreateProcess
60 changes: 60 additions & 0 deletions src/Files.App/Actions/Show/ToggleTerminalPaneAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

namespace Files.App.Actions
{
[GeneratedRichCommand]
internal sealed partial class ToggleTerminalPaneAction : ObservableObject, IToggleAction
{
private readonly IGeneralSettingsService generalSettingsService = Ioc.Default.GetRequiredService<IGeneralSettingsService>();
private readonly MainPageViewModel mainPageViewModel = Ioc.Default.GetRequiredService<MainPageViewModel>();

public string Label
=> Strings.ToggleTerminalPane.GetLocalizedResource();

public string Description
=> Strings.ToggleTerminalPaneDescription.GetLocalizedResource();

public ActionCategory Category
=> ActionCategory.Show;

public RichGlyph Glyph
=> new("\uE756");

public bool IsAccessibleGlobally
=> AppLifecycleHelper.AppEnvironment is AppEnvironment.Dev && generalSettingsService.IsTerminalIntegrationEnabled;

public bool IsExecutable
=> AppLifecycleHelper.AppEnvironment is AppEnvironment.Dev && generalSettingsService.IsTerminalIntegrationEnabled;

public bool IsOn
=> mainPageViewModel.IsTerminalViewOpen;

public ToggleTerminalPaneAction()
{
generalSettingsService.PropertyChanged += GeneralSettingsService_PropertyChanged;
mainPageViewModel.PropertyChanged += MainPageViewModel_PropertyChanged;
}

public Task ExecuteAsync(object? parameter = null)
{
mainPageViewModel.TerminalToggleCommand.Execute(null);
return Task.CompletedTask;
}

private void GeneralSettingsService_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(GeneralSettingsService.IsTerminalIntegrationEnabled))
{
OnPropertyChanged(nameof(IsExecutable));
OnPropertyChanged(nameof(IsAccessibleGlobally));
}
}

private void MainPageViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(MainPageViewModel.IsTerminalViewOpen))
OnPropertyChanged(nameof(IsOn));
}
}
}
5 changes: 5 additions & 0 deletions src/Files.App/Data/Contracts/IGeneralSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@ public interface IGeneralSettingsService : IBaseSettingsService, INotifyProperty
/// </summary>
bool ShowSystemTrayIcon { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to enable terminal integration.
/// </summary>
bool IsTerminalIntegrationEnabled { get; set; }

/// <summary>
/// Gets or sets a value indicating the default option to resolve conflicts.
/// </summary>
Expand Down
21 changes: 21 additions & 0 deletions src/Files.App/Data/Models/TerminalModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Files.App.Data.Models
{
public class TerminalModel : IDisposable
{
public string Id { get; init; }
public string Name { get; init; }
public Control Control { get; init; }

public void Dispose()
{
(Control as IDisposable)?.Dispose();
}
}
}
8 changes: 8 additions & 0 deletions src/Files.App/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ public static string WithEnding(this string str, string ending)
return result;
}

/// <summary>
/// Compares two strings for equality, but assumes that null string is equal to an empty string.
/// </summary>
public static bool NullableEqualTo(this string original, string other,
StringComparison stringComparison = StringComparison.Ordinal) => string.IsNullOrEmpty(original)
? string.IsNullOrEmpty(other)
: original.Equals(other, stringComparison);

private static readonly ResourceMap resourcesTree = new ResourceManager().MainResourceMap.TryGetSubtree("Resources");

private static readonly ConcurrentDictionary<string, string> cachedResources = new();
Expand Down
149 changes: 149 additions & 0 deletions src/Files.App/Extensions/WebView2Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright (c) Mahmoud Al-Qudsi, NeoSmart Technoogies. All rights reserved.
// Licensed under the MIT License.

using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using System.Reflection;
using System.Text;
using System.Text.Encodings.Web;
using Windows.Foundation;

namespace Files.App.Extensions
{
using WebViewMessageReceivedHandler = TypedEventHandler<WebView2, CoreWebView2WebMessageReceivedEventArgs>;

/// <summary>
/// Code modified from https://gist.github.com/mqudsi/ceb4ecee76eb4c32238a438664783480
/// </summary>
public static class WebView2Extensions
{
public static void Navigate(this WebView2 webview, Uri url)
{
webview.Source = url;
}

private struct WebMessage
{
public Guid Guid { get; set; }
}

private struct MethodWebMessage
{
public long Id { get; set; }
public string Method { get; set; }
public string Args { get; set; }
}

private static readonly JsonSerializerOptions _caseInsensitiveOptions = new() { PropertyNameCaseInsensitive = true };
private static readonly JsonSerializerOptions _unsafeRelaxedOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

public static List<WebViewMessageReceivedHandler> _handlers = new();
public static async Task AddWebAllowedObject<T>(this WebView2 webview, string name, T @object)
{
var sb = new StringBuilder();
sb.AppendLine($"window.{name} = {{ ");

var methodsGuid = Guid.NewGuid();
var methodInfo = typeof(T).GetMethods(BindingFlags.Public | BindingFlags.Instance);
var methods = new Dictionary<string, MethodInfo>(methodInfo.Length);
foreach (var method in methodInfo)
{
var functionName = $"{char.ToLower(method.Name[0])}{method.Name.Substring(1)}";
sb.AppendLine($@"{functionName}: function() {{ window.chrome.webview.postMessage(JSON.stringify({{ guid: ""{methodsGuid}"", id: this._callbackIndex++, method: ""{functionName}"", args: JSON.stringify([...arguments]) }})); const promise = new Promise((accept, reject) => this._callbacks.set(this._callbackIndex, {{ accept: accept, reject: reject }})); return promise; }},");
methods.Add(functionName, method);
}

sb.AppendLine($@"_callbacks: new Map(),");
sb.Append($@"_callbackIndex: 0,");
sb.AppendLine("}");

try
{
await webview.ExecuteScriptAsync($"{sb}").AsTask();
}
catch (Exception)
{
}

var handler = (WebViewMessageReceivedHandler)(async (_, e) =>
{
var rawMessage = e.TryGetWebMessageAsString();
var message = JsonSerializer.Deserialize<WebMessage>(rawMessage, _caseInsensitiveOptions);
if (message.Guid != methodsGuid)
return;

var methodMessage = JsonSerializer.Deserialize<MethodWebMessage>(rawMessage, _caseInsensitiveOptions);
var method = methods[methodMessage.Method];
try
{
var args = JsonSerializer.Deserialize<JsonElement[]>(methodMessage.Args).Zip(method.GetParameters(), (val, args) => new { val, args.ParameterType }).Select(item => item.val.Deserialize(item.ParameterType));
var result = method.Invoke(@object, args.ToArray());
if (result is object)
{
var resultType = result.GetType();
dynamic task = null;
if (resultType.Name.StartsWith("TaskToAsyncOperationAdapter")
|| resultType.IsInstanceOfType(typeof(IAsyncInfo)))
{
if (resultType.GenericTypeArguments.Length > 0)
{
var asTask = typeof(WindowsRuntimeSystemExtensions)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(method => method.GetParameters().Length == 1
&& method.Name == "AsTask"
&& method.ToString().Contains("Windows.Foundation.IAsyncOperation`1[TResult]"))
.FirstOrDefault();

asTask = asTask.MakeGenericMethod(resultType.GenericTypeArguments[0]);
task = (Task)asTask.Invoke(null, new[] { result });
}
else
{
task = WindowsRuntimeSystemExtensions.AsTask((dynamic)result);
}
}
else
{
var awaiter = resultType.GetMethod(nameof(Task.GetAwaiter));
if (awaiter is object)
task = result;
}
if (task is object)
result = await task;
}
var json = JsonSerializer.Serialize(result, _unsafeRelaxedOptions);
await webview.ExecuteScriptAsync($@"{name}._callbacks.get({methodMessage.Id}).accept(JSON.parse({json})); {name}._callbacks.delete({methodMessage.Id});");
}
catch (Exception ex)
{
var json = JsonSerializer.Serialize(ex, _unsafeRelaxedOptions);
await webview.ExecuteScriptAsync($@"{name}._callbacks.get({methodMessage.Id}).reject(JSON.parse({json})); {name}._callbacks.delete({methodMessage.Id});");
}
});

_handlers.Add(handler);
webview.WebMessageReceived += handler;
}

public static async Task<string> InvokeScriptAsync(this WebView2 webview, string function, params object[] args)
{
var array = JsonSerializer.Serialize(args, _unsafeRelaxedOptions);
string result = null;
await webview.DispatcherQueue.EnqueueAsync(async () =>
{
var script = $"{function}(...{array});";
try
{
result = await webview.ExecuteScriptAsync(script).AsTask();
result = JsonSerializer.Deserialize<string>(result);
}
catch (Exception)
{
}
}, Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal);

return result;
}
}
}
6 changes: 5 additions & 1 deletion src/Files.App/Files.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<PropertyGroup>
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
</PropertyGroup>

<ItemGroup>
<Manifest Include="app.manifest" />
<Content Update="Assets\Resources\**">
Expand All @@ -65,6 +65,10 @@
<Content Include="7zArm64.dll" Condition="'$(Platform)' == 'arm64'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Utils\Terminal\UI\bundle.js" />
<Content Include="Utils\Terminal\UI\index.html" />
<Content Include="Utils\Terminal\UI\style.css" />
<Content Include="Utils\Terminal\UI\xterm.css" />
</ItemGroup>

<ItemGroup>
Expand Down
6 changes: 6 additions & 0 deletions src/Files.App/Services/Settings/GeneralSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,12 @@ public bool ShowSystemTrayIcon
set => Set(value);
}

public bool IsTerminalIntegrationEnabled
{
get => Get(false);
set => Set(value);
}

public FileNameConflictResolveOptionType ConflictsResolveOption
{
get => (FileNameConflictResolveOptionType)Get((long)FileNameConflictResolveOptionType.GenerateNewName);
Expand Down
Loading
Loading