Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
93 changes: 93 additions & 0 deletions Examples/UICatalog/Scenarios/AnsiStatusLine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#nullable enable
namespace UICatalog.Scenarios;

[ScenarioMetadata ("ANSI StatusLine", "Demonstrates pushing text to the terminal status line while a full-screen app runs.")]
[ScenarioCategory ("Arrangement")]
public sealed class AnsiStatusLine : Scenario
{
private IApplication? _app;

public override void Main ()
{
ConfigurationManager.Enable (ConfigLocations.All);

using IApplication app = Application.Create ();
app.Init ();
_app = app;

using Window appWindow = new () { Title = GetQuitKeyAndName () };
appWindow.IsRunningChanged += AppWindowOnIsRunningChanged;

Label description = new ()
{
X = 1,
Y = 1,
Text = "This full-screen app writes status text to the terminal title/status-line area."
};
appWindow.Add (description);

Label capability = new ()
{
X = 1,
Y = Pos.Bottom (description) + 1,
Text = app.StatusLine.IsSupported ? "StatusLine output: available for this driver." : "StatusLine output: unavailable; calls are no-ops."
};
appWindow.Add (capability);

Label prompt = new () { X = 1, Y = Pos.Bottom (capability) + 1, Text = "Text:" };
appWindow.Add (prompt);

TextField statusText = new ()
{
X = Pos.Right (prompt) + 1,
Y = Pos.Top (prompt),
Width = 50,
Text = "Terminal.Gui status line"
};
appWindow.Add (statusText);

Button updateButton = new ()
{
X = 1,
Y = Pos.Bottom (statusText) + 1,
Text = "_Update StatusLine"
};
updateButton.Accepting += (_, _) => app.StatusLine.SetText (statusText.Text, 2);
appWindow.Add (updateButton);

Button clearButton = new ()
{
X = Pos.Right (updateButton) + 2,
Y = Pos.Top (updateButton),
Text = "_Clear"
};
clearButton.Accepting += (_, _) => app.StatusLine.Clear ();
appWindow.Add (clearButton);

Label statusLineOnlyHint = new ()
{
X = 1,
Y = Pos.Bottom (updateButton) + 2,
Text = "Run the \"ANSI StatusLine Only\" scenario to see an entire app render there."
};
appWindow.Add (statusLineOnlyHint);

app.StatusLine.SetText (statusText.Text, 2);
app.Run (appWindow);
}

private void AppWindowOnIsRunningChanged (object? sender, EventArgs<bool> args)
{
if (args.Value)
{
return;
}

_app?.StatusLine.Clear ();

if (sender is Window appWindow)
{
appWindow.IsRunningChanged -= AppWindowOnIsRunningChanged;
}
}
}
63 changes: 63 additions & 0 deletions Examples/UICatalog/Scenarios/AnsiStatusLineOnly.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#nullable enable
namespace UICatalog.Scenarios;

[ScenarioMetadata ("ANSI StatusLine Only", "Demonstrates running an entire Terminal.Gui app in the terminal status line.")]
[ScenarioCategory ("Arrangement")]
public sealed class AnsiStatusLineOnly : Scenario
{
private IApplication? _app;
private Label? _label;
private Timer? _timer;
private int _tick;

public override void Main ()
{
ConfigurationManager.Enable (ConfigLocations.All);

using IApplication app = Application.Create ();
app.AppModel = AppModel.StatusLine;
app.Init ();
_app = app;

using Runnable runnable = new () { Width = Dim.Fill (), Height = 1 };
runnable.IsRunningChanged += RunnableOnIsRunningChanged;

_label = new Label { X = 0, Y = 0, Text = BuildStatusText () };
runnable.Add (_label);

_timer = new Timer (_ => _app?.Invoke (UpdateStatusText), null, 0, 1000);

app.Run (runnable);
}

private string BuildStatusText () => $"StatusLine-only demo tick {_tick} - press Esc to quit";

private void UpdateStatusText ()
{
_tick++;

if (_label is null)
{
return;
}

_label.Text = BuildStatusText ();
}

private void RunnableOnIsRunningChanged (object? sender, EventArgs<bool> args)
{
if (args.Value)
{
return;
}

_timer?.Dispose ();
_timer = null;
_app?.StatusLine.Clear ();

if (sender is Runnable runnable)
{
runnable.IsRunningChanged -= RunnableOnIsRunningChanged;
}
}
}
8 changes: 7 additions & 1 deletion Terminal.Gui/App/AppModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,11 @@ public enum AppModel
/// The application renders inline within the primary (scrollback) buffer, anchored
/// to the bottom of the visible terminal. No alternate screen buffer is used.
/// </summary>
Inline
Inline,

/// <summary>
/// The application renders its top-level content into the terminal status line.
/// No alternate screen buffer or primary screen drawing is used.
/// </summary>
StatusLine
}
3 changes: 2 additions & 1 deletion Terminal.Gui/App/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ internal static List<CultureInfo> GetSupportedCultures ()
/// <summary>
/// Gets or sets the rendering mode for the application. When set to <see cref="App.AppModel.Inline"/>,
/// the application renders inline within the primary (scrollback) buffer instead of switching to
/// the alternate screen buffer.
/// the alternate screen buffer. When set to <see cref="App.AppModel.StatusLine"/>, the application
/// renders its top-level content into the terminal status line or title area.
/// </summary>
/// <remarks>
/// <para>
Expand Down
2 changes: 2 additions & 0 deletions Terminal.Gui/App/ApplicationImpl.Lifecycle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public IApplication Init (string? driverName = null)

RaiseInitializedChanged (this, new EventArgs<bool> (true));
SubscribeDriverEvents ();
StatusLine.Flush ();

SynchronizationContext.SetSynchronizationContext (new SynchronizationContext ());

Expand Down Expand Up @@ -276,6 +277,7 @@ public void ResetState (bool ignoreDisposed = false)
Iteration = null;
SessionBegun = null;
SessionEnded = null;
StatusLine.Reset ();
StopAfterFirstIteration = false;
ClearScreenNextIteration = false;

Expand Down
23 changes: 20 additions & 3 deletions Terminal.Gui/App/ApplicationImpl.Screen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,23 @@ internal partial class ApplicationImpl
/// <inheritdoc/>
public Rectangle Screen
{
get => _screen ?? Driver?.Screen ?? new Rectangle (new Point (0, 0), new Size (2048, 2048));
get
{
if (AppModel == AppModel.StatusLine && Driver is { } driver)
{
return new Rectangle (0, 0, driver.Screen.Width, 1);
}

return _screen ?? Driver?.Screen ?? new Rectangle (new Point (0, 0), new Size (2048, 2048));
}
set
{
if (AppModel == AppModel.Inline)
if (AppModel == AppModel.StatusLine)
{
_screen = null;
Driver?.SetScreenSize (value.Width, 1);
}
else if (AppModel == AppModel.Inline)
{
// Inline mode: store the sub-rectangle independently.
// Resize the output buffer to match the inline region dimensions.
Expand Down Expand Up @@ -75,7 +88,11 @@ private void Driver_SizeChanged (object? sender, SizeChangedEventArgs e)
{
Size newSize = e.Size ?? Size.Empty;

if (AppModel == AppModel.Inline && _screen is { } screen)
if (AppModel == AppModel.StatusLine)
{
RaiseScreenChangedEvent (new Rectangle (0, 0, newSize.Width, 1));
}
else if (AppModel == AppModel.Inline && _screen is { } screen)
{
// On resize in inline mode, reset to row 0 and clear the screen.
// The next LayoutAndDraw will re-size the inline region from scratch.
Expand Down
4 changes: 4 additions & 0 deletions Terminal.Gui/App/ApplicationImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal partial class ApplicationImpl : IApplication
internal ApplicationImpl (ITimeProvider timeProvider)
{
_timeProvider = timeProvider;
StatusLine = new StatusLine (() => Driver);

// Initialize TimedEvents with the time provider for testable timing
TimedEvents = new TimedEvents (timeProvider);
Expand Down Expand Up @@ -179,6 +180,9 @@ internal static void ResetStateStatic (bool ignoreDisposed = false)
/// <inheritdoc/>
public IClipboard? Clipboard { get => Driver?.Clipboard; set => Driver?.Clipboard = value; }

/// <inheritdoc/>
public StatusLine StatusLine { get; }

#endregion Screen and Driver

#region Input (Mouse/Keyboard)
Expand Down
6 changes: 6 additions & 0 deletions Terminal.Gui/App/IApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,11 @@ public interface IApplication : IDisposable
/// </remarks>
IClipboard? Clipboard { get; internal set; }

/// <summary>
/// Gets the terminal status line or title-area output target for this application instance.
/// </summary>
StatusLine StatusLine { get; }

/// <summary>
/// Forces the use of the specified driver (<see cref="DriverRegistry.Names"/>). If not
/// specified, the driver is selected based on the platform.
Expand All @@ -576,6 +581,7 @@ public interface IApplication : IDisposable
/// Gets or sets how the application interacts with the terminal buffer.
/// <see cref="AppModel.FullScreen"/> uses the alternate screen buffer (default).
/// <see cref="AppModel.Inline"/> renders inline in the primary scrollback buffer.
/// <see cref="AppModel.StatusLine"/> renders the top-level content into the terminal status line or title area.
/// </summary>
AppModel AppModel { get; set; }

Expand Down
3 changes: 3 additions & 0 deletions Terminal.Gui/App/Legacy/Application.Driver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ public static partial class Application // Driver abstractions
[Obsolete ("The legacy static Application object is going away.")]
public static IDriver? Driver { get => ApplicationImpl.Instance.Driver; internal set => ApplicationImpl.Instance.Driver = value; }

/// <inheritdoc cref="IApplication.StatusLine"/>
public static StatusLine StatusLine => ApplicationImpl.Instance.StatusLine;

/// <summary>Raised when <see cref="ForceDriver"/> changes.</summary>
public static event EventHandler<ValueChangedEventArgs<string>>? ForceDriverChanged;

Expand Down
4 changes: 3 additions & 1 deletion Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ public void Initialize (ITimedEvents timedEvents,
outputBufferImpl.InlineMode = true;
}

OutputBuffer.SetSize (consoleOutput.GetSize ().Width, consoleOutput.GetSize ().Height);
Size outputSize = consoleOutput.GetSize ();
int outputHeight = App?.AppModel == AppModel.StatusLine ? 1 : outputSize.Height;
OutputBuffer.SetSize (outputSize.Width, outputHeight);
SizeMonitor = componentFactory.CreateSizeMonitor (Output, OutputBuffer);
}

Expand Down
75 changes: 75 additions & 0 deletions Terminal.Gui/App/StatusLine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
namespace Terminal.Gui.App;

/// <summary>
/// Provides access to the terminal status line or title area.
/// </summary>
/// <remarks>
/// Status line output uses the driver's best available ANSI mechanism. The initial implementation uses OSC 0/1/2
/// title sequences, which are ignored by legacy consoles and unsupported terminals.
/// </remarks>
public sealed class StatusLine
{
private readonly Func<IDriver?> _driverGetter;
private bool _hasPendingWrite;

internal StatusLine (Func<IDriver?> driverGetter) => _driverGetter = driverGetter;

/// <summary>
/// Gets the last text requested for the status line.
/// </summary>
public string Text { get; private set; } = string.Empty;

/// <summary>
/// Gets the last OSC title selector requested.
/// </summary>
public int Mode { get; private set; } = 2;

/// <summary>
/// Gets whether the current driver can emit ANSI status line output.
/// </summary>
public bool IsSupported => _driverGetter () is { IsLegacyConsole: false };

/// <summary>
/// Clears the terminal status line or title area.
/// </summary>
public void Clear () => SetText (string.Empty);

/// <summary>
/// Writes <paramref name="text"/> to the terminal status line or title area.
/// </summary>
/// <param name="text">The text to show. <see langword="null"/> is treated as an empty string.</param>
/// <param name="mode">
/// The OSC title selector: 0 = icon and window title, 1 = icon title, 2 = window title.
/// </param>
public void SetText (string? text, int mode = 2)
{
Text = text ?? string.Empty;
Mode = Math.Clamp (mode, 0, 2);
_hasPendingWrite = true;
Flush ();
}

internal void Flush ()
{
if (!_hasPendingWrite)
{
return;
}

IDriver? driver = _driverGetter ();

if (driver is null || driver.IsLegacyConsole)
{
return;
}

driver.SetTerminalTitle (Text, Mode);
}

internal void Reset ()
{
Text = string.Empty;
Mode = 2;
_hasPendingWrite = false;
}
}
13 changes: 11 additions & 2 deletions Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,12 @@ public AnsiOutput (AppModel appModel = AppModel.FullScreen)
}

// Initialize terminal for ANSI output
if (AppModel == AppModel.Inline)
if (AppModel == AppModel.StatusLine)
{
// StatusLine mode renders only via OSC/status-line sequences and does not take over
// the alternate screen or primary screen buffer.
}
else if (AppModel == AppModel.Inline)
{
// Inline mode: do NOT switch to alternate screen buffer.
// Stay in the primary (scrollback) buffer.
Expand Down Expand Up @@ -385,7 +390,11 @@ public void Dispose ()
Write (EscSeqUtils.CSI_DisableMouseEvents);
Write (EscSeqUtils.CSI_ResetAttributes);

if (AppModel == AppModel.Inline)
if (AppModel == AppModel.StatusLine)
{
// StatusLine mode did not alter the screen buffer, so there is no buffer to restore.
}
else if (AppModel == AppModel.Inline)
{
// Inline mode: do NOT restore alternate buffer. Move cursor to just
// below the inline region so the shell prompt appears naturally.
Expand Down
Loading
Loading