Skip to content

Commit 158235f

Browse files
committed
Merge branch 'master' into feat-pdf
2 parents f6d1713 + 490f7aa commit 158235f

File tree

9 files changed

+647
-0
lines changed

9 files changed

+647
-0
lines changed

BootstrapBlazor.Extensions.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
<Project Path="src/components/BootstrapBlazor.TableExport/BootstrapBlazor.TableExport.csproj" />
8686
<Project Path="src/components/BootstrapBlazor.TagHelper/BootstrapBlazor.TagHelper.csproj" />
8787
<Project Path="src/components/BootstrapBlazor.Tasks.Dashboard/BootstrapBlazor.Tasks.Dashboard.csproj" />
88+
<Project Path="src/components/BootstrapBlazor.Term/BootstrapBlazor.Term.csproj" />
8889
<Project Path="src/components/BootstrapBlazor.Topology/BootstrapBlazor.Topology.csproj" />
8990
<Project Path="src/components/BootstrapBlazor.UniverIcon/BootstrapBlazor.UniverIcon.csproj" />
9091
<Project Path="src/components/BootstrapBlazor.UniverSheet/BootstrapBlazor.UniverSheet.csproj" />
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Razor">
2+
3+
<PropertyGroup>
4+
<Version>10.0.0</Version>
5+
</PropertyGroup>
6+
7+
<PropertyGroup>
8+
<PackageTags>Bootstrap Blazor WebAssembly wasm UI Components Xterm Term Terminal</PackageTags>
9+
<Description>Bootstrap UI components extensions of Term (xterm.js)</Description>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="BootstrapBlazor" Version="$(BBVersion)" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<Using Include="Microsoft.JSInterop" />
18+
</ItemGroup>
19+
20+
</Project>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@namespace BootstrapBlazor.Components
2+
@inherits BootstrapModuleComponentBase
3+
4+
<div @attributes="AdditionalAttributes" id="@Id" class="@ClassString" style="@StyleString"></div>
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
// Website: https://www.blazor.zone or https://argozhang.github.io/
4+
5+
using Microsoft.AspNetCore.Components;
6+
using System.Buffers;
7+
8+
namespace BootstrapBlazor.Components;
9+
10+
/// <summary>
11+
/// <para lang="zh">Term 终端组件</para>
12+
/// <para lang="en">Term Component</para>
13+
/// </summary>
14+
[JSModuleAutoLoader("./_content/BootstrapBlazor.Term/Components/Term.razor.js", JSObjectReference = true)]
15+
public partial class Term
16+
{
17+
/// <summary>
18+
/// <para lang="zh">获得/设置 <see cref="TermOptions"/> 实例 默认 null</para>
19+
/// <para lang="en">Gets or sets the <see cref="TermOptions"/> instance Default value is null.</para>
20+
/// </summary>
21+
[Parameter]
22+
public TermOptions? Options { get; set; }
23+
24+
/// <summary>
25+
/// <para lang="zh">获得/设置 收到数据回调</para>
26+
/// <para lang="en">Gets or sets the callback when data is received.</para>
27+
/// </summary>
28+
[Parameter]
29+
public Func<byte[], Task>? OnReceivedAsync { get; set; }
30+
31+
/// <summary>
32+
/// <para lang="zh">获得/设置 终端 Resize 回调</para>
33+
/// <para lang="en">Gets or sets the callback when terminal is resized.</para>
34+
/// </summary>
35+
[Parameter]
36+
public Func<int, int, Task>? OnResize { get; set; }
37+
38+
/// <summary>
39+
/// <para lang="zh">获得/设置 高度 默认 300px</para>
40+
/// <para lang="en">Gets or sets the height. Default is 300px.</para>
41+
/// </summary>
42+
[Parameter]
43+
public string Height { get; set; } = "300px";
44+
45+
/// <summary>
46+
/// <para lang="zh">获得 终端行数</para>
47+
/// <para lang="en">Gets the number of rows in the terminal.</para>
48+
/// </summary>
49+
public int Rows { get; private set; }
50+
51+
/// <summary>
52+
/// <para lang="zh">获得 终端列数</para>
53+
/// <para lang="en">Gets the number of columns in the terminal.</para>
54+
/// </summary>
55+
public int Columns { get; private set; }
56+
57+
private string? ClassString => CssBuilder.Default("bb-term")
58+
.AddClassFromAttributes(AdditionalAttributes)
59+
.Build();
60+
61+
private string? StyleString => CssBuilder.Default()
62+
.AddClass($"height: {Height};", !string.IsNullOrEmpty(Height))
63+
.AddStyleFromAttributes(AdditionalAttributes)
64+
.Build();
65+
66+
/// <summary>
67+
/// <inheritdoc/>
68+
/// </summary>
69+
protected override async Task InvokeInitAsync()
70+
{
71+
await InvokeVoidAsync("init", Id, Interop, Options);
72+
}
73+
74+
/// <summary>
75+
/// <para lang="zh">写入数据</para>
76+
/// <para lang="en">Writes data to the terminal.</para>
77+
/// </summary>
78+
/// <param name="data"></param>
79+
public async Task Write(string data)
80+
{
81+
await InvokeVoidAsync("write", Id, data);
82+
}
83+
84+
/// <summary>
85+
/// <para lang="zh">写入一行文本数据</para>
86+
/// <para lang="en">Writes a line of data to the terminal.</para>
87+
/// </summary>
88+
/// <param name="data"></param>
89+
public async Task WriteLine(string data)
90+
{
91+
await InvokeVoidAsync("writeln", Id, data);
92+
}
93+
94+
/// <summary>
95+
/// <para lang="zh">写入数据 (<see langword="byte[]"/>)</para>
96+
/// <para lang="en">Writes byte array data to the terminal.</para>
97+
/// </summary>
98+
/// <param name="data"></param>
99+
public async Task Write(byte[] data)
100+
{
101+
await InvokeVoidAsync("write", Id, data);
102+
}
103+
104+
/// <summary>
105+
/// <para lang="zh">写入数据 (<see langword="Memory[]"/>)</para>
106+
/// <para lang="en">Writes data to the terminal.</para>
107+
/// </summary>
108+
/// <param name="data"></param>
109+
public async Task Write(Memory<byte> data)
110+
{
111+
await InvokeVoidAsync("write", Id, data);
112+
}
113+
114+
/// <summary>
115+
/// <para lang="zh">清空终端</para>
116+
/// <para lang="en">Clears the terminal.</para>
117+
/// </summary>
118+
public async Task Clear()
119+
{
120+
await InvokeVoidAsync("clear", Id);
121+
}
122+
123+
/// <summary>
124+
/// <para lang="zh">连接流</para>
125+
/// <para lang="en">Connects a stream.</para>
126+
/// </summary>
127+
/// <param name="stream"></param>
128+
public Task Open(Stream stream) => ReadStreamAsync(stream);
129+
130+
private Stream? _stream;
131+
private CancellationTokenSource? _cancellationTokenSource;
132+
133+
private async Task ReadStreamAsync(Stream stream)
134+
{
135+
if (stream is { CanRead: true })
136+
{
137+
_ = Task.Run(() => LoopRead(stream));
138+
}
139+
}
140+
141+
private async Task LoopRead(Stream stream)
142+
{
143+
_stream = stream;
144+
145+
if (_cancellationTokenSource is { IsCancellationRequested: false })
146+
{
147+
_cancellationTokenSource.Cancel();
148+
_cancellationTokenSource.Dispose();
149+
}
150+
151+
try
152+
{
153+
_cancellationTokenSource = new();
154+
using var memoryOwner = MemoryPool<byte>.Shared.Rent(1024);
155+
var buffer = memoryOwner.Memory;
156+
while (_cancellationTokenSource is { IsCancellationRequested: false })
157+
{
158+
if (stream is { CanRead: true })
159+
{
160+
var length = await stream.ReadAsync(buffer, _cancellationTokenSource.Token);
161+
if (length == 0)
162+
{
163+
await Task.Delay(50);
164+
continue;
165+
}
166+
await Write(buffer.Slice(0, length).ToArray());
167+
}
168+
else
169+
{
170+
break;
171+
}
172+
}
173+
}
174+
catch (OperationCanceledException)
175+
{
176+
// ignored
177+
}
178+
catch (Exception ex)
179+
{
180+
await WriteLine($"\r\nError: {ex.Message}");
181+
}
182+
}
183+
184+
/// <summary>
185+
/// <para lang="zh">收到数据回调方法由 JavaScript 调用</para>
186+
/// <para lang="en">Callback when data is received from JS</para>
187+
/// </summary>
188+
/// <param name="data"></param>
189+
[JSInvokable]
190+
public async Task TriggerReceiveDataAsync(byte[] data)
191+
{
192+
if (_stream is { CanWrite: true })
193+
{
194+
await _stream.WriteAsync(data);
195+
await _stream.FlushAsync();
196+
}
197+
198+
if (OnReceivedAsync != null)
199+
{
200+
await OnReceivedAsync(data);
201+
}
202+
}
203+
204+
/// <summary>
205+
/// <para lang="zh">Resize JSInvoke</para>
206+
/// <para lang="en">Callback when terminal is resized from JS.</para>
207+
/// </summary>
208+
/// <param name="rows"></param>
209+
/// <param name="cols"></param>
210+
[JSInvokable]
211+
public async Task TriggerResizeAsync(int rows, int cols)
212+
{
213+
Rows = rows;
214+
Columns = cols;
215+
216+
if (OnResize != null)
217+
{
218+
await OnResize(rows, cols);
219+
}
220+
}
221+
222+
/// <summary>
223+
/// <inheritdoc/>
224+
/// </summary>
225+
/// <param name="disposing"></param>
226+
protected override async ValueTask DisposeAsync(bool disposing)
227+
{
228+
await base.DisposeAsync(disposing);
229+
230+
if (disposing)
231+
{
232+
_cancellationTokenSource?.Cancel();
233+
_cancellationTokenSource?.Dispose();
234+
_cancellationTokenSource = null;
235+
}
236+
}
237+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import "../lib/xterm.js";
2+
import "../lib/xterm-addon-fit.js";
3+
import { addLink } from '../../BootstrapBlazor/modules/utility.js'
4+
import Data from '../../BootstrapBlazor/modules/data.js'
5+
6+
export async function init(id, invoke, options) {
7+
await addLink('./_content/BootstrapBlazor.Term/lib/xterm.css');
8+
9+
const el = document.getElementById(id);
10+
if (el === null) {
11+
return;
12+
}
13+
14+
options = {
15+
... {
16+
fontFamily: "Consolas, 'Courier New', monospace",
17+
fontSize: 14,
18+
lineHeight: 1.0,
19+
},
20+
...options
21+
};
22+
23+
const term = new Terminal(options);
24+
const fitAddon = new FitAddon.FitAddon();
25+
term.loadAddon(fitAddon);
26+
27+
term.open(el);
28+
fitAddon.fit();
29+
30+
const encoder = new TextEncoder();
31+
term.onData(data => {
32+
invoke.invokeMethodAsync("TriggerReceiveDataAsync", encoder.encode(data));
33+
});
34+
35+
term.onResize(size => {
36+
invoke.invokeMethodAsync("TriggerResizeAsync", size.rows, size.cols);
37+
});
38+
39+
const resizeHandler = () => {
40+
try {
41+
fitAddon.fit();
42+
const dims = fitAddon.proposeDimensions();
43+
if (dims) {
44+
invoke.invokeMethodAsync("TriggerResizeAsync", dims.rows, dims.cols);
45+
}
46+
} catch (e) {
47+
console.warn(e);
48+
}
49+
};
50+
window.addEventListener('resize', resizeHandler);
51+
52+
Data.set(id, {
53+
term,
54+
resizeHandler
55+
});
56+
}
57+
58+
export function write(id, data) {
59+
const terminal = Data.get(id);
60+
const { term } = terminal;
61+
if (term) {
62+
term.write(data);
63+
}
64+
}
65+
66+
export function writeln(id, data) {
67+
const terminal = Data.get(id);
68+
const { term } = terminal;
69+
if (term) {
70+
term.writeln(data);
71+
}
72+
}
73+
74+
export function clear(id) {
75+
const terminal = Data.get(id);
76+
const { term } = terminal;
77+
if (term) {
78+
term.clear();
79+
}
80+
}
81+
82+
export function dispose(id) {
83+
const terminal = Data.get(id);
84+
Data.remove(id);
85+
86+
const { term, resizeHandler } = terminal;
87+
if (resizeHandler) {
88+
window.removeEventListener('resize', resizeHandler);
89+
}
90+
if (term) {
91+
term.dispose();
92+
}
93+
}

0 commit comments

Comments
 (0)