mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-19 20:09:43 +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/cli.cpp
|
||||||
src/dashboard_layout.cpp
|
src/dashboard_layout.cpp
|
||||||
src/display_model.cpp
|
src/display_model.cpp
|
||||||
|
src/frame_rate_meter.cpp
|
||||||
src/meter_display_model.cpp
|
src/meter_display_model.cpp
|
||||||
src/output_selection.cpp
|
src/output_selection.cpp
|
||||||
src/profile_library.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 normalizedLeft = normalizeSettings(left);
|
||||||
TuiSettings normalizedRight = normalizeSettings(right);
|
TuiSettings normalizedRight = normalizeSettings(right);
|
||||||
normalizedLeft.refreshRate = normalizedRight.refreshRate;
|
normalizedLeft.refreshRate = normalizedRight.refreshRate;
|
||||||
|
normalizedLeft.terminalCompatibility =
|
||||||
|
normalizedRight.terminalCompatibility;
|
||||||
return normalizedLeft == normalizedRight;
|
return normalizedLeft == normalizedRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,6 +439,8 @@ TuiSettings applyProfileSettings(const TuiSettings& profile,
|
|||||||
const TuiSettings& working) {
|
const TuiSettings& working) {
|
||||||
TuiSettings applied = normalizeSettings(profile);
|
TuiSettings applied = normalizeSettings(profile);
|
||||||
applied.refreshRate = normalizeSettings(working).refreshRate;
|
applied.refreshRate = normalizeSettings(working).refreshRate;
|
||||||
|
applied.terminalCompatibility =
|
||||||
|
normalizeSettings(working).terminalCompatibility;
|
||||||
return applied;
|
return applied;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+118
-6
@@ -3,6 +3,7 @@
|
|||||||
#include "analysis_pipeline.h"
|
#include "analysis_pipeline.h"
|
||||||
#include "dashboard_layout.h"
|
#include "dashboard_layout.h"
|
||||||
#include "display_model.h"
|
#include "display_model.h"
|
||||||
|
#include "frame_rate_meter.h"
|
||||||
#include "meter_display_model.h"
|
#include "meter_display_model.h"
|
||||||
#include "output_selection.h"
|
#include "output_selection.h"
|
||||||
#include "profile_library.h"
|
#include "profile_library.h"
|
||||||
@@ -200,9 +201,12 @@ struct InterfaceState {
|
|||||||
uint64_t appliedOutputSwitchSerial = 0;
|
uint64_t appliedOutputSwitchSerial = 0;
|
||||||
uint64_t outputListSerial = 0;
|
uint64_t outputListSerial = 0;
|
||||||
uint64_t appliedOutputListSerial = 0;
|
uint64_t appliedOutputListSerial = 0;
|
||||||
|
double renderedFramesPerSecond = 0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TuiTheme* renderTheme = nullptr;
|
const TuiTheme* renderTheme = nullptr;
|
||||||
|
TerminalCompatibilityMode renderTerminalCompatibility =
|
||||||
|
TerminalCompatibilityMode::Modern;
|
||||||
|
|
||||||
const TuiTheme& palette() {
|
const TuiTheme& palette() {
|
||||||
static const TuiTheme fallback = defaultTuiTheme();
|
static const TuiTheme fallback = defaultTuiTheme();
|
||||||
@@ -210,7 +214,83 @@ const TuiTheme& palette() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ftxui::Color terminalColor(const ThemeColor& color) {
|
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,
|
void fillCanvasBackground(ftxui::Canvas& surface,
|
||||||
@@ -268,6 +348,7 @@ const size_t& settingsSelection(const InterfaceState& state) {
|
|||||||
|
|
||||||
std::string makeCaptureStatus(const DisplayFrame& frame,
|
std::string makeCaptureStatus(const DisplayFrame& frame,
|
||||||
const TuiSettings& settings,
|
const TuiSettings& settings,
|
||||||
|
double renderedFramesPerSecond,
|
||||||
bool compact) {
|
bool compact) {
|
||||||
std::ostringstream sampleRate;
|
std::ostringstream sampleRate;
|
||||||
const double kilohertz = frame.sampleRate / 1000.0;
|
const double kilohertz = frame.sampleRate / 1000.0;
|
||||||
@@ -287,6 +368,19 @@ std::string makeCaptureStatus(const DisplayFrame& frame,
|
|||||||
if (frame.captureOverrun) {
|
if (frame.captureOverrun) {
|
||||||
footer += " • capture overrun";
|
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;
|
return footer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1185,7 +1279,7 @@ ftxui::Color interpolateColor(const ThemeColor& from,
|
|||||||
const ThemeColor& to,
|
const ThemeColor& to,
|
||||||
float amount) {
|
float amount) {
|
||||||
const float t = std::clamp(amount, 0.0f, 1.0f);
|
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<uint8_t>(std::lround(from.red +
|
||||||
(static_cast<int>(to.red) - from.red) * t)),
|
(static_cast<int>(to.red) - from.red) * t)),
|
||||||
static_cast<uint8_t>(std::lround(from.green +
|
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)));
|
weights[band] * static_cast<float>(value)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ftxui::Color::RGB(
|
return terminalRgb(
|
||||||
std::clamp(mixed[0], 0, 255),
|
std::clamp(mixed[0], 0, 255),
|
||||||
std::clamp(mixed[1], 0, 255),
|
std::clamp(mixed[1], 0, 255),
|
||||||
std::clamp(mixed[2], 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"
|
" • o outputs • p profiles • s settings • q quit"
|
||||||
: "Tab focus • Enter " + enterAction +
|
: "Tab focus • Enter " + enterAction +
|
||||||
" • o outputs • p profiles • s settings • l edit layout • r reset • q quit";
|
" • 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) {
|
if (frame.captureOverrun) {
|
||||||
status = status | color(terminalColor(palette().danger));
|
status = status | color(terminalColor(palette().danger));
|
||||||
}
|
}
|
||||||
@@ -2395,7 +2493,12 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
|||||||
const TuiSettings requestedSettings = settingsStore.read();
|
const TuiSettings requestedSettings = settingsStore.read();
|
||||||
if (requestedSettings != appliedSettings) {
|
if (requestedSettings != appliedSettings) {
|
||||||
const bool refreshChanged =
|
const bool refreshChanged =
|
||||||
requestedSettings.refreshRate != appliedSettings.refreshRate;
|
effectiveRefreshRate(
|
||||||
|
requestedSettings.refreshRate,
|
||||||
|
requestedSettings.terminalCompatibility) !=
|
||||||
|
effectiveRefreshRate(
|
||||||
|
appliedSettings.refreshRate,
|
||||||
|
appliedSettings.terminalCompatibility);
|
||||||
pipeline->setInputTrimDb(requestedSettings.inputTrimDb);
|
pipeline->setInputTrimDb(requestedSettings.inputTrimDb);
|
||||||
pipeline->setSpectrumTilt(
|
pipeline->setSpectrumTilt(
|
||||||
requestedSettings.spectrumTiltDbPerOctave);
|
requestedSettings.spectrumTiltDbPerOctave);
|
||||||
@@ -2446,7 +2549,9 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
|||||||
screen.PostEvent(Event::Custom);
|
screen.PostEvent(Event::Custom);
|
||||||
}
|
}
|
||||||
nextFrameAt = now +
|
nextFrameAt = now +
|
||||||
displayFrameInterval(appliedSettings.refreshRate);
|
displayFrameInterval(effectiveRefreshRate(
|
||||||
|
appliedSettings.refreshRate,
|
||||||
|
appliedSettings.terminalCompatibility));
|
||||||
}
|
}
|
||||||
std::this_thread::sleep_for(kCapturePollInterval);
|
std::this_thread::sleep_for(kCapturePollInterval);
|
||||||
}
|
}
|
||||||
@@ -2458,8 +2563,15 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
FrameRateMeter frameRateMeter;
|
||||||
auto renderer = Renderer([&]() {
|
auto renderer = Renderer([&]() {
|
||||||
renderTheme = &interfaceState.theme;
|
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(
|
return renderFrame(
|
||||||
frameStore.read(), screen.dimx(), screen.dimy(), interfaceState) |
|
frameStore.read(), screen.dimx(), screen.dimy(), interfaceState) |
|
||||||
color(terminalColor(interfaceState.theme.text)) |
|
color(terminalColor(interfaceState.theme.text)) |
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace {
|
|||||||
|
|
||||||
const std::vector<SettingDescriptor> kAppearanceSettings = {
|
const std::vector<SettingDescriptor> kAppearanceSettings = {
|
||||||
{SettingId::Theme, "Theme", "Uses Prism .iro files from the shared Prism Themes folder."},
|
{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 = {
|
const std::vector<SettingDescriptor> kGeneralSettings = {
|
||||||
@@ -153,6 +154,15 @@ std::string serializeWaveformMode(WaveformMode mode) {
|
|||||||
return mode == WaveformMode::Stereo ? "stereo" : "mono";
|
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) {
|
bool parseBool(const std::string& value, bool fallback) {
|
||||||
if (value == "true" || value == "1" || value == "on") return true;
|
if (value == "true" || value == "1" || value == "on") return true;
|
||||||
if (value == "false" || value == "0" || value == "off") return false;
|
if (value == "false" || value == "0" || value == "off") return false;
|
||||||
@@ -259,6 +269,15 @@ WaveformMode parseWaveformMode(const std::string& value, WaveformMode fallback)
|
|||||||
return 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* environmentValue(const char* name) {
|
||||||
const char* value = std::getenv(name);
|
const char* value = std::getenv(name);
|
||||||
return value != nullptr && value[0] != '\0' ? value : nullptr;
|
return value != nullptr && value[0] != '\0' ? value : nullptr;
|
||||||
@@ -298,6 +317,30 @@ const char* waveformModeName(WaveformMode mode) {
|
|||||||
return mode == WaveformMode::Stereo ? "Stereo" : "Mono";
|
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) {
|
TuiSettings normalizeSettings(TuiSettings settings) {
|
||||||
settings.themeId.erase(std::remove_if(
|
settings.themeId.erase(std::remove_if(
|
||||||
settings.themeId.begin(), settings.themeId.end(), [](char character) {
|
settings.themeId.begin(), settings.themeId.end(), [](char character) {
|
||||||
@@ -329,6 +372,7 @@ TuiSettings normalizeSettings(TuiSettings settings) {
|
|||||||
|
|
||||||
bool operator==(const TuiSettings& left, const TuiSettings& right) {
|
bool operator==(const TuiSettings& left, const TuiSettings& right) {
|
||||||
return left.themeId == right.themeId &&
|
return left.themeId == right.themeId &&
|
||||||
|
left.terminalCompatibility == right.terminalCompatibility &&
|
||||||
left.inputTrimDb == right.inputTrimDb &&
|
left.inputTrimDb == right.inputTrimDb &&
|
||||||
left.refreshRate == right.refreshRate &&
|
left.refreshRate == right.refreshRate &&
|
||||||
left.rackLayout == right.rackLayout &&
|
left.rackLayout == right.rackLayout &&
|
||||||
@@ -428,6 +472,8 @@ std::string settingValue(const TuiSettings& settings, SettingId setting) {
|
|||||||
switch (setting) {
|
switch (setting) {
|
||||||
case SettingId::Theme:
|
case SettingId::Theme:
|
||||||
return settings.themeId;
|
return settings.themeId;
|
||||||
|
case SettingId::TerminalCompatibility:
|
||||||
|
return terminalCompatibilityName(settings.terminalCompatibility);
|
||||||
case SettingId::InputTrim:
|
case SettingId::InputTrim:
|
||||||
return (settings.inputTrimDb > 0.0f ? "+" : "") +
|
return (settings.inputTrimDb > 0.0f ? "+" : "") +
|
||||||
trimFloat(settings.inputTrimDb, 1) + " dB";
|
trimFloat(settings.inputTrimDb, 1) + " dB";
|
||||||
@@ -502,6 +548,13 @@ bool adjustSetting(TuiSettings& settings, SettingId setting, int direction) {
|
|||||||
switch (setting) {
|
switch (setting) {
|
||||||
case SettingId::Theme:
|
case SettingId::Theme:
|
||||||
return false;
|
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:
|
case SettingId::InputTrim:
|
||||||
settings.inputTrimDb += direction > 0 ? 0.5f : -0.5f;
|
settings.inputTrimDb += direction > 0 ? 0.5f : -0.5f;
|
||||||
break;
|
break;
|
||||||
@@ -634,6 +687,7 @@ bool resetSetting(TuiSettings& settings, SettingId setting) {
|
|||||||
const TuiSettings before = settings;
|
const TuiSettings before = settings;
|
||||||
switch (setting) {
|
switch (setting) {
|
||||||
case SettingId::Theme: settings.themeId = defaults.themeId; break;
|
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::InputTrim: settings.inputTrimDb = defaults.inputTrimDb; break;
|
||||||
case SettingId::RefreshRate: settings.refreshRate = defaults.refreshRate; break;
|
case SettingId::RefreshRate: settings.refreshRate = defaults.refreshRate; break;
|
||||||
case SettingId::SpectrumPeakReadout: settings.spectrumPeakReadout = defaults.spectrumPeakReadout; 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 key = line.substr(0, separator);
|
||||||
const std::string value = line.substr(separator + 1);
|
const std::string value = line.substr(separator + 1);
|
||||||
if (key == "theme_id") settings.themeId = value;
|
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 == "input_trim_db") settings.inputTrimDb = parseFloat(value, settings.inputTrimDb);
|
||||||
else if (key == "refresh_rate") settings.refreshRate = parseInt(value, settings.refreshRate);
|
else if (key == "refresh_rate") settings.refreshRate = parseInt(value, settings.refreshRate);
|
||||||
else if (key == "rack_layout") settings.rackLayout = parseRackLayout(value, settings.rackLayout);
|
else if (key == "rack_layout") settings.rackLayout = parseRackLayout(value, settings.rackLayout);
|
||||||
@@ -738,12 +793,14 @@ std::string serializeSettingsText(const TuiSettings& rawSettings,
|
|||||||
bool includeRefreshRate) {
|
bool includeRefreshRate) {
|
||||||
const TuiSettings settings = normalizeSettings(rawSettings);
|
const TuiSettings settings = normalizeSettings(rawSettings);
|
||||||
std::ostringstream output;
|
std::ostringstream output;
|
||||||
output << "theme_id=" << settings.themeId << '\n'
|
output << "theme_id=" << settings.themeId << '\n';
|
||||||
<< "input_trim_db=" << settings.inputTrimDb << '\n';
|
|
||||||
if (includeRefreshRate) {
|
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_peak=" << (settings.spectrumPeakReadout ? "true" : "false") << '\n'
|
||||||
<< "spectrum_tilt=" << settings.spectrumTiltDbPerOctave << '\n'
|
<< "spectrum_tilt=" << settings.spectrumTiltDbPerOctave << '\n'
|
||||||
<< "osc_pitch_lock=" << (settings.oscilloscopePitchLock ? "true" : "false") << '\n'
|
<< "osc_pitch_lock=" << (settings.oscilloscopePitchLock ? "true" : "false") << '\n'
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ enum class SettingsPage {
|
|||||||
|
|
||||||
enum class SettingId {
|
enum class SettingId {
|
||||||
Theme,
|
Theme,
|
||||||
|
TerminalCompatibility,
|
||||||
InputTrim,
|
InputTrim,
|
||||||
RefreshRate,
|
RefreshRate,
|
||||||
SpectrumPeakReadout,
|
SpectrumPeakReadout,
|
||||||
@@ -63,9 +64,12 @@ enum class SpectrogramClarity { Classic, Sharp, Sharper };
|
|||||||
enum class SpectrogramScale { Mel, Logarithmic, Linear };
|
enum class SpectrogramScale { Mel, Logarithmic, Linear };
|
||||||
enum class SpectrogramOrientation { Horizontal, Vertical };
|
enum class SpectrogramOrientation { Horizontal, Vertical };
|
||||||
enum class WaveformMode { Mono, Stereo };
|
enum class WaveformMode { Mono, Stereo };
|
||||||
|
enum class TerminalCompatibilityMode { Modern, Compatible, Safe };
|
||||||
|
|
||||||
struct TuiSettings {
|
struct TuiSettings {
|
||||||
std::string themeId = "Default";
|
std::string themeId = "Default";
|
||||||
|
TerminalCompatibilityMode terminalCompatibility =
|
||||||
|
TerminalCompatibilityMode::Modern;
|
||||||
float inputTrimDb = 0.0f;
|
float inputTrimDb = 0.0f;
|
||||||
int refreshRate = 60;
|
int refreshRate = 60;
|
||||||
RackLayout rackLayout = defaultRackLayout();
|
RackLayout rackLayout = defaultRackLayout();
|
||||||
@@ -119,6 +123,8 @@ const char* spectrogramClarityName(SpectrogramClarity clarity);
|
|||||||
const char* spectrogramScaleName(SpectrogramScale scale);
|
const char* spectrogramScaleName(SpectrogramScale scale);
|
||||||
const char* spectrogramOrientationName(SpectrogramOrientation orientation);
|
const char* spectrogramOrientationName(SpectrogramOrientation orientation);
|
||||||
const char* waveformModeName(WaveformMode mode);
|
const char* waveformModeName(WaveformMode mode);
|
||||||
|
const char* terminalCompatibilityName(TerminalCompatibilityMode mode);
|
||||||
|
int effectiveRefreshRate(int requestedRate, TerminalCompatibilityMode mode);
|
||||||
|
|
||||||
std::filesystem::path defaultSettingsPath();
|
std::filesystem::path defaultSettingsPath();
|
||||||
TuiSettings parseSettingsText(const std::string& text,
|
TuiSettings parseSettingsText(const std::string& text,
|
||||||
|
|||||||
+54
-6
@@ -2,6 +2,7 @@
|
|||||||
#include "cli.h"
|
#include "cli.h"
|
||||||
#include "dashboard_layout.h"
|
#include "dashboard_layout.h"
|
||||||
#include "display_model.h"
|
#include "display_model.h"
|
||||||
|
#include "frame_rate_meter.h"
|
||||||
#include "meter_display_model.h"
|
#include "meter_display_model.h"
|
||||||
#include "output_selection.h"
|
#include "output_selection.h"
|
||||||
#include "profile_library.h"
|
#include "profile_library.h"
|
||||||
@@ -549,10 +550,29 @@ void testSettingsModelAndPersistence() {
|
|||||||
|
|
||||||
const auto pages = Prism::Tui::settingsPages();
|
const auto pages = Prism::Tui::settingsPages();
|
||||||
require(pages.size() == 9 &&
|
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,
|
Prism::Tui::settingsForPage(Prism::Tui::SettingsPage::General).size() == 2,
|
||||||
"settings should expose shallow category pages");
|
"settings should expose shallow category pages");
|
||||||
Prism::Tui::TuiSettings adjusted;
|
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(
|
require(Prism::Tui::adjustSetting(
|
||||||
adjusted, Prism::Tui::SettingId::RefreshRate, 1) &&
|
adjusted, Prism::Tui::SettingId::RefreshRate, 1) &&
|
||||||
adjusted.refreshRate == 120 &&
|
adjusted.refreshRate == 120 &&
|
||||||
@@ -623,6 +643,8 @@ void testSettingsModelAndPersistence() {
|
|||||||
adjusted.vectorscopeMode = Prism::Tui::VectorscopeMode::PolarBipolar;
|
adjusted.vectorscopeMode = Prism::Tui::VectorscopeMode::PolarBipolar;
|
||||||
adjusted.vectorscopeDetail = Prism::Tui::VectorscopeDetail::Maximum;
|
adjusted.vectorscopeDetail = Prism::Tui::VectorscopeDetail::Maximum;
|
||||||
adjusted.themeId = "Test Theme";
|
adjusted.themeId = "Test Theme";
|
||||||
|
adjusted.terminalCompatibility =
|
||||||
|
Prism::Tui::TerminalCompatibilityMode::Compatible;
|
||||||
std::string error;
|
std::string error;
|
||||||
require(Prism::Tui::saveSettings(adjusted, settingsPath, &error),
|
require(Prism::Tui::saveSettings(adjusted, settingsPath, &error),
|
||||||
"settings should persist to a TUI-specific configuration file");
|
"settings should persist to a TUI-specific configuration file");
|
||||||
@@ -650,6 +672,8 @@ void testProfileLibrary() {
|
|||||||
|
|
||||||
Prism::Tui::TuiSettings settings;
|
Prism::Tui::TuiSettings settings;
|
||||||
settings.themeId = "Test Theme";
|
settings.themeId = "Test Theme";
|
||||||
|
settings.terminalCompatibility =
|
||||||
|
Prism::Tui::TerminalCompatibilityMode::Safe;
|
||||||
settings.inputTrimDb = 4.0f;
|
settings.inputTrimDb = 4.0f;
|
||||||
settings.refreshRate = 30;
|
settings.refreshRate = 30;
|
||||||
settings.spectrogramContrast = 1.7f;
|
settings.spectrogramContrast = 1.7f;
|
||||||
@@ -676,20 +700,26 @@ void testProfileLibrary() {
|
|||||||
foundProfileFile = true;
|
foundProfileFile = true;
|
||||||
require(text.find("format=prism-tui-profile") != std::string::npos &&
|
require(text.find("format=prism-tui-profile") != std::string::npos &&
|
||||||
text.find("theme_id=Test Theme") != std::string::npos &&
|
text.find("theme_id=Test Theme") != std::string::npos &&
|
||||||
text.find("refresh_rate=") == std::string::npos,
|
text.find("refresh_rate=") == std::string::npos &&
|
||||||
".prsmt files should retain themes while excluding global refresh settings");
|
text.find("terminal_mode=") == std::string::npos,
|
||||||
|
".prsmt files should retain themes while excluding terminal runtime settings");
|
||||||
}
|
}
|
||||||
require(foundProfileFile,
|
require(foundProfileFile,
|
||||||
"saved TUI profiles should use the .prsmt extension");
|
"saved TUI profiles should use the .prsmt extension");
|
||||||
|
|
||||||
Prism::Tui::TuiSettings differentRefresh = settings;
|
Prism::Tui::TuiSettings differentRefresh = settings;
|
||||||
differentRefresh.refreshRate = 60;
|
differentRefresh.refreshRate = 60;
|
||||||
|
differentRefresh.terminalCompatibility =
|
||||||
|
Prism::Tui::TerminalCompatibilityMode::Modern;
|
||||||
require(Prism::Tui::profileSettingsEqual(settings, differentRefresh),
|
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(
|
const auto applied = Prism::Tui::applyProfileSettings(
|
||||||
settings, differentRefresh);
|
settings, differentRefresh);
|
||||||
require(applied.refreshRate == 60 && applied.inputTrimDb == 4.0f,
|
require(applied.refreshRate == 60 &&
|
||||||
"loading a profile should preserve global refresh rate while applying trim");
|
applied.terminalCompatibility ==
|
||||||
|
Prism::Tui::TerminalCompatibilityMode::Modern &&
|
||||||
|
applied.inputTrimDb == 4.0f,
|
||||||
|
"loading a profile should preserve terminal runtime settings while applying trim");
|
||||||
|
|
||||||
settings.waveformMultiband = true;
|
settings.waveformMultiband = true;
|
||||||
require(library.overwrite(profileId, settings, &error) &&
|
require(library.overwrite(profileId, settings, &error) &&
|
||||||
@@ -1032,6 +1062,23 @@ void testThreadSafeSnapshots() {
|
|||||||
"immutable display snapshots should publish safely across threads");
|
"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
|
} // namespace
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
@@ -1047,6 +1094,7 @@ int main() {
|
|||||||
testPitchReadoutResponse();
|
testPitchReadoutResponse();
|
||||||
testScrollingHistory();
|
testScrollingHistory();
|
||||||
testPipelineAndFakeCapture();
|
testPipelineAndFakeCapture();
|
||||||
|
testFrameRateMeter();
|
||||||
testThreadSafeSnapshots();
|
testThreadSafeSnapshots();
|
||||||
std::cout << "Prism TUI tests passed\n";
|
std::cout << "Prism TUI tests passed\n";
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user