Skip to content

Commit 74bbc22

Browse files
committed
feat: add BootstrapBlazor.Term extension
- Integrate xterm.js (v5.3.0) and xterm-addon-fit (v0.8.0) - Add Term component with options handling - Add JS interop for resizing and data transmission - Support SSH.NET Stream integration via Open(Stream) - Expose Rows and Columns properties for terminal dimensions
1 parent 7f95f94 commit 74bbc22

8 files changed

Lines changed: 599 additions & 0 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Razor">
2+
3+
<PropertyGroup>
4+
<PackageTags>Bootstrap Blazor WebAssembly wasm UI Components Xterm Term Terminal</PackageTags>
5+
<Description>Bootstrap UI components extensions of Term (xterm.js)</Description>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="BootstrapBlazor" Version="$(BBVersion)" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<Using Include="Microsoft.JSInterop" />
14+
</ItemGroup>
15+
16+
</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 @ref="Element" id="@Id" class="@GetClassString()" style="@GetStyleString()"></div>
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
7+
namespace BootstrapBlazor.Components;
8+
9+
/// <summary>
10+
/// Term 终端组件
11+
/// </summary>
12+
[JSModuleAutoLoader("./_content/BootstrapBlazor.Term/Components/Term/Term.razor.js", JSObjectReference = true)]
13+
public partial class Term
14+
{
15+
/// <summary>
16+
/// 获得/设置 UI Element
17+
/// </summary>
18+
private ElementReference Element { get; set; }
19+
20+
/// <summary>
21+
/// 获得/设置 Options
22+
/// </summary>
23+
[Parameter]
24+
public TermOptions Options { get; set; } = new TermOptions();
25+
26+
/// <summary>
27+
/// 获得/设置 收到数据回调
28+
/// </summary>
29+
[Parameter]
30+
public Func<string, Task>? OnData { get; set; }
31+
32+
/// <summary>
33+
/// 获得/设置 终端 Resize 回调
34+
/// </summary>
35+
[Parameter]
36+
public Func<int, int, Task>? OnResize { get; set; }
37+
38+
/// <summary>
39+
/// 获得/设置 高度 默认 300px
40+
/// </summary>
41+
[Parameter]
42+
public string Height { get; set; } = "300px";
43+
44+
/// <summary>
45+
/// GetClassString
46+
/// </summary>
47+
/// <returns></returns>
48+
private string? GetClassString() => CssBuilder.Default("bb-term")
49+
.AddClassFromAttributes(AdditionalAttributes)
50+
.Build();
51+
52+
/// <summary>
53+
/// GetStyleString
54+
/// </summary>
55+
/// <returns></returns>
56+
private string? GetStyleString() => CssBuilder.Default()
57+
.AddClass($"height: {Height};", !string.IsNullOrEmpty(Height))
58+
.AddStyleFromAttributes(AdditionalAttributes)
59+
.Build();
60+
61+
/// <summary>
62+
/// OnInitialized
63+
/// </summary>
64+
protected override void OnInitialized()
65+
{
66+
base.OnInitialized();
67+
}
68+
69+
/// <summary>
70+
/// InvokeInitAsync
71+
/// </summary>
72+
/// <returns></returns>
73+
protected override async Task InvokeInitAsync()
74+
{
75+
await InvokeVoidAsync("init", Id, Interop, Options);
76+
}
77+
78+
/// <summary>
79+
/// 写入数据
80+
/// </summary>
81+
/// <param name="data"></param>
82+
/// <returns></returns>
83+
public async Task Write(string data)
84+
{
85+
await InvokeVoidAsync("write", Id, data);
86+
}
87+
88+
/// <summary>
89+
/// 写入一行数据
90+
/// </summary>
91+
/// <param name="data"></param>
92+
/// <returns></returns>
93+
public async Task WriteLine(string data)
94+
{
95+
await InvokeVoidAsync("writeln", Id, data);
96+
}
97+
98+
/// <summary>
99+
/// 清空终端
100+
/// </summary>
101+
/// <returns></returns>
102+
public async Task Clear()
103+
{
104+
await InvokeVoidAsync("clear", Id);
105+
}
106+
107+
/// <summary>
108+
/// 连接流
109+
/// </summary>
110+
/// <param name="stream"></param>
111+
/// <returns></returns>
112+
public async Task Open(Stream stream)
113+
{
114+
_stream = stream;
115+
_cancellationTokenSource = new CancellationTokenSource();
116+
_ = ReadStreamAsync();
117+
118+
await Task.CompletedTask;
119+
}
120+
121+
private Stream? _stream;
122+
private CancellationTokenSource? _cancellationTokenSource;
123+
124+
private async Task ReadStreamAsync()
125+
{
126+
if (_stream == null) return;
127+
128+
var buffer = new byte[1024];
129+
try
130+
{
131+
while (!_cancellationTokenSource!.IsCancellationRequested)
132+
{
133+
var read = await _stream.ReadAsync(buffer, _cancellationTokenSource.Token);
134+
if (read == 0) break;
135+
var data = System.Text.Encoding.UTF8.GetString(buffer, 0, read);
136+
await Write(data);
137+
}
138+
}
139+
catch (TaskCanceledException)
140+
{
141+
// ignored
142+
}
143+
catch (Exception ex)
144+
{
145+
// Handle error
146+
await WriteLine($"\r\nError: {ex.Message}");
147+
}
148+
}
149+
150+
/// <summary>
151+
/// 收到数据 JSInvoke
152+
/// </summary>
153+
/// <param name="data"></param>
154+
/// <returns></returns>
155+
[JSInvokable]
156+
public async Task OnDataAsync(string data)
157+
{
158+
if (_stream != null && _stream.CanWrite)
159+
{
160+
var buffer = System.Text.Encoding.UTF8.GetBytes(data);
161+
await _stream.WriteAsync(buffer);
162+
await _stream.FlushAsync();
163+
}
164+
165+
if (OnData != null)
166+
{
167+
await OnData(data);
168+
}
169+
}
170+
171+
/// <summary>
172+
/// Dispose
173+
/// </summary>
174+
/// <param name="disposing"></param>
175+
protected override async ValueTask DisposeAsync(bool disposing)
176+
{
177+
_cancellationTokenSource?.Cancel();
178+
_cancellationTokenSource?.Dispose();
179+
await base.DisposeAsync(disposing);
180+
}
181+
182+
/// <summary>
183+
/// 获得/设置 终端行数
184+
/// </summary>
185+
public int Rows { get; private set; }
186+
187+
/// <summary>
188+
/// 获得/设置 终端列数
189+
/// </summary>
190+
public int Columns { get; private set; }
191+
192+
/// <summary>
193+
/// Resize JSInvoke
194+
/// </summary>
195+
/// <param name="rows"></param>
196+
/// <param name="cols"></param>
197+
/// <returns></returns>
198+
[JSInvokable]
199+
public async Task OnResizeAsync(int rows, int cols)
200+
{
201+
Rows = rows;
202+
Columns = cols;
203+
if (OnResize != null)
204+
{
205+
await OnResize(rows, cols);
206+
}
207+
}
208+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import "../../lib/xterm/xterm.js";
2+
import "../../lib/xterm/xterm-addon-fit.js";
3+
4+
export function init(id, invoke, options) {
5+
const el = document.getElementById(id);
6+
if (el === null) {
7+
return;
8+
}
9+
10+
// Load CSS
11+
const linkId = "bb-term-css";
12+
if (!document.getElementById(linkId)) {
13+
const link = document.createElement('link');
14+
link.id = linkId;
15+
link.rel = 'stylesheet';
16+
link.href = './_content/BootstrapBlazor.Term/lib/xterm/xterm.css';
17+
document.head.appendChild(link);
18+
}
19+
20+
const term = new Terminal({
21+
fontFamily: options.fontFamily || "Consolas, 'Courier New', monospace",
22+
fontSize: options.fontSize || 14,
23+
cursorBlink: options.cursorBlink,
24+
lineHeight: options.lineHeight || 1.0,
25+
theme: options.theme || {}
26+
});
27+
28+
const fitAddon = new FitAddon.FitAddon();
29+
term.loadAddon(fitAddon);
30+
31+
term.open(el);
32+
fitAddon.fit();
33+
34+
term.onData(data => {
35+
invoke.invokeMethodAsync("OnDataAsync", data);
36+
});
37+
38+
term.onResize(size => {
39+
invoke.invokeMethodAsync("OnResizeAsync", size.rows, size.cols);
40+
});
41+
42+
// Store instance
43+
el.term = term;
44+
el.fitAddon = fitAddon;
45+
el.invoke = invoke;
46+
47+
// Window resize handling
48+
const resizeHandler = () => {
49+
try {
50+
fitAddon.fit();
51+
const dims = fitAddon.proposeDimensions();
52+
if (dims) {
53+
invoke.invokeMethodAsync("OnResizeAsync", dims.rows, dims.cols);
54+
}
55+
} catch (e) { }
56+
};
57+
window.addEventListener('resize', resizeHandler);
58+
el.resizeHandler = resizeHandler;
59+
}
60+
61+
export function write(id, data) {
62+
const el = document.getElementById(id);
63+
if (el && el.term) {
64+
el.term.write(data);
65+
}
66+
}
67+
68+
export function writeln(id, data) {
69+
const el = document.getElementById(id);
70+
if (el && el.term) {
71+
el.term.writeln(data);
72+
}
73+
}
74+
75+
export function clear(id) {
76+
const el = document.getElementById(id);
77+
if (el && el.term) {
78+
el.term.clear();
79+
}
80+
}
81+
82+
export function dispose(id) {
83+
const el = document.getElementById(id);
84+
if (el) {
85+
if (el.resizeHandler) {
86+
window.removeEventListener('resize', el.resizeHandler);
87+
}
88+
if (el.term) {
89+
el.term.dispose();
90+
delete el.term;
91+
}
92+
if (el.fitAddon) {
93+
delete el.fitAddon;
94+
}
95+
}
96+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
namespace BootstrapBlazor.Components;
6+
7+
/// <summary>
8+
/// Term Options
9+
/// </summary>
10+
public class TermOptions
11+
{
12+
/// <summary>
13+
/// 获得/设置 终端字体大小 默认 14
14+
/// </summary>
15+
public int FontSize { get; set; } = 14;
16+
17+
/// <summary>
18+
/// 获得/设置 终端字体
19+
/// </summary>
20+
public string FontFamily { get; set; } = "Consolas, 'Courier New', monospace";
21+
22+
/// <summary>
23+
/// 获得/设置 终端主题 默认 null 使用默认主题
24+
/// </summary>
25+
public TermTheme? Theme { get; set; }
26+
27+
/// <summary>
28+
/// 获得/设置 光标闪烁 默认 true
29+
/// </summary>
30+
public bool CursorBlink { get; set; } = true;
31+
32+
/// <summary>
33+
/// 获得/设置 行高 默认 1.0
34+
/// </summary>
35+
public double LineHeight { get; set; } = 1.0;
36+
}
37+
38+
/// <summary>
39+
/// Term Theme
40+
/// </summary>
41+
public class TermTheme
42+
{
43+
/// <summary>
44+
/// Background color
45+
/// </summary>
46+
public string? Background { get; set; }
47+
48+
/// <summary>
49+
/// Foreground color
50+
/// </summary>
51+
public string? Foreground { get; set; }
52+
53+
/// <summary>
54+
/// Cursor color
55+
/// </summary>
56+
public string? Cursor { get; set; }
57+
58+
/// <summary>
59+
/// Selection color
60+
/// </summary>
61+
public string? Selection { get; set; }
62+
}

src/components/BootstrapBlazor.Term/wwwroot/lib/xterm/xterm-addon-fit.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)