Forge
csharpdeeb25a2
1using AvaloniaTerminal;
2using CascadeIDE.Features.Terminal.DataAcquisition;
3using CascadeIDE.Services;
4using CascadeIDE.ViewModels;
5
6namespace CascadeIDE.Features.Terminal;
7
8/// <summary>
9/// Вкладка «Terminal» нижней панели: интерактивная shell-сессия (ConPTY на Windows, redirected fallback)
10/// через <see cref="TerminalControlModel"/> (ANSI, сетка, сырой TTY-ввод).
11/// </summary>
12public sealed class TerminalPanelViewModel : ViewModelBase, IDisposable
13{
14 private const int ScrollbackLines = 10_000;
15 private static readonly TimeSpan BackspaceRepeatMinInterval = TimeSpan.FromMilliseconds(50);
16
17 private readonly IntegratedTerminalSessionHost _shellHost;
18 private readonly IntegratedShellBackspaceBurstGuard _backspaceBurstGuard = new();
19 private bool _disposed;
20 private bool _sessionStarted;
21 private (int cols, int rows)? _pendingTerminalSize;
22 private DateTime _lastPureBackspaceSentUtc = DateTime.MinValue;
23 private bool _shellOutputLeadingBomStripped;
24
25 public TerminalPanelViewModel(Func<string?> getSolutionPath)
26 {
27 _shellHost = new IntegratedTerminalSessionHost(getSolutionPath);
28 TerminalModel = new TerminalControlModel(new TerminalOptions
29 {
30 ReflowOnResize = false,
31 Scrollback = ScrollbackLines,
32 });
33
34 TerminalModel.UserInput += OnTerminalUserInput;
35 TerminalModel.SizeChanged += OnTerminalSizeChanged;
36 _shellHost.OutputReceived += OnShellOutput;
37 _shellHost.SessionExited += OnShellSessionExited;
38 }
39
40 public TerminalControlModel TerminalModel { get; }
41
42 public void EnsureSessionStarted()
43 {
44 if (_disposed || _sessionStarted)
45 return;
46
47 try
48 {
49 _shellHost.EnsureStarted();
50 _sessionStarted = true;
51 _backspaceBurstGuard.Reset();
52 _shellOutputLeadingBomStripped = false;
53 ApplyPendingResize();
54 }
55 catch (Exception ex)
56 {
57 FeedOnUiThread(ex.Message + "\r\n");
58 }
59 }
60
61 public void Clear() => FeedOnUiThread("\x1b[2J\x1b[H");
62
63 public void AppendOutput(string text)
64 {
65 if (string.IsNullOrEmpty(text))
66 return;
67
68 FeedOnUiThread(text);
69 }
70
71 private void OnTerminalUserInput(object? sender, TerminalUserInputEventArgs e)
72 {
73 if (_disposed || e.Data.Length == 0)
74 return;
75
76 var rawInput = e.Data.ToArray();
77 if (IntegratedShellLaunch.IsPureBackspaceInput(rawInput))
78 {
79 var now = DateTime.UtcNow;
80 if (now - _lastPureBackspaceSentUtc < BackspaceRepeatMinInterval)
81 return;
82
83 _lastPureBackspaceSentUtc = now;
84 }
85
86 var filteredInput = _backspaceBurstGuard.FilterUserInput(rawInput);
87 if (filteredInput.Length == 0)
88 return;
89
90 try
91 {
92 EnsureSessionStarted();
93 _shellHost.SendInput(filteredInput);
94 }
95 catch (Exception ex)
96 {
97 FeedOnUiThread(ex.Message + "\r\n");
98 }
99 }
100
101 private void OnTerminalSizeChanged(object? sender, TerminalSizeChangedEventArgs e)
102 {
103 if (_disposed || e.Cols <= 0 || e.Rows <= 0)
104 return;
105
106 _pendingTerminalSize = (e.Cols, e.Rows);
107 if (_sessionStarted)
108 _shellHost.Resize(e.Cols, e.Rows);
109 }
110
111 private void OnShellOutput(byte[] data)
112 {
113 if (data.Length == 0)
114 return;
115
116 _backspaceBurstGuard.NotifyShellOutput();
117 var sanitized = IntegratedShellStreamSanitizer.SanitizeShellOutput(data, ref _shellOutputLeadingBomStripped);
118 if (sanitized.Length == 0)
119 return;
120
121 if (UiScheduler.Default.CheckAccess())
122 {
123 TerminalModel.Feed(sanitized, sanitized.Length);
124 return;
125 }
126
127 UiScheduler.Default.Post(() =>
128 {
129 if (_disposed)
130 return;
131
132 TerminalModel.Feed(sanitized, sanitized.Length);
133 });
134 }
135
136 private void OnShellSessionExited(int exitCode)
137 {
138 _sessionStarted = false;
139 _backspaceBurstGuard.Reset();
140 if (exitCode != 0)
141 FeedOnUiThread($"\r\nShell exited: {exitCode}\r\n");
142 }
143
144 private void ApplyPendingResize()
145 {
146 if (_pendingTerminalSize is not { } size)
147 return;
148
149 _shellHost.Resize(size.cols, size.rows);
150 }
151
152 private void FeedOnUiThread(string text)
153 {
154 if (string.IsNullOrEmpty(text))
155 return;
156
157 if (UiScheduler.Default.CheckAccess())
158 {
159 TerminalModel.Feed(text);
160 return;
161 }
162
163 UiScheduler.Default.Post(() =>
164 {
165 if (_disposed)
166 return;
167
168 TerminalModel.Feed(text);
169 });
170 }
171
172 public void Dispose()
173 {
174 if (_disposed)
175 return;
176
177 _disposed = true;
178 TerminalModel.UserInput -= OnTerminalUserInput;
179 TerminalModel.SizeChanged -= OnTerminalSizeChanged;
180 _shellHost.OutputReceived -= OnShellOutput;
181 _shellHost.SessionExited -= OnShellSessionExited;
182 _shellHost.Dispose();
183 }
184}
185
View only · write via MCP/CIDE