feat: make everything threads-safe.
feat: complete `uninstall.sh` logic to make everything synced. feat: complete basic play logic to handle background, music, lyric play.
This commit is contained in:
+68
-26
@@ -121,9 +121,13 @@ bool isWide(int cp) {
|
||||
}
|
||||
|
||||
// Truncate `text` so it is at most `maxWidth` terminal columns wide, never
|
||||
// splitting a UTF-8 character in the middle.
|
||||
std::string truncateToWidth(const std::string& text, int maxWidth) {
|
||||
if (maxWidth <= 0) return std::string();
|
||||
// splitting a UTF-8 character in the middle. Optionally reports the display
|
||||
// width of the returned string.
|
||||
std::string truncateToWidth(const std::string& text, int maxWidth, int* outWidth) {
|
||||
if (maxWidth <= 0) {
|
||||
if (outWidth) *outWidth = 0;
|
||||
return std::string();
|
||||
}
|
||||
int width = 0;
|
||||
size_t i = 0;
|
||||
size_t lastBreak = 0;
|
||||
@@ -151,19 +155,39 @@ std::string truncateToWidth(const std::string& text, int maxWidth) {
|
||||
lastBreak = i + static_cast<size_t>(len);
|
||||
i += static_cast<size_t>(len);
|
||||
}
|
||||
if (outWidth) *outWidth = width;
|
||||
return text.substr(0, lastBreak);
|
||||
}
|
||||
|
||||
// Rewrite the current lyric on its row: clear the line, or write the text and
|
||||
// erase to the end of the line.
|
||||
void refreshLyricLine(int row, const std::string& text, int maxWidth) {
|
||||
// Rewrite the current lyric at (row, col). When `maxWidth` is given the area
|
||||
// is cleared within exactly those columns (ECH), so the lyric band never
|
||||
// extends past the caller's chosen width (e.g. the video frame). Otherwise the
|
||||
// whole line is cleared/written.
|
||||
void refreshLyricLine(int row, int col, const std::string& text, int maxWidth) {
|
||||
std::string out;
|
||||
out += ansiCursorTo(row, 1);
|
||||
if (text.empty()) {
|
||||
out += "\x1b[2K"; // erase entire line
|
||||
out += ansiCursorTo(row, col);
|
||||
if (maxWidth <= 0) {
|
||||
if (text.empty()) {
|
||||
out += "\x1b[2K"; // erase entire line
|
||||
} else {
|
||||
out += text;
|
||||
out += "\x1b[K"; // erase to end of line
|
||||
}
|
||||
} else {
|
||||
out += truncateToWidth(text, maxWidth);
|
||||
out += "\x1b[K"; // erase to end of line
|
||||
int width = 0;
|
||||
if (text.empty()) {
|
||||
out += "\x1b[";
|
||||
out += std::to_string(maxWidth);
|
||||
out += "X"; // erase the whole lyric band within the video width
|
||||
} else {
|
||||
out += truncateToWidth(text, maxWidth, &width);
|
||||
const int remain = maxWidth - width;
|
||||
if (remain > 0) {
|
||||
out += "\x1b[";
|
||||
out += std::to_string(remain);
|
||||
out += "X";
|
||||
}
|
||||
}
|
||||
}
|
||||
writeRaw(out);
|
||||
}
|
||||
@@ -241,34 +265,52 @@ LyricInfo parseLrcFile(const std::string& path) {
|
||||
}
|
||||
|
||||
int playLyrics(const LyricInfo& lyric, const LyricDisplayOptions& opts,
|
||||
std::atomic<bool>* stop) {
|
||||
Timeline* timeline, std::atomic<bool>* stop) {
|
||||
if (!lyric.valid) return 1;
|
||||
|
||||
Timeline localTimeline;
|
||||
if (!timeline) {
|
||||
timeline = &localTimeline;
|
||||
timeline->start();
|
||||
}
|
||||
|
||||
const TermSize ts = terminalSize();
|
||||
const int row = (opts.row != 0) ? opts.row : (ts.ok ? ts.rows : 1);
|
||||
const int maxWidth = ts.ok ? std::max(1, ts.cols - 1) : 120;
|
||||
const int col = (opts.col > 0) ? opts.col : 1;
|
||||
const int maxWidth = (opts.maxWidth > 0)
|
||||
? opts.maxWidth
|
||||
: (ts.ok ? std::max(1, ts.cols - 1) : 120);
|
||||
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
for (const LyricLine& line : lyric.lines) {
|
||||
if (stop && stop->load()) break;
|
||||
// Sleep in small steps so a stop request is honoured promptly even
|
||||
// when the next lyric line is far in the future.
|
||||
const auto target = start + std::chrono::milliseconds(line.timeMs);
|
||||
while (std::chrono::steady_clock::now() < target) {
|
||||
if (stop && stop->load()) break;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
if (lyric.lines.empty()) return 0;
|
||||
// Hold the last line on screen a little while after it ends.
|
||||
const double endTime = lyric.lines.back().timeMs / 1000.0 + 5.0;
|
||||
|
||||
int shown = -1; // index of the line currently on screen
|
||||
while (!(stop && stop->load())) {
|
||||
const double t = timeline->seconds();
|
||||
|
||||
// Active line = last line whose timestamp the timeline has reached.
|
||||
int idx = -1;
|
||||
for (size_t i = 0; i < lyric.lines.size(); ++i) {
|
||||
if (lyric.lines[i].timeMs / 1000.0 <= t) idx = static_cast<int>(i);
|
||||
else break;
|
||||
}
|
||||
if (stop && stop->load()) break;
|
||||
if (opts.show) refreshLyricLine(row, line.text, maxWidth);
|
||||
if (idx != shown) {
|
||||
shown = idx;
|
||||
const std::string& text = (idx >= 0) ? lyric.lines[idx].text : std::string();
|
||||
if (opts.show) refreshLyricLine(row, col, text, maxWidth);
|
||||
}
|
||||
if (t >= endTime) break;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(40));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int playLyricsFile(const std::string& path, const LyricDisplayOptions& opts,
|
||||
std::atomic<bool>* stop) {
|
||||
Timeline* timeline, std::atomic<bool>* stop) {
|
||||
const LyricInfo info = parseLrcFile(path);
|
||||
if (!info.valid) return 1;
|
||||
return playLyrics(info, opts, stop);
|
||||
return playLyrics(info, opts, timeline, stop);
|
||||
}
|
||||
|
||||
} // namespace lr
|
||||
|
||||
Reference in New Issue
Block a user