Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a753d0c
links with tests
ccoulioufr Feb 22, 2026
5e30891
Merge branch 'v2_develop' into v2_develop
tig Feb 22, 2026
26f56b1
Merge branch 'v2_develop' into v2_develop
tig Feb 22, 2026
474573a
Merge branch 'v2_develop' into v2_develop
tig Feb 22, 2026
0e0df72
Merge branch 'v2_develop' into v2_develop
tig Feb 22, 2026
ba9bca5
Merge branch 'v2_develop' into v2_develop
ccoulioufr Feb 22, 2026
fac8853
Update Terminal.Gui/Views/Link.cs
ccoulioufr Feb 22, 2026
7888860
Update Terminal.Gui/Views/Link.cs
ccoulioufr Feb 22, 2026
7986d8b
explicit type declarations in tests
ccoulioufr Feb 22, 2026
e5c065c
Merge branch 'v2_develop' of https://github.com/ccoulioufr/Terminal.G…
ccoulioufr Feb 22, 2026
9977b43
using Cancellable Work Pattern recommandations
ccoulioufr Feb 22, 2026
7deb8ae
events testing
ccoulioufr Feb 22, 2026
4852380
Merge branch 'v2_develop' into v2_develop
tig Feb 22, 2026
34224c2
uicatalog url indicator + no crash Uri
ccoulioufr Feb 22, 2026
e887832
Merge branch 'v2_develop' of https://github.com/ccoulioufr/Terminal.G…
ccoulioufr Feb 22, 2026
d45b0a6
fix scenario test
ccoulioufr Feb 22, 2026
2b074bd
Link Osc8 test
ccoulioufr Feb 23, 2026
0ecfcdd
bad url is now has no effect and is o
ccoulioufr Feb 23, 2026
5481454
Url validation in Link class
ccoulioufr Feb 24, 2026
ba38682
Merge branch 'v2_develop' into v2_develop
tig Feb 26, 2026
b8db53e
fix link test scheme color
ccoulioufr Feb 26, 2026
d436c0e
Merge branch 'v2_develop' into v2_develop
tig Feb 27, 2026
6a66717
Merge branch 'v2_develop' into v2_develop
tig Mar 1, 2026
52463a6
no url in cell + SetNeedsDraw on url change + empty default url + up…
ccoulioufr Mar 1, 2026
cb4b3a2
Merge branch 'v2_develop' of https://github.com/ccoulioufr/Terminal.G…
ccoulioufr Mar 1, 2026
30f0348
Merge branch 'v2_develop' into v2_develop
ccoulioufr Mar 2, 2026
50f627f
Merge branch 'gui-cs:v2_develop' into v2_develop
ccoulioufr Mar 5, 2026
1e55338
Merge branch 'v2_develop' into v2_develop
tig Mar 6, 2026
db81f44
Refactor Link control: API, UI, and platform integration
tig Mar 6, 2026
f42e548
Refactor Link: decouple Text/Title/Url, expand tests
tig Mar 6, 2026
abdb993
Improve Link API documentation
tig Mar 6, 2026
8111ac1
Merge v2_develop into pr-4741-link
tig Mar 9, 2026
38eb861
Merge branch 'v2_develop' into pr-4741-link
tig Mar 9, 2026
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
87 changes: 87 additions & 0 deletions Examples/UICatalog/Scenarios/Links.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#nullable enable

namespace UICatalog.Scenarios;

[ScenarioMetadata ("Links", "Demonstrates how Links work.")]
[ScenarioCategory ("Controls")]
[ScenarioCategory ("Mouse and Keyboard")]
public class Links : Scenario
{
private IApplication? _app;
private Window? _appWindow;
private Link? _link;

public override void Main ()
{
ConfigurationManager.Enable (ConfigLocations.All);
using IApplication app = Application.Create ();
app.Init ();
_app = app;

_appWindow = new Window { Title = GetName (), BorderStyle = LineStyle.None };

Label titleLabel = new () { Text = "_Title:", X = 1, Y = 1 };
_appWindow.Add (titleLabel);

TextField titleTextField = new () { X = Pos.Right (titleLabel) + 1, Y = Pos.Top (titleLabel), Width = Dim.Fill () };
_appWindow.Add (titleTextField);

Label textLabel = new () { Text = " Te_xt:", X = Pos.Left (titleLabel), Y = Pos.Bottom(titleLabel) };
_appWindow.Add (textLabel);

TextField textTextField = new () { X = Pos.Right (textLabel) + 1, Y = Pos.Top(textLabel), Width = Dim.Fill () };
_appWindow.Add (textTextField);

Label urlLabel = new () { Text = " _Url:", X = 1, Y = Pos.Bottom (titleTextField) + 1 };
_appWindow.Add (urlLabel);

TextField urlTextField = new () { X = Pos.Right (urlLabel) + 1, Y = Pos.Bottom (titleTextField) + 1, Width = Dim.Fill () };
_appWindow.Add (urlTextField);

Label simpleUrlLabel = new () { X = 1, Y = Pos.Bottom (urlTextField) + 2 };
_appWindow.Add (simpleUrlLabel);

FrameView linkFrame = new ()
{
Title = "_Link Demo",
X = 0,
Y = Pos.Bottom (simpleUrlLabel) + 2,
Width = Dim.Fill(),
Height = Dim.Auto (),
AssignHotKeys = true,
TabStop = TabBehavior.TabStop
};

_link = new Link { X = 1, Y = 1, BorderStyle = LineStyle.Dotted };

_link.TextChanged += (s, e) => simpleUrlLabel.Text = $"This is just a Label with a URL in Text (WT automatically enables URLs) - {_link.Text}";
titleTextField.ValueChanged += (s, e) => _link.Title = e.NewValue ?? string.Empty;
textTextField.ValueChanged += (s, e) => _link.Text = e.NewValue ?? string.Empty;
urlTextField.ValueChanged += (s, e) => _link.Url = e.NewValue ?? Link.DEFAULT_URL;
linkFrame.Add (_link);

titleTextField.Text = "Title";
textTextField.Text = "GitHub repo";
urlTextField.Text = "https://github.com/gui-cs/Terminal.Gui";

Button copyButton = new () { Title = "_Copy", X = Pos.Center (), Y = Pos.AnchorEnd () };
copyButton.Accepting += (s, e) => _link.Copy ();

linkFrame.Add (copyButton);

_appWindow.Add (linkFrame);

// StatusBar
Shortcut urlIndicator = new (Key.Empty, "", null);

StatusBar statusBar = new ([new Shortcut (Application.QuitKey, "Quit", Quit), urlIndicator]);
_link.MouseEnter += (s, e) => urlIndicator.Title = _link.Text;
_link.MouseLeave += (s, e) => urlIndicator.Title = "";
_appWindow.Add (statusBar);

_app.Run (_appWindow);
_appWindow.Dispose ();
}

private void Quit () => _appWindow?.RequestStop ();
}
34 changes: 3 additions & 31 deletions Examples/UICatalog/UICatalogRunnable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,11 @@ private MenuBar CreateMenuBar ()
[
new MenuItem ("_Documentation",
"API docs",
() => OpenUrl ("https://gui-cs.github.io/Terminal.Gui"),
() => Link.OpenUrl ("https://gui-cs.github.io/Terminal.Gui"),
Key.F1),
new MenuItem ("_README",
"Project readme",
() => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"),
() => Link.OpenUrl ("https://github.com/gui-cs/Terminal.Gui"),
Key.F2),
new MenuItem ("_About...",
"About UI Catalog",
Expand Down Expand Up @@ -401,7 +401,7 @@ View [] CreateLoggingMenuItems ()
// add a separator
menuItems.Add (new Line ());

menuItems.Add (new MenuItem ("_Open Log Folder", string.Empty, () => OpenUrl (UICatalog.LOGFILE_LOCATION)));
menuItems.Add (new MenuItem ("_Open Log Folder", string.Empty, () => Link.OpenUrl (UICatalog.LOGFILE_LOCATION)));

return menuItems.ToArray ()!;

Expand Down Expand Up @@ -747,34 +747,6 @@ _______ _ _ _____ _
return msg.ToString ();
}

public static void OpenUrl (string url)
{
if (PlatformDetection.IsWindows ())
{
url = url.Replace ("&", "^&");
Process.Start (new ProcessStartInfo ("cmd", $"/c start {url}") { CreateNoWindow = true });
}
else if (PlatformDetection.IsMac ())
{
Process.Start ("open", url);
}
else if (PlatformDetection.IsLinux ())
{
using Process process = new ();

process.StartInfo = new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = url,
RedirectStandardError = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
UseShellExecute = false
};
process.Start ();
}
}

/// <summary>
/// Shows a dialog displaying error logs from a scenario run.
/// </summary>
Expand Down
4 changes: 4 additions & 0 deletions Terminal.Gui/Drivers/DriverImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,15 @@ public virtual void SetScreenSize (int width, int height)
/// <inheritdoc/>
public Region? Clip { get => _outputBuffer.Clip; set => _outputBuffer.Clip = value; }

/// <inheritdoc/>
public string? CurrentUrl { get => _outputBuffer.CurrentUrl; set => _outputBuffer.CurrentUrl = value; }

/// <summary>Clears the <see cref="IDriver.Contents"/> of the driver.</summary>
public void ClearContents ()
{
_outputBuffer.ClearContents ();
ClearedContents?.Invoke (this, EventArgs.Empty);
CurrentUrl = null;
}

/// <inheritdoc/>
Expand Down
7 changes: 7 additions & 0 deletions Terminal.Gui/Drivers/IDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ public interface IDriver : IDisposable
/// </summary>
Attribute CurrentAttribute { get; set; }

/// <summary>
/// Gets or sets the URL that will be associated with cells added via <see cref="AddRune(Rune)"/> or <see cref="AddStr(string)"/>.
/// When set, subsequent cells will include this URL for OSC 8 hyperlink rendering.
/// Set to <see langword="null"/> to stop associating URLs with cells.
/// </summary>
string? CurrentUrl { get; set; }

/// <summary>
/// Updates <see cref="IDriver.Col"/> and <see cref="IDriver.Row"/> to the specified column and row in
/// <see cref="IDriver.Contents"/>.
Expand Down
15 changes: 15 additions & 0 deletions Terminal.Gui/Drivers/Output/IOutputBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,21 @@ public interface IOutputBuffer
/// <summary>The number of rows visible in the terminal.</summary>
int Rows { get; set; }

/// <summary>
/// Gets or sets the URL that will be associated with cells added via <see cref="AddRune(Rune)"/> or <see cref="AddStr(string)"/>.
/// When set, subsequent cells will include this URL for OSC 8 hyperlink rendering.
/// </summary>
string? CurrentUrl { get; set; }

/// <summary>
/// Gets the URL associated with the cell at the specified position.
/// Returns <see langword="null"/> if no URL is associated with the cell.
/// </summary>
/// <param name="col">The column position.</param>
/// <param name="row">The row position.</param>
/// <returns>The URL associated with the cell, or <see langword="null"/> if none exists.</returns>
string? GetCellUrl (int col, int row);

/// <summary>
/// Changes the size of the buffer to the given size
/// </summary>
Expand Down
34 changes: 34 additions & 0 deletions Terminal.Gui/Drivers/Output/OutputBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ public bool IsLegacyConsole
// Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange().
private TextStyle _redrawTextStyle = TextStyle.None;

// Last URL used for tracking hyperlink state
private string? _lastUrl = null;

private readonly StringBuilder _lastOutputStringBuilder = new ();
private bool _clearLastOutputPending;

Expand Down Expand Up @@ -83,6 +86,7 @@ public virtual void Write (IOutputBuffer buffer)
}

outputStringBuilder.Clear ();
_lastUrl = null; // Reset URL state at the start of each row

// Process columns in row
for (int col = left; col < cols; col++)
Expand Down Expand Up @@ -121,6 +125,29 @@ public virtual void Write (IOutputBuffer buffer)
lastCol = col;
}

// Handle URL hyperlink state changes
if (!IsLegacyConsole)
{
string? cellUrl = buffer.GetCellUrl (col, row);

if (cellUrl != _lastUrl)
{
// If we were in a hyperlink, end it
if (_lastUrl is { })
{
outputStringBuilder.Append (EscSeqUtils.OSC_EndHyperlink ());
}

// If starting a new hyperlink, begin it
if (!string.IsNullOrEmpty (cellUrl))
{
outputStringBuilder.Append (EscSeqUtils.OSC_StartHyperlink (cellUrl));
}

_lastUrl = cellUrl;
}
}

// Append dirty cell as ANSI and mark clean
Cell cell = buffer.Contents [row, col];
buffer.Contents [row, col].IsDirty = false;
Expand Down Expand Up @@ -149,6 +176,13 @@ public virtual void Write (IOutputBuffer buffer)
{
SetCursorPositionImpl (lastCol, row);

// Close any open hyperlink before processing URLs
if (_lastUrl is { })
{
outputStringBuilder.Append (EscSeqUtils.OSC_EndHyperlink ());
_lastUrl = null;
}

// Wrap URLs with OSC 8 hyperlink sequences
StringBuilder processed = Osc8UrlLinker.WrapOsc8 (outputStringBuilder);
Write (processed);
Expand Down
44 changes: 44 additions & 0 deletions Terminal.Gui/Drivers/Output/OutputBufferImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,41 @@ public class OutputBufferImpl : IOutputBuffer
/// </summary>
public Attribute CurrentAttribute { get; set; }

/// <summary>
/// Gets or sets the URL that will be associated with cells added via <see cref="AddRune(Rune)"/> or <see cref="AddStr(string)"/>.
/// When set, subsequent cells will include this URL for OSC 8 hyperlink rendering.
/// </summary>
public string? CurrentUrl { get; set; }

/// <summary>
/// Maps cell positions to URLs for OSC 8 hyperlink support.
/// Only stores entries for cells that actually have URLs, minimizing memory overhead.
/// </summary>
private Dictionary<Point, string>? _urlMap;

/// <summary>
/// Gets the URL associated with the cell at the specified position.
/// </summary>
/// <param name="col">The column.</param>
/// <param name="row">The row.</param>
/// <returns>The URL if one exists, otherwise null.</returns>
public string? GetCellUrl (int col, int row)
{
return _urlMap?.TryGetValue (new Point (col, row), out string? url) == true ? url : null;
}

/// <summary>
/// Sets the URL for the cell at the specified position.
/// </summary>
/// <param name="col">The column.</param>
/// <param name="row">The row.</param>
/// <param name="url">The URL to associate with this cell.</param>
private void SetCellUrl (int col, int row, string url)
{
_urlMap ??= [];
_urlMap [new Point (col, row)] = url;
}

/// <summary>The leftmost column in the terminal.</summary>
public virtual int Left { get; set; } = 0;

Expand Down Expand Up @@ -214,6 +249,12 @@ private void SetAttributeAndDirty (int col, int row)
{
Contents! [row, col].Attribute = CurrentAttribute;
Contents [row, col].IsDirty = true;

// If CurrentUrl is set, store it in the URL map
if (!string.IsNullOrEmpty (CurrentUrl))
{
SetCellUrl (col, row, CurrentUrl);
}
}

/// <summary>
Expand Down Expand Up @@ -321,6 +362,9 @@ public void ClearContents ()

DirtyLines = new bool [Rows];

// Clear the URL map
_urlMap?.Clear ();

lock (Contents)
{
for (var row = 0; row < Rows; row++)
Expand Down
Loading
Loading