diff --git a/src/TeamsISO.App/MainWindow.xaml b/src/TeamsISO.App/MainWindow.xaml index a787a96..cfe66b7 100644 --- a/src/TeamsISO.App/MainWindow.xaml +++ b/src/TeamsISO.App/MainWindow.xaml @@ -367,12 +367,33 @@ + VerticalAlignment="Center" + Margin="0,0,8,0"> + + + + + + + + + diff --git a/src/TeamsISO.App/ViewModels/MainViewModel.cs b/src/TeamsISO.App/ViewModels/MainViewModel.cs index 223c2d5..37c79e9 100644 --- a/src/TeamsISO.App/ViewModels/MainViewModel.cs +++ b/src/TeamsISO.App/ViewModels/MainViewModel.cs @@ -221,6 +221,26 @@ public sealed class MainViewModel : ObservableObject, IDisposable private string _recordingElapsed = string.Empty; private DateTimeOffset? _recordingStartedAt; + /// + /// 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. + /// + public string RecordingFreeSpace + { + get => _recordingFreeSpace; + private set => SetField(ref _recordingFreeSpace, value); + } + private string _recordingFreeSpace = string.Empty; + + /// True when free disk space drops below 10GB — UI cues coral. + public bool IsLowDiskSpace + { + get => _isLowDiskSpace; + private set => SetField(ref _isLowDiskSpace, value); + } + private bool _isLowDiskSpace; + /// True when the REST control surface (or OSC bridge, or both) is listening. public bool IsControlSurfaceRunning { @@ -555,6 +575,23 @@ public sealed class MainViewModel : ObservableObject, IDisposable Toast.Show($"Stopped {enabled.Length} ISO(s)"); } + /// + /// 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. + /// + 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"; + } + /// /// 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