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
+
+
+ --:-- --:--