From de8f0b18b90093b94f8a4c5d33a803c95d8225b3 Mon Sep 17 00:00:00 2001 From: Matt Razza <504088+mrazza@users.noreply.github.com> Date: Sat, 9 May 2026 23:02:16 -0400 Subject: [PATCH 01/11] refactor: move to Terminal.Gui ImageView control for sixel album art images --- Terminal.Gui | 2 +- smoc.Tests/Fakes/FakeMainWindow.cs | 4 - smoc.Tests/Fakes/FakeSixelDriver.cs | 37 --------- ...itialState_ShowsEmptyNowPlayingInfo.golden | 24 +++--- ...State_ShowsEmptyNowPlayingInfo_Tall.golden | 2 +- ...State_ShowsEmptyNowPlayingInfo_Wide.golden | 2 +- .../OnPositionChanged_UpdatesPosition.golden | 2 +- ...anged_UpdatesPosition_MultipleTimes.golden | 2 +- ...anged_SongIsNull_UpdatesSongDetails.golden | 2 +- .../OnSongChanged_UpdatesSongDetails.golden | 2 +- smoc/Program.cs | 5 +- smoc/Ui/Components/SixelImageView.cs | 36 ++------- smoc/Ui/Drawing/ISixelDriver.cs | 53 ------------- smoc/Ui/Drawing/SixelDriver.cs | 77 ------------------- smoc/Ui/IMainWindow.cs | 6 -- smoc/Ui/MainWindow.cs | 8 +- smoc/Ui/NowPlayingBar.cs | 8 +- smoc/Ui/NowPlayingView.cs | 13 +++- 18 files changed, 43 insertions(+), 242 deletions(-) delete mode 100644 smoc.Tests/Fakes/FakeSixelDriver.cs delete mode 100644 smoc/Ui/Drawing/ISixelDriver.cs delete mode 100644 smoc/Ui/Drawing/SixelDriver.cs diff --git a/Terminal.Gui b/Terminal.Gui index f9d117f..719614d 160000 --- a/Terminal.Gui +++ b/Terminal.Gui @@ -1 +1 @@ -Subproject commit f9d117f97f020ec411482253afefafb865fcd71f +Subproject commit 719614d2f896b7274f1c22b9b8991a4d4d71f18d diff --git a/smoc.Tests/Fakes/FakeMainWindow.cs b/smoc.Tests/Fakes/FakeMainWindow.cs index 70268d9..b8c2454 100644 --- a/smoc.Tests/Fakes/FakeMainWindow.cs +++ b/smoc.Tests/Fakes/FakeMainWindow.cs @@ -1,5 +1,4 @@ using Smoc.Ui; -using Smoc.Ui.Drawing; using Smoc.Ui.Models; using Terminal.Gui.App; @@ -13,9 +12,6 @@ public class FakeMainWindow : IMainWindow { /// public IApplication? App { get; } = FakeApplication.New(); - /// - public ISixelDriver SixelDriver { get; set; } = new FakeSixelDriver(); - /// public Mode CurrentMode { get; set; } diff --git a/smoc.Tests/Fakes/FakeSixelDriver.cs b/smoc.Tests/Fakes/FakeSixelDriver.cs deleted file mode 100644 index e817c41..0000000 --- a/smoc.Tests/Fakes/FakeSixelDriver.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Drawing; -using Smoc.Ui.Drawing; -using Terminal.Gui.Drawing; - -namespace smoc.Tests.Fakes; - -/// -/// A fake implementation of for use in tests. -/// -public class FakeSixelDriver : ISixelDriver { - /// - public double CellAspectRatio => 2.0; - - /// - public bool IsSupported => true; - - /// - public Size? Resolution => null; - - /// - public int MaxPaletteColors => 256; - - /// - public string EncodeSixel(Terminal.Gui.Drawing.Color[,] colors) { - return ""; - } - - /// - public void EnqueueSixel(SixelToRender sixelToRender) { - return; - } - - /// - public void Initialize() { - return; - } -} \ No newline at end of file diff --git a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden index 076210c..88c2bd6 100644 --- a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden +++ b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden @@ -3,23 +3,23 @@ NowPlayingViewTest.InitialState_ShowsEmptyNowPlayingInfo_0: - ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ - ┆ ?? ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ + ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ?? ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ no track no artist - --:-- --:-- + --:-- --:-- diff --git a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Tall.golden b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Tall.golden index a142a46..a29ce3d 100644 --- a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Tall.golden +++ b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Tall.golden @@ -6,7 +6,6 @@ NowPlayingViewTest.InitialState_ShowsEmptyNowPlayingInfo_Tall_0: ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ - ┆ ?? ┆ ┆ ┆ ┆ ┆ ┆ ┆ @@ -15,6 +14,7 @@ NowPlayingViewTest.InitialState_ShowsEmptyNowPlayingInfo_Tall_0: ┆ ┆ ┆ ┆ ┆ ┆ + ┆ ?? ┆ ┆ ┆ ┆ ┆ ┆ ┆ diff --git a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Wide.golden b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Wide.golden index 16f7dae..42eb5a4 100644 --- a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Wide.golden +++ b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Wide.golden @@ -4,11 +4,11 @@ NowPlayingViewTest.InitialState_ShowsEmptyNowPlayingInfo_Wide_0: ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ - ┆ ?? ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + ┆ ?? ┆ ┆ ┆ ┆ ┆ ┆ ┆ diff --git a/smoc.Tests/goldens/NowPlayingViewTest/OnPositionChanged_UpdatesPosition.golden b/smoc.Tests/goldens/NowPlayingViewTest/OnPositionChanged_UpdatesPosition.golden index d247e0d..706c045 100644 --- a/smoc.Tests/goldens/NowPlayingViewTest/OnPositionChanged_UpdatesPosition.golden +++ b/smoc.Tests/goldens/NowPlayingViewTest/OnPositionChanged_UpdatesPosition.golden @@ -4,11 +4,11 @@ NowPlayingViewTest.OnPositionChanged_UpdatesPosition_0: ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ - ┆ ?? ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + ┆ ?? ┆ ┆ ┆ ┆ ┆ ┆ ┆ diff --git a/smoc.Tests/goldens/NowPlayingViewTest/OnPositionChanged_UpdatesPosition_MultipleTimes.golden b/smoc.Tests/goldens/NowPlayingViewTest/OnPositionChanged_UpdatesPosition_MultipleTimes.golden index ccf4cae..6525d30 100644 --- a/smoc.Tests/goldens/NowPlayingViewTest/OnPositionChanged_UpdatesPosition_MultipleTimes.golden +++ b/smoc.Tests/goldens/NowPlayingViewTest/OnPositionChanged_UpdatesPosition_MultipleTimes.golden @@ -4,11 +4,11 @@ NowPlayingViewTest.OnPositionChanged_UpdatesPosition_MultipleTimes_0: ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ - ┆ ?? ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + ┆ ?? ┆ ┆ ┆ ┆ ┆ ┆ ┆ diff --git a/smoc.Tests/goldens/NowPlayingViewTest/OnSongChanged_SongIsNull_UpdatesSongDetails.golden b/smoc.Tests/goldens/NowPlayingViewTest/OnSongChanged_SongIsNull_UpdatesSongDetails.golden index ff03699..e43766a 100644 --- a/smoc.Tests/goldens/NowPlayingViewTest/OnSongChanged_SongIsNull_UpdatesSongDetails.golden +++ b/smoc.Tests/goldens/NowPlayingViewTest/OnSongChanged_SongIsNull_UpdatesSongDetails.golden @@ -4,11 +4,11 @@ NowPlayingViewTest.OnSongChanged_SongIsNull_UpdatesSongDetails_0: ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ - ┆ ?? ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + ┆ ?? ┆ ┆ ┆ ┆ ┆ ┆ ┆ diff --git a/smoc.Tests/goldens/NowPlayingViewTest/OnSongChanged_UpdatesSongDetails.golden b/smoc.Tests/goldens/NowPlayingViewTest/OnSongChanged_UpdatesSongDetails.golden index ecab2a7..7d67767 100644 --- a/smoc.Tests/goldens/NowPlayingViewTest/OnSongChanged_UpdatesSongDetails.golden +++ b/smoc.Tests/goldens/NowPlayingViewTest/OnSongChanged_UpdatesSongDetails.golden @@ -4,11 +4,11 @@ NowPlayingViewTest.OnSongChanged_UpdatesSongDetails_0: ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ - ┆ ?? ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ + ┆ ?? ┆ ┆ ┆ ┆ ┆ ┆ ┆ diff --git a/smoc/Program.cs b/smoc/Program.cs index b92c619..0f44ec3 100644 --- a/smoc/Program.cs +++ b/smoc/Program.cs @@ -8,7 +8,6 @@ using Smoc.Streaming.Subsonic; using Smoc.Streaming.YouTubeMusic; using Smoc.Ui; -using Smoc.Ui.Drawing; using Terminal.Gui; using Terminal.Gui.App; using Terminal.Gui.Configuration; @@ -62,9 +61,7 @@ public static async Task Main(string[] args) { application.Mouse.IsMouseDisabled = true; VimKeyBindings.AddNavigationKeyBindings(application.Keyboard.KeyBindings); IStreamingClient streamingClient = CreateStreamingClient(); - var sixelDriver = new SixelDriver(application); - sixelDriver.Initialize(); - using var window = new MainWindow(streamingClient, sixelDriver); + using var window = new MainWindow(streamingClient); try { application.Run(window, (e) => { Logging.Error(e.ToString()); diff --git a/smoc/Ui/Components/SixelImageView.cs b/smoc/Ui/Components/SixelImageView.cs index 5495ca4..b0ba500 100644 --- a/smoc/Ui/Components/SixelImageView.cs +++ b/smoc/Ui/Components/SixelImageView.cs @@ -1,8 +1,7 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -using Terminal.Gui.Drawing; -using Terminal.Gui.ViewBase; +using Terminal.Gui.Views; using Color = Terminal.Gui.Drawing.Color; namespace Smoc.Ui.Components; @@ -10,18 +9,14 @@ namespace Smoc.Ui.Components; /// /// A view that renders images using Sixel graphics sequences. /// -public sealed class SixelImageView : View { - private readonly IMainWindow _mainWindow; +public sealed class SixelImageView : ImageView { private Image? _image; - private SixelToRender? _sixelToRender; /// /// Initializes a new instance of the class. /// - /// The main window. /// The initial image to display. - public SixelImageView(IMainWindow mainWindow, Image? image = null) { - _mainWindow = mainWindow; + public SixelImageView(Image? image = null) { _image = image; } @@ -34,7 +29,6 @@ public void ClearImage() { } _image = null; - _sixelToRender = null; SetNeedsDraw(); } @@ -44,29 +38,15 @@ public void ClearImage() { /// The image to display. public void SetImage(Image image) { _image = image; - _sixelToRender = null; UpdateSixelData(); SetNeedsDraw(); } protected override void OnFrameChanged(in System.Drawing.Rectangle frame) { - base.OnFrameChanged(frame); UpdateSixelData(); SetNeedsDraw(); } - protected override bool OnDrawingContent(DrawContext? context) { - base.OnDrawingContent(context); - - if (_sixelToRender is not null) { - _mainWindow.SixelDriver.EnqueueSixel(_sixelToRender); - context?.AddDrawnRectangle(GetRenderableArea()); - return true; - } - - return false; - } - private System.Drawing.Rectangle GetRenderableArea() { var frame = FrameToScreen(); return new( @@ -77,17 +57,15 @@ private System.Drawing.Rectangle GetRenderableArea() { } private void UpdateSixelData() { - if (_image is null || !_mainWindow.SixelDriver.IsSupported) { + if (_image is null || App?.Driver?.SixelSupport is not { IsSupported: true }) { return; } + var resolution = App!.Driver!.SixelSupport!.Resolution; var boundsRect = GetRenderableArea(); var resizedImage = _image.Clone( - i => i.Resize(boundsRect.Width * _mainWindow.SixelDriver.Resolution!.Value.Width, boundsRect.Height * _mainWindow.SixelDriver.Resolution!.Value.Height)); - _sixelToRender = new SixelToRender() { - SixelData = _mainWindow.SixelDriver.EncodeSixel(ConvertToColorArray(resizedImage)), - ScreenPosition = new System.Drawing.Point(boundsRect.X, boundsRect.Y) - }; + i => i.Resize(boundsRect.Width * resolution.Width, boundsRect.Height * resolution.Height)); + Image = ConvertToColorArray(resizedImage); } private static Color[,] ConvertToColorArray(Image image) { diff --git a/smoc/Ui/Drawing/ISixelDriver.cs b/smoc/Ui/Drawing/ISixelDriver.cs deleted file mode 100644 index 28577c5..0000000 --- a/smoc/Ui/Drawing/ISixelDriver.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Drawing; -using Terminal.Gui.Drawing; -using Color = Terminal.Gui.Drawing.Color; - -namespace Smoc.Ui.Drawing; - -/// -/// Provides an abstraction over Sixel graphics rendering. -/// -public interface ISixelDriver { - - /// - /// Gets the aspect ratio (height/width) of terminal character cells in pixels. - /// - double CellAspectRatio { get; } - - /// - /// Gets a value indicating whether the terminal supports Sixel graphics. - /// - bool IsSupported { get; } - - /// - /// Gets the detected pixel resolution of a terminal character cell, or null if - /// detection has not yet completed. - /// - Size? Resolution { get; } - - /// - /// The maximum number of colors that can be included in a sixel image. Defaults - /// to 256. - /// - int MaxPaletteColors { get; } - - /// - /// Initializes the Sixel driver, triggering asynchronous detection of Sixel support - /// and terminal cell resolution. - /// - void Initialize(); - - /// - /// Enqueues a Sixel image for rendering in the terminal. If the driver has not yet - /// finished initializing, the render request is deferred until initialization completes. - /// - /// The Sixel render request to enqueue. - void EnqueueSixel(SixelToRender sixelToRender); - - /// - /// Encodes a two-dimensional array of colors into a Sixel-formatted string. - /// - /// A 2D array of colors representing the image to encode. - /// A Sixel-encoded string representation of the image. - string EncodeSixel(Color[,] colors); -} \ No newline at end of file diff --git a/smoc/Ui/Drawing/SixelDriver.cs b/smoc/Ui/Drawing/SixelDriver.cs deleted file mode 100644 index dc2e7fe..0000000 --- a/smoc/Ui/Drawing/SixelDriver.cs +++ /dev/null @@ -1,77 +0,0 @@ -namespace Smoc.Ui.Drawing; - -using System.Collections.Concurrent; -using System.Drawing; -using Terminal.Gui.App; -using Terminal.Gui.Drawing; -using Color = Terminal.Gui.Drawing.Color; - -/// -/// Standard implementation of . -/// -public sealed class SixelDriver : ISixelDriver { - private SixelSupportDetector? _sixelSupportDetector; - private SixelSupportResult? _sixelSupportResult; - private readonly IApplication _app; - private readonly ConcurrentQueue> _sixelInitQueue = new(); - - /// - public double CellAspectRatio => - _sixelSupportResult?.Resolution.Height / (double?)_sixelSupportResult?.Resolution.Width ?? 2.0; - - /// - public bool IsSupported => _sixelSupportResult?.IsSupported ?? false; - - /// - public Size? Resolution => _sixelSupportResult?.Resolution; - - /// - public int MaxPaletteColors => _sixelSupportResult?.MaxPaletteColors ?? 256; - - /// - /// Initializes a new instance of the class. - /// - /// The application instance to use when rendering. - public SixelDriver(IApplication app) { - _app = app; - } - - /// - public void Initialize() { - EnsureInitialized(); - } - - /// - public void EnqueueSixel(SixelToRender sixelToRender) { - if (_sixelSupportResult is null) { - _sixelInitQueue.Enqueue((driver) => driver.EnqueueSixel(sixelToRender)); - EnsureInitialized(); - } else if (_sixelSupportResult is not null && _sixelSupportResult.IsSupported) { - if (!_app.Driver!.GetSixels().Contains(sixelToRender)) { - _app.Driver!.GetSixels().Enqueue(sixelToRender); - } - } - } - - /// - public string EncodeSixel(Color[,] colors) { - var encoder = new SixelEncoder(); - encoder.Quantizer.MaxColors = Math.Min(encoder.Quantizer.MaxColors, MaxPaletteColors); - return encoder.EncodeSixel(colors); - } - - private void EnsureInitialized() { - if (_sixelSupportDetector is not null) { - return; - } - _sixelSupportDetector = new SixelSupportDetector(_app.Driver); - _sixelSupportDetector.Detect((result) => { - _app.Invoke(() => { - _sixelSupportResult = result; - while (_sixelInitQueue.TryDequeue(out var action)) { - action(this); - } - }); - }); - } -} \ No newline at end of file diff --git a/smoc/Ui/IMainWindow.cs b/smoc/Ui/IMainWindow.cs index 78122f9..5a217b8 100644 --- a/smoc/Ui/IMainWindow.cs +++ b/smoc/Ui/IMainWindow.cs @@ -1,4 +1,3 @@ -using Smoc.Ui.Drawing; using Smoc.Ui.Models; using Terminal.Gui.App; @@ -13,11 +12,6 @@ public interface IMainWindow { /// IApplication? App { get; } - /// - /// Gets the sixel driver. - /// - ISixelDriver SixelDriver { get; } - /// /// Changes the application's current mode (view). /// diff --git a/smoc/Ui/MainWindow.cs b/smoc/Ui/MainWindow.cs index d77eb99..2ffba35 100644 --- a/smoc/Ui/MainWindow.cs +++ b/smoc/Ui/MainWindow.cs @@ -4,7 +4,6 @@ using Smoc.Services.Audio.SoundFlow; using Smoc.Services.Streaming; using Smoc.Streaming; -using Smoc.Ui.Drawing; using Smoc.Ui.Models; using Terminal.Gui.Input; using Terminal.Gui.ViewBase; @@ -23,25 +22,20 @@ public sealed class MainWindow : Runnable, IMainWindow { private readonly IPlaybackQueueService _playbackQueueService; private readonly CommandService _commandService; private readonly IPlaybackTrackingService _playbackTrackingService; - private readonly ISixelDriver _sixelDriver; private Mode? _currentMode; private View? _preCommandFocusedView; private Mode? _preCommandMode; - /// - public ISixelDriver SixelDriver => _sixelDriver; - /// /// Initializes a new instance of the class. /// /// The initialized streaming client. - public MainWindow(IStreamingClient streamingClient, ISixelDriver sixelDriver) { + public MainWindow(IStreamingClient streamingClient) { Width = Dim.Fill(); Height = Dim.Fill(); CanFocus = true; - _sixelDriver = sixelDriver; _playbackQueueService = StandardPlaybackQueueService.UsingAudioService(this, streamingClient); _playbackTrackingService = new StreamingListenHistoryService( streamingClient, diff --git a/smoc/Ui/NowPlayingBar.cs b/smoc/Ui/NowPlayingBar.cs index 43afeef..b734c6b 100644 --- a/smoc/Ui/NowPlayingBar.cs +++ b/smoc/Ui/NowPlayingBar.cs @@ -56,18 +56,22 @@ public NowPlayingBar(IMainWindow mainWindow, IPlaybackQueueService playbackQueue Padding!.Thickness = new Thickness(0, 0, 1, 0); CanFocus = false; - _albumArtView = new SixelImageView(_mainWindow) { + _albumArtView = new SixelImageView() { X = Pos.Absolute(1), Y = Pos.Absolute(0), Height = Dim.Fill(), Width = Dim.Func((view) => { int height = view!.Frame.Height; - return (int)Math.Round(height * _mainWindow.SixelDriver.CellAspectRatio); + var resolution = App?.Driver?.SixelSupport?.Resolution ?? new System.Drawing.Size(1, 2); + return (int)Math.Round(height * ((double)resolution.Height / resolution.Width)); }, this) + 1, + SixelEncoder = new SixelEncoder(), BorderStyle = LineStyle.Dashed, TextAlignment = Alignment.Center, + VerticalTextAlignment = Alignment.Center, Text = "??" }; + _albumArtView.Padding!.Thickness = new Thickness(-1, -1, -1, -1); _albumArtView.Margin!.Thickness = new Thickness(0, 0, 1, 0); _songLabel = new Label() { diff --git a/smoc/Ui/NowPlayingView.cs b/smoc/Ui/NowPlayingView.cs index 040ea09..a7ed900 100644 --- a/smoc/Ui/NowPlayingView.cs +++ b/smoc/Ui/NowPlayingView.cs @@ -53,24 +53,29 @@ public NowPlayingView(IMainWindow mainWindow, CommandService commandService, IPl _streamingClient = streamingClient; Width = Dim.Fill(); Height = Dim.Fill(); - _albumArtView = new SixelImageView(_mainWindow) { + _albumArtView = new SixelImageView() { X = Pos.Center(), Y = Pos.Center() - Pos.Percent(10), + SixelEncoder = new SixelEncoder(), Height = Dim.Func((view) => { float maxHeight = view!.Frame.Height * _albumArtMaxViewportPercent; float maxWidth = view!.Frame.Width * _albumArtMaxViewportPercent; - return (int)Math.Round(Math.Min(maxHeight, maxWidth / _mainWindow.SixelDriver.CellAspectRatio)); + var resolution = App?.Driver?.SixelSupport?.Resolution ?? new System.Drawing.Size(1, 2); + return (int)Math.Round(Math.Min(maxHeight, maxWidth / ((double)resolution.Height / resolution.Width))); }, this), Width = Dim.Func((view) => { float maxHeight = view!.Frame.Height * _albumArtMaxViewportPercent; float maxWidth = view!.Frame.Width * _albumArtMaxViewportPercent; - double height = Math.Min(maxHeight, maxWidth / _mainWindow.SixelDriver.CellAspectRatio); - return (int)Math.Round(height * _mainWindow.SixelDriver.CellAspectRatio); + var resolution = App?.Driver?.SixelSupport?.Resolution ?? new System.Drawing.Size(1, 2); + double height = Math.Min(maxHeight, maxWidth / ((double)resolution.Height / resolution.Width)); + return (int)Math.Round(height * ((double)resolution.Height / resolution.Width)); }, this), BorderStyle = LineStyle.Dashed, TextAlignment = Alignment.Center, + VerticalTextAlignment = Alignment.Center, Text = "??" }; + _albumArtView.Padding!.Thickness = new Thickness(-1, -1, -1, -1); _albumArtView.Margin!.Thickness = new Thickness(0, 0, 1, 1); _songLabel = new Label() { From c4383d62296925300fb94e9ea28c0dd9da6b993e Mon Sep 17 00:00:00 2001 From: Matt Razza <504088+mrazza@users.noreply.github.com> Date: Sun, 10 May 2026 14:35:02 -0400 Subject: [PATCH 02/11] refactor: simplify SixelImageView image resizing and adjust UI component alignment --- Terminal.Gui | 2 +- ...itialState_ShowsEmptyNowPlayingInfo.golden | 24 +++++++++---------- smoc/Ui/Components/SixelImageView.cs | 16 ++----------- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/Terminal.Gui b/Terminal.Gui index 719614d..2940e24 160000 --- a/Terminal.Gui +++ b/Terminal.Gui @@ -1 +1 @@ -Subproject commit 719614d2f896b7274f1c22b9b8991a4d4d71f18d +Subproject commit 2940e24b7b9c49d0e3ae7b1cb40a303d6c5373a1 diff --git a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden index 88c2bd6..37d76ff 100644 --- a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden +++ b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden @@ -3,23 +3,23 @@ NowPlayingViewTest.InitialState_ShowsEmptyNowPlayingInfo_0: - ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ?? ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ + ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ?? ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ no track no artist - --:-- --:-- + --:-- --:-- diff --git a/smoc/Ui/Components/SixelImageView.cs b/smoc/Ui/Components/SixelImageView.cs index b0ba500..67f4533 100644 --- a/smoc/Ui/Components/SixelImageView.cs +++ b/smoc/Ui/Components/SixelImageView.cs @@ -46,25 +46,13 @@ protected override void OnFrameChanged(in System.Drawing.Rectangle frame) { UpdateSixelData(); SetNeedsDraw(); } - - private System.Drawing.Rectangle GetRenderableArea() { - var frame = FrameToScreen(); - return new( - frame.X + (Margin?.Thickness.Left ?? 0), - frame.Y + (Margin?.Thickness.Top ?? 0), - frame.Width - (Margin?.Thickness.Horizontal ?? 0), - frame.Height - (Margin?.Thickness.Vertical ?? 0)); - } - private void UpdateSixelData() { if (_image is null || App?.Driver?.SixelSupport is not { IsSupported: true }) { return; } - var resolution = App!.Driver!.SixelSupport!.Resolution; - var boundsRect = GetRenderableArea(); - var resizedImage = _image.Clone( - i => i.Resize(boundsRect.Width * resolution.Width, boundsRect.Height * resolution.Height)); + var targetSize = FitImageInViewportInPixels(new(_image.Width, _image.Height)); + var resizedImage = _image.Clone(i => i.Resize(targetSize.Width, targetSize.Height)); Image = ConvertToColorArray(resizedImage); } From 08df8730d0e2dd938c8fa84ad96171bae67aad86 Mon Sep 17 00:00:00 2001 From: Matt Razza <504088+mrazza@users.noreply.github.com> Date: Mon, 11 May 2026 18:02:27 -0400 Subject: [PATCH 03/11] chore: sync termgui branch --- Terminal.Gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui b/Terminal.Gui index 2940e24..35d50de 160000 --- a/Terminal.Gui +++ b/Terminal.Gui @@ -1 +1 @@ -Subproject commit 2940e24b7b9c49d0e3ae7b1cb40a303d6c5373a1 +Subproject commit 35d50de1766a9d57fc6a0fd53937bb56845997a7 From 5a52bc30d1b4ee0185a99004ea6ab0cb89b5ee30 Mon Sep 17 00:00:00 2001 From: Matt Razza <504088+mrazza@users.noreply.github.com> Date: Mon, 11 May 2026 23:20:13 -0400 Subject: [PATCH 04/11] chore: update to latest terminal.gui and toggle the border based on whether or not an image is being rendered --- Terminal.Gui | 2 +- smoc/Ui/NowPlayingBar.cs | 3 +++ smoc/Ui/NowPlayingView.cs | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui b/Terminal.Gui index 35d50de..e79e00d 160000 --- a/Terminal.Gui +++ b/Terminal.Gui @@ -1 +1 @@ -Subproject commit 35d50de1766a9d57fc6a0fd53937bb56845997a7 +Subproject commit e79e00db1b9ce22a233b5415324e09ce5b2c31b1 diff --git a/smoc/Ui/NowPlayingBar.cs b/smoc/Ui/NowPlayingBar.cs index b734c6b..667ead1 100644 --- a/smoc/Ui/NowPlayingBar.cs +++ b/smoc/Ui/NowPlayingBar.cs @@ -204,6 +204,7 @@ private async void OnSongChanged(object? sender, Song? song) { // Only bother downloading the album art if it has changed. if (!song.Album.Covers.Any()) { _albumArtView.ClearImage(); + _albumArtView.BorderStyle = LineStyle.Dashed; } else if (_currentAlbum != song.Album) { _currentAlbum = song.Album; _albumArtCancellationTokenSource?.Cancel(); @@ -214,6 +215,7 @@ private async void OnSongChanged(object? sender, Song? song) { Logging.Debug($"Album art loaded: {song.Title}"); token.ThrowIfCancellationRequested(); _albumArtView.SetImage(image); + _albumArtView.BorderStyle = LineStyle.None; } catch (OperationCanceledException) { Logging.Debug($"Album art load cancelled: {song.Title}"); } catch (Exception ex) { @@ -228,6 +230,7 @@ private void Reset() { _positionLabel.Text = "--:--"; _durationLabel.Text = "--:--"; _albumArtView.ClearImage(); + _albumArtView.BorderStyle = LineStyle.Dashed; _volumeLabel.Text = string.Format(Messages.VOLUME, (int)Math.Round(_playbackQueueService.Volume * 100)); _progressBar.Fraction = 0.0f; } diff --git a/smoc/Ui/NowPlayingView.cs b/smoc/Ui/NowPlayingView.cs index a7ed900..eec7deb 100644 --- a/smoc/Ui/NowPlayingView.cs +++ b/smoc/Ui/NowPlayingView.cs @@ -145,6 +145,7 @@ private async void OnSongChanged(object? sender, Song? song) { // Only bother downloading the album art if it has changed. if (!song.Album.Covers.Any()) { _albumArtView.ClearImage(); + _albumArtView.BorderStyle = LineStyle.Dashed; } else if (_currentAlbum != song.Album) { _currentAlbum = song.Album; _albumArtCancellationTokenSource?.Cancel(); @@ -155,6 +156,7 @@ private async void OnSongChanged(object? sender, Song? song) { Logging.Debug($"Album art loaded: {song.Title}"); token.ThrowIfCancellationRequested(); _albumArtView.SetImage(image); + _albumArtView.BorderStyle = LineStyle.None; } catch (OperationCanceledException) { Logging.Debug($"Album art load cancelled: {song.Title}"); } catch (Exception ex) { @@ -180,6 +182,7 @@ private void Reset() { _positionLabel.Text = "--:--"; _durationLabel.Text = "--:--"; _albumArtView.ClearImage(); + _albumArtView.BorderStyle = LineStyle.Dashed; _progressBar.Fraction = 0.0f; } } From ca5c017f5b580579cd15bf12cc5ea8c92b9b1c85 Mon Sep 17 00:00:00 2001 From: Matt Razza <504088+mrazza@users.noreply.github.com> Date: Mon, 11 May 2026 23:55:46 -0400 Subject: [PATCH 05/11] test: remove ASCII box borders from UI golden files --- .../OnSongChanged_UpdatesSongDetails.golden | 6 ++--- .../OnSongChanged_UpdatesSongDetails.golden | 22 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_UpdatesSongDetails.golden b/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_UpdatesSongDetails.golden index eb04c8f..91b30c7 100644 --- a/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_UpdatesSongDetails.golden +++ b/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_UpdatesSongDetails.golden @@ -1,7 +1,7 @@ NowPlayingBarTest.OnSongChanged_UpdatesSongDetails_0: - ┌╌╌╌╌┐ OnSongChangedUpdatesSongDetails - ┆ ?? ┆ Radiohead volume: 0% - └╌╌╌╌┘ --:-- --:-- + OnSongChangedUpdatesSongDetails + Radiohead volume: 0% + --:-- --:-- diff --git a/smoc.Tests/goldens/NowPlayingViewTest/OnSongChanged_UpdatesSongDetails.golden b/smoc.Tests/goldens/NowPlayingViewTest/OnSongChanged_UpdatesSongDetails.golden index 7d67767..70585e2 100644 --- a/smoc.Tests/goldens/NowPlayingViewTest/OnSongChanged_UpdatesSongDetails.golden +++ b/smoc.Tests/goldens/NowPlayingViewTest/OnSongChanged_UpdatesSongDetails.golden @@ -3,17 +3,17 @@ NowPlayingViewTest.OnSongChanged_UpdatesSongDetails_0: - ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ?? ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ + + + + + + ?? + + + + + OnSongChangedUpdatesSongDetails Radiohead From f039a9f3a0f9444acd8c477010997cf9bdaefdea Mon Sep 17 00:00:00 2001 From: Matt Razza <504088+mrazza@users.noreply.github.com> Date: Thu, 14 May 2026 20:08:06 -0400 Subject: [PATCH 06/11] chore: update terminal.gui --- Terminal.Gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui b/Terminal.Gui index e79e00d..5cc4b7f 160000 --- a/Terminal.Gui +++ b/Terminal.Gui @@ -1 +1 @@ -Subproject commit e79e00db1b9ce22a233b5415324e09ce5b2c31b1 +Subproject commit 5cc4b7f70cde25c0bfb57bc48b480a73c692dfbe From 71958cc0be78af0a7efa916606fe784afbd67b51 Mon Sep 17 00:00:00 2001 From: Matt Razza <504088+mrazza@users.noreply.github.com> Date: Thu, 14 May 2026 20:13:50 -0400 Subject: [PATCH 07/11] fix: adjust album art text and padding in NowPlayingBar UI --- .../goldens/NowPlayingBarTest/InitialState_ShowsEmpty.golden | 2 +- .../OnPositionChanged_UpdatesPosition.golden | 2 +- .../OnPositionChanged_UpdatesPosition_MultipleTimes.golden | 2 +- .../OnSongChanged_SongIsNull_UpdatesSongDetails.golden | 2 +- .../goldens/NowPlayingBarTest/Volume_ShowsVolume.golden | 2 +- .../NowPlayingBarTest/Volume_VolumeChanged_UpdatesUi.golden | 2 +- smoc/Ui/NowPlayingBar.cs | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/smoc.Tests/goldens/NowPlayingBarTest/InitialState_ShowsEmpty.golden b/smoc.Tests/goldens/NowPlayingBarTest/InitialState_ShowsEmpty.golden index 936944b..a819517 100644 --- a/smoc.Tests/goldens/NowPlayingBarTest/InitialState_ShowsEmpty.golden +++ b/smoc.Tests/goldens/NowPlayingBarTest/InitialState_ShowsEmpty.golden @@ -1,6 +1,6 @@ NowPlayingBarTest.InitialState_ShowsEmpty_0: ┌╌╌╌╌┐ no track - ┆ ?? ┆ no artist volume: 0% + ┆ ??⠀┆ no artist volume: 0% └╌╌╌╌┘ --:-- --:-- diff --git a/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition.golden b/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition.golden index d208087..b8b356b 100644 --- a/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition.golden +++ b/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition.golden @@ -1,6 +1,6 @@ NowPlayingBarTest.OnPositionChanged_UpdatesPosition_0: ┌╌╌╌╌┐ no track - ┆ ?? ┆ no artist volume: 0% + ┆ ??⠀┆ no artist volume: 0% └╌╌╌╌┘ 00:00 05:00 diff --git a/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition_MultipleTimes.golden b/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition_MultipleTimes.golden index 690fb02..26a56cf 100644 --- a/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition_MultipleTimes.golden +++ b/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition_MultipleTimes.golden @@ -1,6 +1,6 @@ NowPlayingBarTest.OnPositionChanged_UpdatesPosition_MultipleTimes_0: ┌╌╌╌╌┐ no track - ┆ ?? ┆ no artist volume: 0% + ┆ ??⠀┆ no artist volume: 0% └╌╌╌╌┘ 02:00 ███████████████████████████████ 05:00 diff --git a/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_SongIsNull_UpdatesSongDetails.golden b/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_SongIsNull_UpdatesSongDetails.golden index 3b17de6..becf018 100644 --- a/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_SongIsNull_UpdatesSongDetails.golden +++ b/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_SongIsNull_UpdatesSongDetails.golden @@ -1,6 +1,6 @@ NowPlayingBarTest.OnSongChanged_SongIsNull_UpdatesSongDetails_0: ┌╌╌╌╌┐ no track - ┆ ?? ┆ no artist volume: 0% + ┆ ??⠀┆ no artist volume: 0% └╌╌╌╌┘ --:-- --:-- diff --git a/smoc.Tests/goldens/NowPlayingBarTest/Volume_ShowsVolume.golden b/smoc.Tests/goldens/NowPlayingBarTest/Volume_ShowsVolume.golden index 3fcb209..eeb06cc 100644 --- a/smoc.Tests/goldens/NowPlayingBarTest/Volume_ShowsVolume.golden +++ b/smoc.Tests/goldens/NowPlayingBarTest/Volume_ShowsVolume.golden @@ -1,6 +1,6 @@ NowPlayingBarTest.Volume_ShowsVolume_0: ┌╌╌╌╌┐ no track - ┆ ?? ┆ no artist volume: 50% + ┆ ??⠀┆ no artist volume: 50% └╌╌╌╌┘ --:-- --:-- diff --git a/smoc.Tests/goldens/NowPlayingBarTest/Volume_VolumeChanged_UpdatesUi.golden b/smoc.Tests/goldens/NowPlayingBarTest/Volume_VolumeChanged_UpdatesUi.golden index e4cbc75..9b411e2 100644 --- a/smoc.Tests/goldens/NowPlayingBarTest/Volume_VolumeChanged_UpdatesUi.golden +++ b/smoc.Tests/goldens/NowPlayingBarTest/Volume_VolumeChanged_UpdatesUi.golden @@ -1,6 +1,6 @@ NowPlayingBarTest.Volume_VolumeChanged_UpdatesUi_0: ┌╌╌╌╌┐ no track - ┆ ?? ┆ no artist volume: 20% + ┆ ??⠀┆ no artist volume: 20% └╌╌╌╌┘ --:-- --:-- diff --git a/smoc/Ui/NowPlayingBar.cs b/smoc/Ui/NowPlayingBar.cs index 667ead1..fa19169 100644 --- a/smoc/Ui/NowPlayingBar.cs +++ b/smoc/Ui/NowPlayingBar.cs @@ -69,9 +69,9 @@ public NowPlayingBar(IMainWindow mainWindow, IPlaybackQueueService playbackQueue BorderStyle = LineStyle.Dashed, TextAlignment = Alignment.Center, VerticalTextAlignment = Alignment.Center, - Text = "??" + Text = "??⠀" }; - _albumArtView.Padding!.Thickness = new Thickness(-1, -1, -1, -1); + _albumArtView.Padding!.Thickness = new Thickness(0, -1, -2, -2); _albumArtView.Margin!.Thickness = new Thickness(0, 0, 1, 0); _songLabel = new Label() { From 62a75d99fa47954428d8b9a790189b3e0c9354a1 Mon Sep 17 00:00:00 2001 From: Matt Razza <504088+mrazza@users.noreply.github.com> Date: Thu, 14 May 2026 20:30:05 -0400 Subject: [PATCH 08/11] refactor: adjust UI layout padding and fix album art rendering in NowPlayingView and SixelImageView --- .../InitialState_ShowsEmpty.golden | 2 +- .../OnPositionChanged_UpdatesPosition.golden | 2 +- ...anged_UpdatesPosition_MultipleTimes.golden | 2 +- .../OnSongChanged_UpdatesSongDetails.golden | 2 +- .../Volume_ShowsVolume.golden | 2 +- .../Volume_VolumeChanged_UpdatesUi.golden | 2 +- ...State_ShowsEmptyNowPlayingInfo_Wide.golden | 24 ++++++------- smoc/Ui/Components/SixelImageView.cs | 34 ++++++++++++++++--- smoc/Ui/NowPlayingBar.cs | 5 ++- smoc/Ui/NowPlayingView.cs | 3 +- 10 files changed, 51 insertions(+), 27 deletions(-) diff --git a/smoc.Tests/goldens/NowPlayingBarTest/InitialState_ShowsEmpty.golden b/smoc.Tests/goldens/NowPlayingBarTest/InitialState_ShowsEmpty.golden index a819517..936944b 100644 --- a/smoc.Tests/goldens/NowPlayingBarTest/InitialState_ShowsEmpty.golden +++ b/smoc.Tests/goldens/NowPlayingBarTest/InitialState_ShowsEmpty.golden @@ -1,6 +1,6 @@ NowPlayingBarTest.InitialState_ShowsEmpty_0: ┌╌╌╌╌┐ no track - ┆ ??⠀┆ no artist volume: 0% + ┆ ?? ┆ no artist volume: 0% └╌╌╌╌┘ --:-- --:-- diff --git a/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition.golden b/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition.golden index b8b356b..d208087 100644 --- a/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition.golden +++ b/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition.golden @@ -1,6 +1,6 @@ NowPlayingBarTest.OnPositionChanged_UpdatesPosition_0: ┌╌╌╌╌┐ no track - ┆ ??⠀┆ no artist volume: 0% + ┆ ?? ┆ no artist volume: 0% └╌╌╌╌┘ 00:00 05:00 diff --git a/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition_MultipleTimes.golden b/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition_MultipleTimes.golden index 26a56cf..690fb02 100644 --- a/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition_MultipleTimes.golden +++ b/smoc.Tests/goldens/NowPlayingBarTest/OnPositionChanged_UpdatesPosition_MultipleTimes.golden @@ -1,6 +1,6 @@ NowPlayingBarTest.OnPositionChanged_UpdatesPosition_MultipleTimes_0: ┌╌╌╌╌┐ no track - ┆ ??⠀┆ no artist volume: 0% + ┆ ?? ┆ no artist volume: 0% └╌╌╌╌┘ 02:00 ███████████████████████████████ 05:00 diff --git a/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_UpdatesSongDetails.golden b/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_UpdatesSongDetails.golden index 91b30c7..cb0db8f 100644 --- a/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_UpdatesSongDetails.golden +++ b/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_UpdatesSongDetails.golden @@ -1,6 +1,6 @@ NowPlayingBarTest.OnSongChanged_UpdatesSongDetails_0: OnSongChangedUpdatesSongDetails - Radiohead volume: 0% + ?? Radiohead volume: 0% --:-- --:-- diff --git a/smoc.Tests/goldens/NowPlayingBarTest/Volume_ShowsVolume.golden b/smoc.Tests/goldens/NowPlayingBarTest/Volume_ShowsVolume.golden index eeb06cc..3fcb209 100644 --- a/smoc.Tests/goldens/NowPlayingBarTest/Volume_ShowsVolume.golden +++ b/smoc.Tests/goldens/NowPlayingBarTest/Volume_ShowsVolume.golden @@ -1,6 +1,6 @@ NowPlayingBarTest.Volume_ShowsVolume_0: ┌╌╌╌╌┐ no track - ┆ ??⠀┆ no artist volume: 50% + ┆ ?? ┆ no artist volume: 50% └╌╌╌╌┘ --:-- --:-- diff --git a/smoc.Tests/goldens/NowPlayingBarTest/Volume_VolumeChanged_UpdatesUi.golden b/smoc.Tests/goldens/NowPlayingBarTest/Volume_VolumeChanged_UpdatesUi.golden index 9b411e2..e4cbc75 100644 --- a/smoc.Tests/goldens/NowPlayingBarTest/Volume_VolumeChanged_UpdatesUi.golden +++ b/smoc.Tests/goldens/NowPlayingBarTest/Volume_VolumeChanged_UpdatesUi.golden @@ -1,6 +1,6 @@ NowPlayingBarTest.Volume_VolumeChanged_UpdatesUi_0: ┌╌╌╌╌┐ no track - ┆ ??⠀┆ no artist volume: 20% + ┆ ?? ┆ no artist volume: 20% └╌╌╌╌┘ --:-- --:-- diff --git a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Wide.golden b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Wide.golden index 42eb5a4..b05706f 100644 --- a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Wide.golden +++ b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Wide.golden @@ -3,23 +3,23 @@ NowPlayingViewTest.InitialState_ShowsEmptyNowPlayingInfo_Wide_0: - ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ?? ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ + ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ?? ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ no track no artist - --:-- --:-- + --:-- --:-- diff --git a/smoc/Ui/Components/SixelImageView.cs b/smoc/Ui/Components/SixelImageView.cs index 67f4533..0e2aa2d 100644 --- a/smoc/Ui/Components/SixelImageView.cs +++ b/smoc/Ui/Components/SixelImageView.cs @@ -1,6 +1,7 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using Terminal.Gui.App; using Terminal.Gui.Views; using Color = Terminal.Gui.Drawing.Color; @@ -11,6 +12,7 @@ namespace Smoc.Ui.Components; /// public sealed class SixelImageView : ImageView { private Image? _image; + private CancellationTokenSource? _cancellationTokenSource; /// /// Initializes a new instance of the class. @@ -29,6 +31,7 @@ public void ClearImage() { } _image = null; + Image = null; SetNeedsDraw(); } @@ -39,21 +42,44 @@ public void ClearImage() { public void SetImage(Image image) { _image = image; UpdateSixelData(); - SetNeedsDraw(); } protected override void OnFrameChanged(in System.Drawing.Rectangle frame) { UpdateSixelData(); - SetNeedsDraw(); } + private void UpdateSixelData() { + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource = new CancellationTokenSource(); + + var token = _cancellationTokenSource.Token; + Task.Run(() => { + Color[,]? data = null; + try { + data = GenerateSixelData(); + } catch (Exception ex) { + Logging.Warning($"Failed to render album art: {ex.Message}"); + return; + } + + App?.Invoke(() => { + if (token.IsCancellationRequested) { + return; + } + Image = data; + SetNeedsDraw(); + }); + }, token); + } + + private Color[,] GenerateSixelData() { if (_image is null || App?.Driver?.SixelSupport is not { IsSupported: true }) { - return; + throw new InvalidOperationException("Sixel not supported."); } var targetSize = FitImageInViewportInPixels(new(_image.Width, _image.Height)); var resizedImage = _image.Clone(i => i.Resize(targetSize.Width, targetSize.Height)); - Image = ConvertToColorArray(resizedImage); + return ConvertToColorArray(resizedImage); } private static Color[,] ConvertToColorArray(Image image) { diff --git a/smoc/Ui/NowPlayingBar.cs b/smoc/Ui/NowPlayingBar.cs index fa19169..c1658e7 100644 --- a/smoc/Ui/NowPlayingBar.cs +++ b/smoc/Ui/NowPlayingBar.cs @@ -69,9 +69,8 @@ public NowPlayingBar(IMainWindow mainWindow, IPlaybackQueueService playbackQueue BorderStyle = LineStyle.Dashed, TextAlignment = Alignment.Center, VerticalTextAlignment = Alignment.Center, - Text = "??⠀" + Text = "??" }; - _albumArtView.Padding!.Thickness = new Thickness(0, -1, -2, -2); _albumArtView.Margin!.Thickness = new Thickness(0, 0, 1, 0); _songLabel = new Label() { @@ -214,8 +213,8 @@ private async void OnSongChanged(object? sender, Song? song) { var image = await _streamingClient.GetAlbumArtAsync(song.Album, (covers) => covers.OrderBy(c => c.Width).First(), token); Logging.Debug($"Album art loaded: {song.Title}"); token.ThrowIfCancellationRequested(); - _albumArtView.SetImage(image); _albumArtView.BorderStyle = LineStyle.None; + _albumArtView.SetImage(image); } catch (OperationCanceledException) { Logging.Debug($"Album art load cancelled: {song.Title}"); } catch (Exception ex) { diff --git a/smoc/Ui/NowPlayingView.cs b/smoc/Ui/NowPlayingView.cs index eec7deb..3e5589b 100644 --- a/smoc/Ui/NowPlayingView.cs +++ b/smoc/Ui/NowPlayingView.cs @@ -75,7 +75,6 @@ public NowPlayingView(IMainWindow mainWindow, CommandService commandService, IPl VerticalTextAlignment = Alignment.Center, Text = "??" }; - _albumArtView.Padding!.Thickness = new Thickness(-1, -1, -1, -1); _albumArtView.Margin!.Thickness = new Thickness(0, 0, 1, 1); _songLabel = new Label() { @@ -155,8 +154,8 @@ private async void OnSongChanged(object? sender, Song? song) { var image = await _streamingClient.GetAlbumArtAsync(song.Album, (covers) => covers.OrderByDescending(c => c.Width).First(), token); Logging.Debug($"Album art loaded: {song.Title}"); token.ThrowIfCancellationRequested(); - _albumArtView.SetImage(image); _albumArtView.BorderStyle = LineStyle.None; + _albumArtView.SetImage(image); } catch (OperationCanceledException) { Logging.Debug($"Album art load cancelled: {song.Title}"); } catch (Exception ex) { From 880737d7ec6881214a531e21b0b56b24387dd140 Mon Sep 17 00:00:00 2001 From: Matt Razza <504088+mrazza@users.noreply.github.com> Date: Thu, 14 May 2026 20:31:06 -0400 Subject: [PATCH 09/11] test: update golden file snapshots for layout and padding alignment changes --- ...anged_SongIsNull_UpdatesSongDetails.golden | 2 +- ...itialState_ShowsEmptyNowPlayingInfo.golden | 24 +++++++++---------- ...State_ShowsEmptyNowPlayingInfo_Wide.golden | 24 +++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_SongIsNull_UpdatesSongDetails.golden b/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_SongIsNull_UpdatesSongDetails.golden index becf018..3b17de6 100644 --- a/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_SongIsNull_UpdatesSongDetails.golden +++ b/smoc.Tests/goldens/NowPlayingBarTest/OnSongChanged_SongIsNull_UpdatesSongDetails.golden @@ -1,6 +1,6 @@ NowPlayingBarTest.OnSongChanged_SongIsNull_UpdatesSongDetails_0: ┌╌╌╌╌┐ no track - ┆ ??⠀┆ no artist volume: 0% + ┆ ?? ┆ no artist volume: 0% └╌╌╌╌┘ --:-- --:-- diff --git a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden index 37d76ff..88c2bd6 100644 --- a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden +++ b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden @@ -3,23 +3,23 @@ NowPlayingViewTest.InitialState_ShowsEmptyNowPlayingInfo_0: - ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ?? ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ + ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ?? ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ no track no artist - --:-- --:-- + --:-- --:-- diff --git a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Wide.golden b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Wide.golden index b05706f..42eb5a4 100644 --- a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Wide.golden +++ b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo_Wide.golden @@ -3,23 +3,23 @@ NowPlayingViewTest.InitialState_ShowsEmptyNowPlayingInfo_Wide_0: - ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ?? ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ + ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ?? ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ no track no artist - --:-- --:-- + --:-- --:-- From 7c00831f6576d2edc9e0fab39d3f841e4efefbd1 Mon Sep 17 00:00:00 2001 From: Matt Razza <504088+mrazza@users.noreply.github.com> Date: Thu, 14 May 2026 20:34:20 -0400 Subject: [PATCH 10/11] test: update NowPlayingView golden output to reflect adjusted layout alignment --- ...itialState_ShowsEmptyNowPlayingInfo.golden | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden index 88c2bd6..e4c92c5 100644 --- a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden +++ b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden @@ -3,23 +3,23 @@ NowPlayingViewTest.InitialState_ShowsEmptyNowPlayingInfo_0: - ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ?? ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ - - no track - no artist - - - --:-- --:-- + ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ?? ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ + + no track + no artist + + + --:-- --:-- From a7aed3a65a31e2bc9c8b5d2947b765d17af610a2 Mon Sep 17 00:00:00 2001 From: Matt Razza <504088+mrazza@users.noreply.github.com> Date: Thu, 14 May 2026 21:04:35 -0400 Subject: [PATCH 11/11] test: initialize test environment to mock driver IO and update NowPlayingView golden layout --- smoc.Tests/TestInit.cs | 15 ++++++++ ...itialState_ShowsEmptyNowPlayingInfo.golden | 34 +++++++++---------- 2 files changed, 32 insertions(+), 17 deletions(-) create mode 100644 smoc.Tests/TestInit.cs diff --git a/smoc.Tests/TestInit.cs b/smoc.Tests/TestInit.cs new file mode 100644 index 0000000..65b2e0f --- /dev/null +++ b/smoc.Tests/TestInit.cs @@ -0,0 +1,15 @@ +using System.Runtime.CompilerServices; + +/// +/// Initializes the test environment. +/// +internal static class TestInitializer { + /// + /// Sets up the test environment. + /// + [ModuleInitializer] + public static void SetupEnvironment() { + // Disable real driver IO to prevent terminal.gui from opening a real terminal. + Environment.SetEnvironmentVariable("DisableRealDriverIO", "1"); + } +} \ No newline at end of file diff --git a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden index e4c92c5..37d76ff 100644 --- a/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden +++ b/smoc.Tests/goldens/NowPlayingViewTest/InitialState_ShowsEmptyNowPlayingInfo.golden @@ -3,23 +3,23 @@ NowPlayingViewTest.InitialState_ShowsEmptyNowPlayingInfo_0: - ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ?? ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - ┆ ┆ - └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ - - no track - no artist - - - --:-- --:-- + ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ?? ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + ┆ ┆ + └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ + + no track + no artist + + + --:-- --:--