mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-18 03:33:40 +02:00
terminal compatibility modes and fps counter
This commit is contained in:
@@ -33,6 +33,7 @@ add_library(prism_tui_analysis STATIC
|
||||
src/cli.cpp
|
||||
src/dashboard_layout.cpp
|
||||
src/display_model.cpp
|
||||
src/frame_rate_meter.cpp
|
||||
src/meter_display_model.cpp
|
||||
src/output_selection.cpp
|
||||
src/profile_library.cpp
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#include "frame_rate_meter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace Prism::Tui {
|
||||
|
||||
FrameRateMeter::FrameRateMeter(double windowSeconds)
|
||||
: windowSeconds_(
|
||||
std::isfinite(windowSeconds)
|
||||
? std::clamp(windowSeconds, 0.25, 5.0)
|
||||
: 1.0) {}
|
||||
|
||||
double FrameRateMeter::record(double timestampSeconds) {
|
||||
if (!std::isfinite(timestampSeconds)) return framesPerSecond_;
|
||||
if (!timestamps_.empty() && timestampSeconds <= timestamps_.back()) {
|
||||
if (timestampSeconds < timestamps_.back()) reset();
|
||||
return framesPerSecond_;
|
||||
}
|
||||
|
||||
timestamps_.push_back(timestampSeconds);
|
||||
const double cutoff = timestampSeconds - windowSeconds_;
|
||||
while (timestamps_.size() > 2 && timestamps_[1] <= cutoff) {
|
||||
timestamps_.pop_front();
|
||||
}
|
||||
if (timestamps_.size() < 2) {
|
||||
framesPerSecond_ = 0.0;
|
||||
return framesPerSecond_;
|
||||
}
|
||||
|
||||
const double elapsed = timestamps_.back() - timestamps_.front();
|
||||
const double minimumSampleWindow = std::min(0.25, windowSeconds_);
|
||||
if (elapsed < minimumSampleWindow) return framesPerSecond_;
|
||||
|
||||
framesPerSecond_ = elapsed > 0.0
|
||||
? static_cast<double>(timestamps_.size() - 1) / elapsed
|
||||
: framesPerSecond_;
|
||||
return framesPerSecond_;
|
||||
}
|
||||
|
||||
void FrameRateMeter::reset() {
|
||||
timestamps_.clear();
|
||||
framesPerSecond_ = 0.0;
|
||||
}
|
||||
|
||||
} // namespace Prism::Tui
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
|
||||
namespace Prism::Tui {
|
||||
|
||||
class FrameRateMeter {
|
||||
public:
|
||||
explicit FrameRateMeter(double windowSeconds = 1.0);
|
||||
|
||||
double record(double timestampSeconds);
|
||||
double framesPerSecond() const { return framesPerSecond_; }
|
||||
void reset();
|
||||
|
||||
private:
|
||||
double windowSeconds_ = 1.0;
|
||||
double framesPerSecond_ = 0.0;
|
||||
std::deque<double> timestamps_;
|
||||
};
|
||||
|
||||
} // namespace Prism::Tui
|
||||
@@ -430,6 +430,8 @@ bool profileSettingsEqual(const TuiSettings& left,
|
||||
TuiSettings normalizedLeft = normalizeSettings(left);
|
||||
TuiSettings normalizedRight = normalizeSettings(right);
|
||||
normalizedLeft.refreshRate = normalizedRight.refreshRate;
|
||||
normalizedLeft.terminalCompatibility =
|
||||
normalizedRight.terminalCompatibility;
|
||||
return normalizedLeft == normalizedRight;
|
||||
}
|
||||
|
||||
@@ -437,6 +439,8 @@ TuiSettings applyProfileSettings(const TuiSettings& profile,
|
||||
const TuiSettings& working) {
|
||||
TuiSettings applied = normalizeSettings(profile);
|
||||
applied.refreshRate = normalizeSettings(working).refreshRate;
|
||||
applied.terminalCompatibility =
|
||||
normalizeSettings(working).terminalCompatibility;
|
||||
return applied;
|
||||
}
|
||||
|
||||
|
||||
+118
-6
@@ -3,6 +3,7 @@
|
||||
#include "analysis_pipeline.h"
|
||||
#include "dashboard_layout.h"
|
||||
#include "display_model.h"
|
||||
#include "frame_rate_meter.h"
|
||||
#include "meter_display_model.h"
|
||||
#include "output_selection.h"
|
||||
#include "profile_library.h"
|
||||
@@ -200,9 +201,12 @@ struct InterfaceState {
|
||||
uint64_t appliedOutputSwitchSerial = 0;
|
||||
uint64_t outputListSerial = 0;
|
||||
uint64_t appliedOutputListSerial = 0;
|
||||
double renderedFramesPerSecond = 0.0;
|
||||
};
|
||||
|
||||
const TuiTheme* renderTheme = nullptr;
|
||||
TerminalCompatibilityMode renderTerminalCompatibility =
|
||||
TerminalCompatibilityMode::Modern;
|
||||
|
||||
const TuiTheme& palette() {
|
||||
static const TuiTheme fallback = defaultTuiTheme();
|
||||
@@ -210,7 +214,83 @@ const TuiTheme& palette() {
|
||||
}
|
||||
|
||||
ftxui::Color terminalColor(const ThemeColor& color) {
|
||||
return ftxui::Color::RGB(color.red, color.green, color.blue);
|
||||
using Color = ftxui::Color;
|
||||
if (renderTerminalCompatibility == TerminalCompatibilityMode::Modern) {
|
||||
return Color::RGB(color.red, color.green, color.blue);
|
||||
}
|
||||
|
||||
if (renderTerminalCompatibility == TerminalCompatibilityMode::Compatible) {
|
||||
constexpr std::array<int, 6> levels = {0, 95, 135, 175, 215, 255};
|
||||
const auto nearestLevel = [&](int channel) {
|
||||
size_t nearest = 0;
|
||||
int nearestDistance = 256;
|
||||
for (size_t index = 0; index < levels.size(); ++index) {
|
||||
const int distance = std::abs(channel - levels[index]);
|
||||
if (distance < nearestDistance) {
|
||||
nearest = index;
|
||||
nearestDistance = distance;
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
};
|
||||
const size_t red = nearestLevel(color.red);
|
||||
const size_t green = nearestLevel(color.green);
|
||||
const size_t blue = nearestLevel(color.blue);
|
||||
const int cubeRed = levels[red];
|
||||
const int cubeGreen = levels[green];
|
||||
const int cubeBlue = levels[blue];
|
||||
const int cubeDistance =
|
||||
(static_cast<int>(color.red) - cubeRed) *
|
||||
(static_cast<int>(color.red) - cubeRed) +
|
||||
(static_cast<int>(color.green) - cubeGreen) *
|
||||
(static_cast<int>(color.green) - cubeGreen) +
|
||||
(static_cast<int>(color.blue) - cubeBlue) *
|
||||
(static_cast<int>(color.blue) - cubeBlue);
|
||||
const int average = (
|
||||
static_cast<int>(color.red) + color.green + color.blue) / 3;
|
||||
const int grayIndex = std::clamp(
|
||||
static_cast<int>(std::lround((average - 8) / 10.0)), 0, 23);
|
||||
const int gray = 8 + grayIndex * 10;
|
||||
const int grayDistance =
|
||||
(static_cast<int>(color.red) - gray) *
|
||||
(static_cast<int>(color.red) - gray) +
|
||||
(static_cast<int>(color.green) - gray) *
|
||||
(static_cast<int>(color.green) - gray) +
|
||||
(static_cast<int>(color.blue) - gray) *
|
||||
(static_cast<int>(color.blue) - gray);
|
||||
const int index = grayDistance < cubeDistance
|
||||
? 232 + grayIndex
|
||||
: 16 + static_cast<int>(red * 36 + green * 6 + blue);
|
||||
return Color(static_cast<Color::Palette256>(index));
|
||||
}
|
||||
|
||||
static constexpr std::array<std::array<int, 3>, 16> ansi = {{
|
||||
{{0, 0, 0}}, {{205, 0, 0}}, {{0, 205, 0}}, {{205, 205, 0}},
|
||||
{{0, 0, 238}}, {{205, 0, 205}}, {{0, 205, 205}}, {{229, 229, 229}},
|
||||
{{127, 127, 127}}, {{255, 0, 0}}, {{0, 255, 0}}, {{255, 255, 0}},
|
||||
{{92, 92, 255}}, {{255, 0, 255}}, {{0, 255, 255}}, {{255, 255, 255}},
|
||||
}};
|
||||
size_t nearest = 0;
|
||||
int nearestDistance = 3 * 256 * 256;
|
||||
for (size_t index = 0; index < ansi.size(); ++index) {
|
||||
const int red = static_cast<int>(color.red) - ansi[index][0];
|
||||
const int green = static_cast<int>(color.green) - ansi[index][1];
|
||||
const int blue = static_cast<int>(color.blue) - ansi[index][2];
|
||||
const int distance = red * red + green * green + blue * blue;
|
||||
if (distance < nearestDistance) {
|
||||
nearest = index;
|
||||
nearestDistance = distance;
|
||||
}
|
||||
}
|
||||
return Color(static_cast<Color::Palette16>(nearest));
|
||||
}
|
||||
|
||||
ftxui::Color terminalRgb(int red, int green, int blue) {
|
||||
return terminalColor({
|
||||
static_cast<uint8_t>(std::clamp(red, 0, 255)),
|
||||
static_cast<uint8_t>(std::clamp(green, 0, 255)),
|
||||
static_cast<uint8_t>(std::clamp(blue, 0, 255)),
|
||||
});
|
||||
}
|
||||
|
||||
void fillCanvasBackground(ftxui::Canvas& surface,
|
||||
@@ -268,6 +348,7 @@ const size_t& settingsSelection(const InterfaceState& state) {
|
||||
|
||||
std::string makeCaptureStatus(const DisplayFrame& frame,
|
||||
const TuiSettings& settings,
|
||||
double renderedFramesPerSecond,
|
||||
bool compact) {
|
||||
std::ostringstream sampleRate;
|
||||
const double kilohertz = frame.sampleRate / 1000.0;
|
||||
@@ -287,6 +368,19 @@ std::string makeCaptureStatus(const DisplayFrame& frame,
|
||||
if (frame.captureOverrun) {
|
||||
footer += " • capture overrun";
|
||||
}
|
||||
if (!compact) {
|
||||
const int target = effectiveRefreshRate(
|
||||
settings.refreshRate, settings.terminalCompatibility);
|
||||
footer += " • ";
|
||||
if (renderedFramesPerSecond > 0.0 &&
|
||||
std::isfinite(renderedFramesPerSecond)) {
|
||||
footer += std::to_string(static_cast<int>(
|
||||
std::lround(renderedFramesPerSecond)));
|
||||
} else {
|
||||
footer += "--";
|
||||
}
|
||||
footer += "/" + std::to_string(target) + " FPS";
|
||||
}
|
||||
return footer;
|
||||
}
|
||||
|
||||
@@ -1185,7 +1279,7 @@ ftxui::Color interpolateColor(const ThemeColor& from,
|
||||
const ThemeColor& to,
|
||||
float amount) {
|
||||
const float t = std::clamp(amount, 0.0f, 1.0f);
|
||||
return ftxui::Color::RGB(
|
||||
return terminalRgb(
|
||||
static_cast<uint8_t>(std::lround(from.red +
|
||||
(static_cast<int>(to.red) - from.red) * t)),
|
||||
static_cast<uint8_t>(std::lround(from.green +
|
||||
@@ -1448,7 +1542,7 @@ ftxui::Color waveformBandColor(const float* summary, bool multiband) {
|
||||
weights[band] * static_cast<float>(value)));
|
||||
}
|
||||
}
|
||||
return ftxui::Color::RGB(
|
||||
return terminalRgb(
|
||||
std::clamp(mixed[0], 0, 255),
|
||||
std::clamp(mixed[1], 0, 255),
|
||||
std::clamp(mixed[2], 0, 255));
|
||||
@@ -1706,7 +1800,11 @@ ftxui::Element renderFooter(const DisplayFrame& frame,
|
||||
" • o outputs • p profiles • s settings • q quit"
|
||||
: "Tab focus • Enter " + enterAction +
|
||||
" • o outputs • p profiles • s settings • l edit layout • r reset • q quit";
|
||||
auto status = text(makeCaptureStatus(frame, state.settings, compact)) | dim;
|
||||
auto status = text(makeCaptureStatus(
|
||||
frame,
|
||||
state.settings,
|
||||
state.renderedFramesPerSecond,
|
||||
compact)) | dim;
|
||||
if (frame.captureOverrun) {
|
||||
status = status | color(terminalColor(palette().danger));
|
||||
}
|
||||
@@ -2395,7 +2493,12 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
||||
const TuiSettings requestedSettings = settingsStore.read();
|
||||
if (requestedSettings != appliedSettings) {
|
||||
const bool refreshChanged =
|
||||
requestedSettings.refreshRate != appliedSettings.refreshRate;
|
||||
effectiveRefreshRate(
|
||||
requestedSettings.refreshRate,
|
||||
requestedSettings.terminalCompatibility) !=
|
||||
effectiveRefreshRate(
|
||||
appliedSettings.refreshRate,
|
||||
appliedSettings.terminalCompatibility);
|
||||
pipeline->setInputTrimDb(requestedSettings.inputTrimDb);
|
||||
pipeline->setSpectrumTilt(
|
||||
requestedSettings.spectrumTiltDbPerOctave);
|
||||
@@ -2446,7 +2549,9 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
||||
screen.PostEvent(Event::Custom);
|
||||
}
|
||||
nextFrameAt = now +
|
||||
displayFrameInterval(appliedSettings.refreshRate);
|
||||
displayFrameInterval(effectiveRefreshRate(
|
||||
appliedSettings.refreshRate,
|
||||
appliedSettings.terminalCompatibility));
|
||||
}
|
||||
std::this_thread::sleep_for(kCapturePollInterval);
|
||||
}
|
||||
@@ -2458,8 +2563,15 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
||||
}
|
||||
});
|
||||
|
||||
FrameRateMeter frameRateMeter;
|
||||
auto renderer = Renderer([&]() {
|
||||
renderTheme = &interfaceState.theme;
|
||||
renderTerminalCompatibility =
|
||||
interfaceState.settings.terminalCompatibility;
|
||||
const double renderedAt = std::chrono::duration<double>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
interfaceState.renderedFramesPerSecond =
|
||||
frameRateMeter.record(renderedAt);
|
||||
return renderFrame(
|
||||
frameStore.read(), screen.dimx(), screen.dimy(), interfaceState) |
|
||||
color(terminalColor(interfaceState.theme.text)) |
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace {
|
||||
|
||||
const std::vector<SettingDescriptor> kAppearanceSettings = {
|
||||
{SettingId::Theme, "Theme", "Uses Prism .iro files from the shared Prism Themes folder."},
|
||||
{SettingId::TerminalCompatibility, "Terminal mode", "Modern uses truecolor; Compatible uses 256 colors at up to 60 FPS; Safe uses ANSI colors at 30 FPS."},
|
||||
};
|
||||
|
||||
const std::vector<SettingDescriptor> kGeneralSettings = {
|
||||
@@ -153,6 +154,15 @@ std::string serializeWaveformMode(WaveformMode mode) {
|
||||
return mode == WaveformMode::Stereo ? "stereo" : "mono";
|
||||
}
|
||||
|
||||
std::string serializeTerminalCompatibility(TerminalCompatibilityMode mode) {
|
||||
switch (mode) {
|
||||
case TerminalCompatibilityMode::Modern: return "modern";
|
||||
case TerminalCompatibilityMode::Compatible: return "compatible";
|
||||
case TerminalCompatibilityMode::Safe: return "safe";
|
||||
}
|
||||
return "modern";
|
||||
}
|
||||
|
||||
bool parseBool(const std::string& value, bool fallback) {
|
||||
if (value == "true" || value == "1" || value == "on") return true;
|
||||
if (value == "false" || value == "0" || value == "off") return false;
|
||||
@@ -259,6 +269,15 @@ WaveformMode parseWaveformMode(const std::string& value, WaveformMode fallback)
|
||||
return fallback;
|
||||
}
|
||||
|
||||
TerminalCompatibilityMode parseTerminalCompatibility(
|
||||
const std::string& value,
|
||||
TerminalCompatibilityMode fallback) {
|
||||
if (value == "modern") return TerminalCompatibilityMode::Modern;
|
||||
if (value == "compatible") return TerminalCompatibilityMode::Compatible;
|
||||
if (value == "safe") return TerminalCompatibilityMode::Safe;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const char* environmentValue(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
return value != nullptr && value[0] != '\0' ? value : nullptr;
|
||||
@@ -298,6 +317,30 @@ const char* waveformModeName(WaveformMode mode) {
|
||||
return mode == WaveformMode::Stereo ? "Stereo" : "Mono";
|
||||
}
|
||||
|
||||
const char* terminalCompatibilityName(TerminalCompatibilityMode mode) {
|
||||
switch (mode) {
|
||||
case TerminalCompatibilityMode::Modern: return "Modern";
|
||||
case TerminalCompatibilityMode::Compatible: return "Compatible";
|
||||
case TerminalCompatibilityMode::Safe: return "Safe";
|
||||
}
|
||||
return "Modern";
|
||||
}
|
||||
|
||||
int effectiveRefreshRate(int requestedRate, TerminalCompatibilityMode mode) {
|
||||
const int normalized = requestedRate <= 30
|
||||
? 30
|
||||
: requestedRate <= 60 ? 60 : 120;
|
||||
switch (mode) {
|
||||
case TerminalCompatibilityMode::Modern:
|
||||
return normalized;
|
||||
case TerminalCompatibilityMode::Compatible:
|
||||
return std::min(normalized, 60);
|
||||
case TerminalCompatibilityMode::Safe:
|
||||
return 30;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
TuiSettings normalizeSettings(TuiSettings settings) {
|
||||
settings.themeId.erase(std::remove_if(
|
||||
settings.themeId.begin(), settings.themeId.end(), [](char character) {
|
||||
@@ -329,6 +372,7 @@ TuiSettings normalizeSettings(TuiSettings settings) {
|
||||
|
||||
bool operator==(const TuiSettings& left, const TuiSettings& right) {
|
||||
return left.themeId == right.themeId &&
|
||||
left.terminalCompatibility == right.terminalCompatibility &&
|
||||
left.inputTrimDb == right.inputTrimDb &&
|
||||
left.refreshRate == right.refreshRate &&
|
||||
left.rackLayout == right.rackLayout &&
|
||||
@@ -428,6 +472,8 @@ std::string settingValue(const TuiSettings& settings, SettingId setting) {
|
||||
switch (setting) {
|
||||
case SettingId::Theme:
|
||||
return settings.themeId;
|
||||
case SettingId::TerminalCompatibility:
|
||||
return terminalCompatibilityName(settings.terminalCompatibility);
|
||||
case SettingId::InputTrim:
|
||||
return (settings.inputTrimDb > 0.0f ? "+" : "") +
|
||||
trimFloat(settings.inputTrimDb, 1) + " dB";
|
||||
@@ -502,6 +548,13 @@ bool adjustSetting(TuiSettings& settings, SettingId setting, int direction) {
|
||||
switch (setting) {
|
||||
case SettingId::Theme:
|
||||
return false;
|
||||
case SettingId::TerminalCompatibility: {
|
||||
int value = static_cast<int>(settings.terminalCompatibility);
|
||||
value = (value + (direction > 0 ? 1 : 2)) % 3;
|
||||
settings.terminalCompatibility =
|
||||
static_cast<TerminalCompatibilityMode>(value);
|
||||
break;
|
||||
}
|
||||
case SettingId::InputTrim:
|
||||
settings.inputTrimDb += direction > 0 ? 0.5f : -0.5f;
|
||||
break;
|
||||
@@ -634,6 +687,7 @@ bool resetSetting(TuiSettings& settings, SettingId setting) {
|
||||
const TuiSettings before = settings;
|
||||
switch (setting) {
|
||||
case SettingId::Theme: settings.themeId = defaults.themeId; break;
|
||||
case SettingId::TerminalCompatibility: settings.terminalCompatibility = defaults.terminalCompatibility; break;
|
||||
case SettingId::InputTrim: settings.inputTrimDb = defaults.inputTrimDb; break;
|
||||
case SettingId::RefreshRate: settings.refreshRate = defaults.refreshRate; break;
|
||||
case SettingId::SpectrumPeakReadout: settings.spectrumPeakReadout = defaults.spectrumPeakReadout; break;
|
||||
@@ -704,6 +758,7 @@ TuiSettings parseSettingsText(const std::string& text,
|
||||
const std::string key = line.substr(0, separator);
|
||||
const std::string value = line.substr(separator + 1);
|
||||
if (key == "theme_id") settings.themeId = value;
|
||||
else if (key == "terminal_mode") settings.terminalCompatibility = parseTerminalCompatibility(value, settings.terminalCompatibility);
|
||||
else if (key == "input_trim_db") settings.inputTrimDb = parseFloat(value, settings.inputTrimDb);
|
||||
else if (key == "refresh_rate") settings.refreshRate = parseInt(value, settings.refreshRate);
|
||||
else if (key == "rack_layout") settings.rackLayout = parseRackLayout(value, settings.rackLayout);
|
||||
@@ -738,12 +793,14 @@ std::string serializeSettingsText(const TuiSettings& rawSettings,
|
||||
bool includeRefreshRate) {
|
||||
const TuiSettings settings = normalizeSettings(rawSettings);
|
||||
std::ostringstream output;
|
||||
output << "theme_id=" << settings.themeId << '\n'
|
||||
<< "input_trim_db=" << settings.inputTrimDb << '\n';
|
||||
output << "theme_id=" << settings.themeId << '\n';
|
||||
if (includeRefreshRate) {
|
||||
output << "refresh_rate=" << settings.refreshRate << '\n';
|
||||
output << "terminal_mode="
|
||||
<< serializeTerminalCompatibility(settings.terminalCompatibility) << '\n'
|
||||
<< "refresh_rate=" << settings.refreshRate << '\n';
|
||||
}
|
||||
output << "rack_layout=" << serializeRackLayout(settings.rackLayout) << '\n'
|
||||
output << "input_trim_db=" << settings.inputTrimDb << '\n'
|
||||
<< "rack_layout=" << serializeRackLayout(settings.rackLayout) << '\n'
|
||||
<< "spectrum_peak=" << (settings.spectrumPeakReadout ? "true" : "false") << '\n'
|
||||
<< "spectrum_tilt=" << settings.spectrumTiltDbPerOctave << '\n'
|
||||
<< "osc_pitch_lock=" << (settings.oscilloscopePitchLock ? "true" : "false") << '\n'
|
||||
|
||||
@@ -25,6 +25,7 @@ enum class SettingsPage {
|
||||
|
||||
enum class SettingId {
|
||||
Theme,
|
||||
TerminalCompatibility,
|
||||
InputTrim,
|
||||
RefreshRate,
|
||||
SpectrumPeakReadout,
|
||||
@@ -63,9 +64,12 @@ enum class SpectrogramClarity { Classic, Sharp, Sharper };
|
||||
enum class SpectrogramScale { Mel, Logarithmic, Linear };
|
||||
enum class SpectrogramOrientation { Horizontal, Vertical };
|
||||
enum class WaveformMode { Mono, Stereo };
|
||||
enum class TerminalCompatibilityMode { Modern, Compatible, Safe };
|
||||
|
||||
struct TuiSettings {
|
||||
std::string themeId = "Default";
|
||||
TerminalCompatibilityMode terminalCompatibility =
|
||||
TerminalCompatibilityMode::Modern;
|
||||
float inputTrimDb = 0.0f;
|
||||
int refreshRate = 60;
|
||||
RackLayout rackLayout = defaultRackLayout();
|
||||
@@ -119,6 +123,8 @@ const char* spectrogramClarityName(SpectrogramClarity clarity);
|
||||
const char* spectrogramScaleName(SpectrogramScale scale);
|
||||
const char* spectrogramOrientationName(SpectrogramOrientation orientation);
|
||||
const char* waveformModeName(WaveformMode mode);
|
||||
const char* terminalCompatibilityName(TerminalCompatibilityMode mode);
|
||||
int effectiveRefreshRate(int requestedRate, TerminalCompatibilityMode mode);
|
||||
|
||||
std::filesystem::path defaultSettingsPath();
|
||||
TuiSettings parseSettingsText(const std::string& text,
|
||||
|
||||
+54
-6
@@ -2,6 +2,7 @@
|
||||
#include "cli.h"
|
||||
#include "dashboard_layout.h"
|
||||
#include "display_model.h"
|
||||
#include "frame_rate_meter.h"
|
||||
#include "meter_display_model.h"
|
||||
#include "output_selection.h"
|
||||
#include "profile_library.h"
|
||||
@@ -549,10 +550,29 @@ void testSettingsModelAndPersistence() {
|
||||
|
||||
const auto pages = Prism::Tui::settingsPages();
|
||||
require(pages.size() == 9 &&
|
||||
Prism::Tui::settingsForPage(Prism::Tui::SettingsPage::Appearance).size() == 1 &&
|
||||
Prism::Tui::settingsForPage(Prism::Tui::SettingsPage::Appearance).size() == 2 &&
|
||||
Prism::Tui::settingsForPage(Prism::Tui::SettingsPage::General).size() == 2,
|
||||
"settings should expose shallow category pages");
|
||||
Prism::Tui::TuiSettings adjusted;
|
||||
require(Prism::Tui::adjustSetting(
|
||||
adjusted, Prism::Tui::SettingId::TerminalCompatibility, 1) &&
|
||||
adjusted.terminalCompatibility ==
|
||||
Prism::Tui::TerminalCompatibilityMode::Compatible &&
|
||||
Prism::Tui::effectiveRefreshRate(
|
||||
120, adjusted.terminalCompatibility) == 60,
|
||||
"compatible terminals should use the 256-color mode with a 60 FPS cap");
|
||||
require(Prism::Tui::adjustSetting(
|
||||
adjusted, Prism::Tui::SettingId::TerminalCompatibility, 1) &&
|
||||
adjusted.terminalCompatibility ==
|
||||
Prism::Tui::TerminalCompatibilityMode::Safe &&
|
||||
Prism::Tui::effectiveRefreshRate(
|
||||
120, adjusted.terminalCompatibility) == 30,
|
||||
"safe terminals should use the ANSI mode with a 30 FPS cap");
|
||||
require(Prism::Tui::adjustSetting(
|
||||
adjusted, Prism::Tui::SettingId::TerminalCompatibility, 1) &&
|
||||
adjusted.terminalCompatibility ==
|
||||
Prism::Tui::TerminalCompatibilityMode::Modern,
|
||||
"terminal compatibility should cycle back to modern truecolor");
|
||||
require(Prism::Tui::adjustSetting(
|
||||
adjusted, Prism::Tui::SettingId::RefreshRate, 1) &&
|
||||
adjusted.refreshRate == 120 &&
|
||||
@@ -623,6 +643,8 @@ void testSettingsModelAndPersistence() {
|
||||
adjusted.vectorscopeMode = Prism::Tui::VectorscopeMode::PolarBipolar;
|
||||
adjusted.vectorscopeDetail = Prism::Tui::VectorscopeDetail::Maximum;
|
||||
adjusted.themeId = "Test Theme";
|
||||
adjusted.terminalCompatibility =
|
||||
Prism::Tui::TerminalCompatibilityMode::Compatible;
|
||||
std::string error;
|
||||
require(Prism::Tui::saveSettings(adjusted, settingsPath, &error),
|
||||
"settings should persist to a TUI-specific configuration file");
|
||||
@@ -650,6 +672,8 @@ void testProfileLibrary() {
|
||||
|
||||
Prism::Tui::TuiSettings settings;
|
||||
settings.themeId = "Test Theme";
|
||||
settings.terminalCompatibility =
|
||||
Prism::Tui::TerminalCompatibilityMode::Safe;
|
||||
settings.inputTrimDb = 4.0f;
|
||||
settings.refreshRate = 30;
|
||||
settings.spectrogramContrast = 1.7f;
|
||||
@@ -676,20 +700,26 @@ void testProfileLibrary() {
|
||||
foundProfileFile = true;
|
||||
require(text.find("format=prism-tui-profile") != std::string::npos &&
|
||||
text.find("theme_id=Test Theme") != std::string::npos &&
|
||||
text.find("refresh_rate=") == std::string::npos,
|
||||
".prsmt files should retain themes while excluding global refresh settings");
|
||||
text.find("refresh_rate=") == std::string::npos &&
|
||||
text.find("terminal_mode=") == std::string::npos,
|
||||
".prsmt files should retain themes while excluding terminal runtime settings");
|
||||
}
|
||||
require(foundProfileFile,
|
||||
"saved TUI profiles should use the .prsmt extension");
|
||||
|
||||
Prism::Tui::TuiSettings differentRefresh = settings;
|
||||
differentRefresh.refreshRate = 60;
|
||||
differentRefresh.terminalCompatibility =
|
||||
Prism::Tui::TerminalCompatibilityMode::Modern;
|
||||
require(Prism::Tui::profileSettingsEqual(settings, differentRefresh),
|
||||
"refresh rate should not make an active profile dirty");
|
||||
"terminal runtime settings should not make an active profile dirty");
|
||||
const auto applied = Prism::Tui::applyProfileSettings(
|
||||
settings, differentRefresh);
|
||||
require(applied.refreshRate == 60 && applied.inputTrimDb == 4.0f,
|
||||
"loading a profile should preserve global refresh rate while applying trim");
|
||||
require(applied.refreshRate == 60 &&
|
||||
applied.terminalCompatibility ==
|
||||
Prism::Tui::TerminalCompatibilityMode::Modern &&
|
||||
applied.inputTrimDb == 4.0f,
|
||||
"loading a profile should preserve terminal runtime settings while applying trim");
|
||||
|
||||
settings.waveformMultiband = true;
|
||||
require(library.overwrite(profileId, settings, &error) &&
|
||||
@@ -1032,6 +1062,23 @@ void testThreadSafeSnapshots() {
|
||||
"immutable display snapshots should publish safely across threads");
|
||||
}
|
||||
|
||||
void testFrameRateMeter() {
|
||||
Prism::Tui::FrameRateMeter meter;
|
||||
require(meter.record(0.0) == 0.0 && meter.record(0.01) == 0.0,
|
||||
"render telemetry should wait for a stable sample window");
|
||||
meter.reset();
|
||||
for (int frame = 0; frame <= 60; ++frame) {
|
||||
meter.record(static_cast<double>(frame) / 60.0);
|
||||
}
|
||||
require(std::abs(meter.framesPerSecond() - 60.0) < 0.1,
|
||||
"render telemetry should measure a deterministic 60 FPS cadence");
|
||||
meter.reset();
|
||||
require(meter.framesPerSecond() == 0.0 && meter.record(5.0) == 0.0,
|
||||
"render telemetry should reset without inventing an initial frame rate");
|
||||
require(meter.record(4.0) == 0.0,
|
||||
"render telemetry should recover safely from a regressing clock");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
@@ -1047,6 +1094,7 @@ int main() {
|
||||
testPitchReadoutResponse();
|
||||
testScrollingHistory();
|
||||
testPipelineAndFakeCapture();
|
||||
testFrameRateMeter();
|
||||
testThreadSafeSnapshots();
|
||||
std::cout << "Prism TUI tests passed\n";
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user