Footer shows recording drive free space
Some checks failed
CI / build-and-test (push) Failing after 32s

Operators recording long shows previously had to open File Explorer to check disk pressure. New '· 245 GB free' indicator next to the REC badge polls DriveInfo on the recording drive at the existing 1Hz stats tick. Coral tint kicks in below 10GB; existing DiskSpaceWatcher still auto-disables recording at 1GB as a hard safety net.

FormatBytes helper produces footer-readable strings: '1.2 TB' / '245 GB' (no decimal for 100+ GB to avoid clutter) / '8.4 GB' (decimal for the low-warning case) / '450 MB'.

Polling is wrapped in try/catch — network paths occasionally throw, and disk-space display is a comfort feature, not a critical signal.
This commit is contained in:
Zac Gaetano 2026-05-10 21:19:34 -04:00
parent 61dce2eecd
commit 5c491c9d83
2 changed files with 86 additions and 1 deletions

View file

@ -367,12 +367,33 @@
<TextBlock Style="{StaticResource Wd.Text.Mono}"
FontSize="11"
Foreground="{DynamicResource Wd.Accent.Coral}"
VerticalAlignment="Center">
VerticalAlignment="Center"
Margin="0,0,8,0">
<Run Text="REC "/>
<Run Text="{Binding ActiveRecordingCount, Mode=OneWay}"/>
<Run Text=" · "/>
<Run Text="{Binding RecordingElapsed, Mode=OneWay}"/>
</TextBlock>
<!-- Free disk space on the recording drive. Coral
tint kicks in below 10GB; existing DiskSpaceWatcher
auto-disables recording at 1GB. -->
<TextBlock VerticalAlignment="Center"
ToolTip="Free disk space on the recording drive. Recording auto-disables when free space drops below 1GB.">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource Wd.Text.Mono}">
<Setter Property="FontSize" Value="11"/>
<Setter Property="Foreground" Value="{DynamicResource Wd.Text.Tertiary}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsLowDiskSpace}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Wd.Accent.Coral}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
<Run Text="· "/>
<Run Text="{Binding RecordingFreeSpace, Mode=OneWay}"/>
<Run Text=" free"/>
</TextBlock>
</StackPanel>
<!-- Control surface badge — cyan dot + REST/OSC string when active -->

View file

@ -221,6 +221,26 @@ public sealed class MainViewModel : ObservableObject, IDisposable
private string _recordingElapsed = string.Empty;
private DateTimeOffset? _recordingStartedAt;
/// <summary>
/// Free disk space on the recording drive, formatted as "245 GB" /
/// "1.2 TB" / "8.4 GB". Empty when recording isn't enabled or the
/// recording path is invalid. Polled at the existing 1Hz stats tick.
/// </summary>
public string RecordingFreeSpace
{
get => _recordingFreeSpace;
private set => SetField(ref _recordingFreeSpace, value);
}
private string _recordingFreeSpace = string.Empty;
/// <summary>True when free disk space drops below 10GB — UI cues coral.</summary>
public bool IsLowDiskSpace
{
get => _isLowDiskSpace;
private set => SetField(ref _isLowDiskSpace, value);
}
private bool _isLowDiskSpace;
/// <summary>True when the REST control surface (or OSC bridge, or both) is listening.</summary>
public bool IsControlSurfaceRunning
{
@ -555,6 +575,23 @@ public sealed class MainViewModel : ObservableObject, IDisposable
Toast.Show($"Stopped {enabled.Length} ISO(s)");
}
/// <summary>
/// Format a byte count as a human-readable string with 0-1 decimal
/// places — e.g. "8.4 GB", "245 GB", "1.2 TB". Optimized for footer
/// readability over precision: nobody needs to know it's 245.34 GB.
/// </summary>
private static string FormatBytes(long bytes)
{
if (bytes < 0) return "—";
const long GB = 1024L * 1024 * 1024;
const long TB = GB * 1024;
if (bytes >= TB) return $"{bytes / (double)TB:0.0} TB";
if (bytes >= GB) return bytes >= 100 * GB
? $"{bytes / GB} GB" // 100+ GB: no decimal — clutter
: $"{bytes / (double)GB:0.0} GB"; // < 100GB: one decimal for the low-warning case
return $"{bytes / 1024 / 1024} MB";
}
/// <summary>
/// Pull the meaningful "meeting title" out of Teams' raw window title.
/// Teams uses formats like:
@ -628,11 +665,38 @@ public sealed class MainViewModel : ObservableObject, IDisposable
RecordingElapsed = recElapsed.TotalHours >= 1
? $"{(int)recElapsed.TotalHours:D2}:{recElapsed.Minutes:D2}:{recElapsed.Seconds:D2}"
: $"{recElapsed.Minutes:D2}:{recElapsed.Seconds:D2}";
// Disk free on the recording drive. DriveInfo is cheap (a
// single GetDiskFreeSpaceEx call). We tolerate any failure
// path silently — disk-space display is a comfort feature, not
// a critical signal, and the existing DiskSpaceWatcher already
// handles the auto-disable-at-1GB safety net.
try
{
var dir = Settings.RecordingDirectory;
if (!string.IsNullOrEmpty(dir))
{
var root = System.IO.Path.GetPathRoot(dir);
if (!string.IsNullOrEmpty(root))
{
var drive = new System.IO.DriveInfo(root);
if (drive.IsReady)
{
var freeBytes = drive.AvailableFreeSpace;
RecordingFreeSpace = FormatBytes(freeBytes);
IsLowDiskSpace = freeBytes < 10L * 1024 * 1024 * 1024; // <10GB
}
}
}
}
catch { /* defensive — drive enumeration occasionally throws on network paths */ }
}
else
{
_recordingStartedAt = null;
RecordingElapsed = string.Empty;
RecordingFreeSpace = string.Empty;
IsLowDiskSpace = false;
}
// Session timer — start on first ISO going live, reset when none are