Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
8e0c129
[WIP]
gave92 Oct 24, 2023
e2c1356
[WIP]
gave92 Oct 24, 2023
5b74005
[WIP]
gave92 Oct 25, 2023
8e2d1ee
Initial work
gave92 Oct 25, 2023
b83f527
Add build files
gave92 Oct 25, 2023
1ac0f35
Added credits to source files
gave92 Oct 25, 2023
631d219
Add build files for xterm.js
gave92 Oct 26, 2023
b5b93fa
Implement copy and resize
gave92 Oct 26, 2023
c8750be
Set background and hide double scrollbars
gave92 Oct 27, 2023
dac93f8
Toggle button & cleanup
gave92 Oct 27, 2023
507dc23
Start from current folder
gave92 Oct 27, 2023
0216d9a
Sync folder to cmd
gave92 Oct 28, 2023
1ccbe4d
Implemented up sync button
gave92 Oct 28, 2023
0f71084
Enable profile selection
gave92 Oct 28, 2023
cd16860
Updated comments
gave92 Oct 29, 2023
2c3243d
Test theme change
gave92 Oct 29, 2023
28b5fe6
Place classes in project structure
gave92 Nov 1, 2023
9e5df75
Added missing attribution
gave92 Nov 1, 2023
ccc7379
Fix webview scrollbars in dark mode
gave92 Nov 1, 2023
aa802f0
Moved Terminal under Utils
gave92 Nov 1, 2023
b459fb1
Removed extra colon
gave92 Nov 1, 2023
966bfca
Fix Release build
gave92 Nov 1, 2023
017df14
Set background color in html
gave92 Nov 1, 2023
05b8260
Fix buttons for wsl
gave92 Nov 5, 2023
525e6a4
Fix webview flashing white
gave92 Nov 5, 2023
12dfeca
Remove extra commands
gave92 Apr 13, 2024
b33da03
Improve scrollbar
gave92 Apr 13, 2024
db2c4f6
Use windows style scrollbars
gave92 Apr 13, 2024
dbb2092
Code suggestions
gave92 Apr 17, 2024
63bf435
Switch native methods to CsWin32
gave92 Apr 17, 2024
95e2930
Added settings under experimental
May 1, 2024
762b298
WIP
May 1, 2024
c68c356
Call AddWebAllowedObject after navigation
gave92 May 8, 2024
35c79ac
Multiple terminals
gave92 May 8, 2024
7c520c5
Hide pane when no terminals
gave92 May 8, 2024
04c9ec0
Switch to CsWin32 for process api
gave92 May 11, 2024
ea52bf8
Add terminal model
gave92 May 11, 2024
7e1d250
Minor change
gave92 May 11, 2024
895bacf
Minor change
gave92 May 11, 2024
e124f24
Added margin
yaira2 May 28, 2024
ed29c7e
Readded Newtonsoft.Json dep
gave92 Jun 18, 2024
9680671
Stupid solution n.1
gave92 Jun 19, 2024
e661d8e
Switched WebView2Extensions to System.Text.Json
gave92 Jun 21, 2024
b0521aa
Switch status bar with terminal
yaira2 Jun 26, 2024
385ee9a
Remove duplicates from NativeMethods.txt
gave92 Dec 14, 2024
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
28 changes: 23 additions & 5 deletions src/Files.App.CsWin32/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ SendMessage
IsWindowVisible
COPYDATASTRUCT
WINDOW_LONG_PTR_INDEX
SetWindowLongPtr
CallWindowProc
MINMAXINFO
SUBCLASSPROC
Expand All @@ -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 @@ WINTRUST_DATA
HCERTSTORE
CERT_QUERY_ENCODING_TYPE
CertGetNameString
// 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
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 @@ -270,6 +270,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
222 changes: 222 additions & 0 deletions src/Files.App/Extensions/WebView2Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// 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 enum PropertyAction
{
Read = 0,
Write = 1,
}

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 struct PropertyWebMessage
{
public long Id { get; set; }
public string Property { get; set; }
public PropertyAction Action { get; set; }
public string Value { get; set; }
}

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);
}

var propertiesGuid = Guid.NewGuid();
var propertyInfo = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
var properties = new Dictionary<string, PropertyInfo>(propertyInfo.Length);
//foreach (var property in propertyInfo)
//{
// var propertyName = $"{char.ToLower(property.Name[0])}{property.Name.Substring(1)}";
// if (property.CanRead)
// {
// sb.AppendLine($@"get {propertyName}() {{ window.chrome.webview.postMessage(JSON.stringify({{ guid: ""{propertiesGuid}"", id: this._callbackIndex++, property: ""{propertyName}"", action: ""{(int) PropertyAction.Read}"" }})); const promise = new Promise((accept, reject) => this._callbacks.set(this._callbackIndex, {{ accept: accept, reject: reject }})); return promise; }},");
// }
// if (property.CanWrite)
// {
// sb.AppendLine($@"set {propertyName}(value) {{ window.chrome.webview.postMessage(JSON.stringify({{ guid: ""{propertiesGuid}"", id: this._callbackIndex++, property: ""{propertyName}"", action: ""{(int)PropertyAction.Write}"", value: JSON.stringify(value) }})); const promise = new Promise((accept, reject) => this._callbacks.set(this._callbackIndex, {{ accept: accept, reject: reject }})); return promise; }},");
// }
// properties[propertyName] = property;
//}

// Add a map<int, (promiseAccept, promiseReject)> to the object used to resolve results
sb.AppendLine($@"_callbacks: new Map(),");
// And a shared counter to index into that map
sb.Append($@"_callbackIndex: 0,");

sb.AppendLine("}");

try
{
//await webview.ExecuteScriptAsync($"try {{ {sb} }} catch (ex) {{ console.error(ex); }}").AsTask();
await webview.ExecuteScriptAsync($"{sb}").AsTask();
}
catch (Exception ex)
{
// So we can see it in the JS debugger
}

var handler = (WebViewMessageReceivedHandler)(async (_, e) =>
{
var message = JsonSerializer.Deserialize<WebMessage>(e.TryGetWebMessageAsString(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
if (message.Guid == methodsGuid)
{

var methodMessage = JsonSerializer.Deserialize<MethodWebMessage>(e.TryGetWebMessageAsString(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
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)))
{
// IAsyncOperation that needs to be converted to a task first
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();

//var asTask = typeof(WindowsRuntimeSystemExtensions)
// .GetMethod(nameof(WindowsRuntimeSystemExtensions.AsTask),
// new[] { typeof(IAsyncOperation<>).MakeGenericType(resultType.GenericTypeArguments[0]) }
// );

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, new JsonSerializerOptions() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); ;
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, new JsonSerializerOptions() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
await webview.ExecuteScriptAsync($@"{name}._callbacks.get({methodMessage.Id}).reject(JSON.parse({json})); {name}._callbacks.delete({methodMessage.Id});");
//throw;
}
}
else if (message.Guid == propertiesGuid)
{
var propertyMessage = JsonSerializer.Deserialize<PropertyWebMessage>(e.TryGetWebMessageAsString(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
var property = properties[propertyMessage.Property];
try
{
object result;
if (propertyMessage.Action == PropertyAction.Read)
{
result = property.GetValue(@object);
}
else
{
var value = JsonSerializer.Deserialize(propertyMessage.Value, property.PropertyType);
property.SetValue(@object, value);
result = new object();
}

var json = JsonSerializer.Serialize(result, new JsonSerializerOptions() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
await webview.ExecuteScriptAsync($@"{name}._callbacks.get({propertyMessage.Id}).accept(JSON.parse({json})); {name}._callbacks.delete({propertyMessage.Id});");
}
catch (Exception ex)
{
//var json = JsonSerializer.Serialize(ex, new JsonSerializerOptions() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
//await webview.ExecuteScriptAsync($@"{name}._callbacks.get({propertyMessage.Id}).reject(JSON.parse({json})); {name}._callbacks.delete({propertyMessage.Id});");
//throw;
}
}
});

_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, new JsonSerializerOptions() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
string result = null;
// Tested and checked: this dispatch is required, even though the web view is in a different process
await webview.DispatcherQueue.EnqueueAsync(async () =>
{
var script = $"{function}(...{array});";
try
{
result = await webview.ExecuteScriptAsync(script).AsTask();
result = JsonSerializer.Deserialize<string>(result);
}
catch (Exception ex)
{
}
}, Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal);

return result;
}
}
}
4 changes: 4 additions & 0 deletions src/Files.App/Files.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
<Content Include="7zArm64.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Terminal\UI\bundle.js" />
<Content Include="Terminal\UI\index.html" />
<Content Include="Terminal\UI\style.css" />
<Content Include="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 @@ -333,6 +333,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
3 changes: 3 additions & 0 deletions src/Files.App/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -4332,4 +4332,7 @@
<data name="FailedToOpenLogFile" xml:space="preserve">
<value>Unable to open the log file</value>
</data>
<data name="SettingsTerminalIntegration" xml:space="preserve">
<value>Enable Terminal integration</value>
</data>
</root>
3 changes: 2 additions & 1 deletion src/Files.App/UserControls/FilePreviews/HtmlPreview.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
x:Name="WebViewControl"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Loaded="WebViewControl_Loaded" />
DefaultBackgroundColor="Transparent"
Loaded="WebViewControl_LoadedAsync" />

</Border>
</UserControl>
Loading
Loading