Skip to content

Commit e9b5097

Browse files
Merge pull request #1863 from contour-terminal/fix/sixel-on-non-first-tab
Fix Sixel images not rendering on non-first tabs
2 parents da936c8 + 86201e3 commit e9b5097

File tree

11 files changed

+174
-40
lines changed

11 files changed

+174
-40
lines changed

metainfo.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,10 @@
108108
<description>
109109
<ul>
110110
<li>Fixes glyph scaling and vertical centering of colored emoji</li>
111+
<li>Fixes Sixel images not rendering on non-first tabs</li>
111112
<li>Fixes text rendering artifacts and corruption when rapidly outputting content</li>
112113
<li>Fixes garbled or missing characters when displaying Unicode text</li>
114+
<li>Adds configurable cell blink animation style (classic/smooth/linger) via blink_style profile setting</li>
113115
</ul>
114116
</description>
115117
</release>

src/contour/Config.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ void YAMLConfigReader::loadFromEntry(YAML::Node const& node, std::string const&
421421
loadFromEntry(child, "highlight_word_and_matches_on_double_click", where.highlightDoubleClickedWord);
422422
loadFromEntry(child, "font", where.fonts);
423423
loadFromEntry(child, "draw_bold_text_with_bright_colors", where.drawBoldTextWithBrightColors);
424+
loadFromEntry(child, "blink_style", where.blinkStyle);
424425
if (child["cursor"])
425426
{
426427
loadFromEntry(child["cursor"], "shape", where.modeInsert.value().cursor.cursorShape);
@@ -2147,6 +2148,30 @@ void YAMLConfigReader::loadFromEntry(YAML::Node const& node,
21472148
}
21482149
}
21492150

2151+
void YAMLConfigReader::loadFromEntry(YAML::Node const& node,
2152+
std::string const& entry,
2153+
vtbackend::BlinkStyle& where)
2154+
{
2155+
auto parse = [&](std::string const& key) -> std::optional<vtbackend::BlinkStyle> {
2156+
auto const upperKey = crispy::toUpper(key);
2157+
logger()("Loading entry: {}, value {}", entry, upperKey);
2158+
if (upperKey == "CLASSIC")
2159+
return vtbackend::BlinkStyle::Classic;
2160+
if (upperKey == "SMOOTH")
2161+
return vtbackend::BlinkStyle::Smooth;
2162+
if (upperKey == "LINGER")
2163+
return vtbackend::BlinkStyle::Linger;
2164+
return std::nullopt;
2165+
};
2166+
2167+
if (auto const child = node[entry])
2168+
{
2169+
auto opt = parse(child.as<std::string>());
2170+
if (opt.has_value())
2171+
where = opt.value();
2172+
}
2173+
}
2174+
21502175
void YAMLConfigReader::loadFromEntry(YAML::Node const& node,
21512176
std::string const& entry,
21522177
crispy::lru_capacity& where)

src/contour/Config.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,9 @@ struct TerminalProfile
420420
ConfigEntry<bool, documentation::HighlightDoubleClickerWord> highlightDoubleClickedWord { true };
421421
ConfigEntry<vtrasterizer::FontDescriptions, documentation::Fonts> fonts { defaultFont };
422422
ConfigEntry<bool, documentation::DrawBoldTextWithBrightColors> drawBoldTextWithBrightColors { false };
423+
ConfigEntry<vtbackend::BlinkStyle, documentation::BlinkStyle> blinkStyle {
424+
vtbackend::BlinkStyle::Smooth
425+
};
423426
ConfigEntry<InputModeConfig, documentation::ModeInsert> modeInsert { CursorConfig {
424427
.cursorShape = vtbackend::CursorShape::Bar,
425428
.cursorDisplay = vtbackend::CursorDisplay::Steady,
@@ -963,6 +966,7 @@ struct YAMLConfigReader
963966
void loadFromEntry(YAML::Node const& node, std::string const& entry, vtbackend::MaxHistoryLineCount& where);
964967
void loadFromEntry(YAML::Node const& node, std::string const& entry, crispy::lru_capacity& where);
965968
void loadFromEntry(YAML::Node const& node, std::string const& entry, vtbackend::CursorDisplay& where);
969+
void loadFromEntry(YAML::Node const& node, std::string const& entry, vtbackend::BlinkStyle& where);
966970
void loadFromEntry(YAML::Node const& node, std::string const& entry, vtbackend::Modifiers& where);
967971
void loadFromEntry(YAML::Node const& node, std::string const& entry, vtbackend::CursorShape& where);
968972
void loadFromEntry(YAML::Node const& node, std::string const& entry, contour::config::SelectionAction& where);

src/contour/ConfigDocumentation.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1803,6 +1803,34 @@ using Fonts = DocumentationEntry<FontsConfig, FontsWeb>;
18031803
using Permissions = DocumentationEntry<PermissionsConfig, PermissionsWeb>;
18041804
using DrawBoldTextWithBrightColors =
18051805
DocumentationEntry<DrawBoldTextWithBrightColorsConfig, DrawBoldTextWithBrightColorsWeb>;
1806+
constexpr StringLiteral BlinkStyleConfig {
1807+
"{comment} Determines the visual style of cell blink animation (SGR 5/6).\n"
1808+
"{comment}\n"
1809+
"{comment} Valid values are:\n"
1810+
"{comment} - classic: Abrupt on/off toggle.\n"
1811+
"{comment} - smooth: Continuous cosine-based pulse.\n"
1812+
"{comment} - linger: Like smooth but stays visible longer.\n"
1813+
"blink_style: {}\n"
1814+
"\n"
1815+
};
1816+
1817+
constexpr StringLiteral BlinkStyleWeb {
1818+
"Determines the visual style of cell blink animation (SGR attributes 5 and 6).\n"
1819+
"\n"
1820+
"Valid values are:\n"
1821+
"- `classic` - abrupt on/off toggle\n"
1822+
"- `smooth` - continuous cosine-based pulse\n"
1823+
"- `linger` - like smooth but stays visible longer\n"
1824+
"\n"
1825+
"``` yaml\n"
1826+
"profiles:\n"
1827+
" profile_name:\n"
1828+
" blink_style: smooth\n"
1829+
"```\n"
1830+
"\n"
1831+
};
1832+
1833+
using BlinkStyle = DocumentationEntry<BlinkStyleConfig, BlinkStyleWeb>;
18061834
using Colors = DocumentationEntry<ColorsConfig, ColorsWeb>;
18071835
using ModalCursorScrollOff = DocumentationEntry<ModalCursorScrollOffConfig, ModalCursorScrollOffWeb>;
18081836
using ModeInsert = DocumentationEntry<ModeInsertConfig, ModeInsertWeb>;

src/contour/TerminalSession.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ namespace
139139
settings.cursorBlinkInterval = profile.modeInsert.value().cursor.cursorBlinkInterval;
140140
settings.cursorShape = profile.modeInsert.value().cursor.cursorShape;
141141
settings.cursorDisplay = profile.modeInsert.value().cursor.cursorDisplay;
142+
settings.blinkStyle = profile.blinkStyle.value();
142143
settings.smoothLineScrolling = profile.smoothLineScrolling.value();
143144
settings.wordDelimiters = unicode::from_utf8(config.wordDelimiters.value());
144145
settings.mouseProtocolBypassModifiers = config.bypassMouseProtocolModifiers.value();
@@ -289,6 +290,27 @@ void TerminalSession::attachDisplay(display::TerminalDisplay& newDisplay)
289290
_terminal.setRefreshRate(_display->refreshRate());
290291
}
291292

293+
// Ensure max image size is based on the actual display dimensions,
294+
// not just the (possibly zero) config default.
295+
// This is needed because configureDisplay() is only called from createRenderer(),
296+
// which only runs once for the first session.
297+
{
298+
auto const dpr = _display->contentScale();
299+
auto const qActualScreenSize = _display->window()->screen()->size() * dpr;
300+
auto const actualScreenSize = ImageSize { Width::cast_from(qActualScreenSize.width()),
301+
Height::cast_from(qActualScreenSize.height()) };
302+
auto const configuredMaxImageSize = _config.images.value().maxImageSize;
303+
auto const _ = std::scoped_lock { _terminal };
304+
// clang-format off
305+
auto const maxImageSize = ImageSize {
306+
.width = unbox(configuredMaxImageSize.width) == 0 ? actualScreenSize.width : configuredMaxImageSize.width,
307+
.height = unbox(configuredMaxImageSize.height) == 0 ? actualScreenSize.height : configuredMaxImageSize.height,
308+
};
309+
// clang-format on
310+
311+
_terminal.setMaxImageSize(maxImageSize, maxImageSize);
312+
}
313+
292314
{
293315
auto const _ = std::scoped_lock { _onClosedMutex };
294316
if (_onClosedHandled)
@@ -1750,6 +1772,7 @@ void TerminalSession::configureTerminal()
17501772
_terminal.viewport().setScrollOff(_profile.modalCursorScrollOff.value());
17511773
_terminal.inputHandler().setSearchModeSwitch(_profile.searchModeSwitch.value());
17521774
_terminal.settings().isInsertAfterYank = _profile.insertAfterYank.value();
1775+
_terminal.settings().blinkStyle = _profile.blinkStyle.value();
17531776
}
17541777

17551778
void TerminalSession::configureCursor(config::CursorConfig const& cursorConfig)

src/vtbackend/CellUtil.h

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ namespace vtbackend::CellUtil
2020
bool reverseVideo,
2121
Color foregroundColor,
2222
Color backgroundColor,
23-
bool blinkingState,
24-
bool rapidBlinkState) noexcept
23+
float blinkingState,
24+
float rapidBlinkState) noexcept
2525
{
2626
auto const fgMode = [](CellFlags flags, ColorPalette const& colorPalette) {
2727
if (flags & CellFlag::Faint)
@@ -47,10 +47,10 @@ namespace vtbackend::CellUtil
4747
if (cellFlags & CellFlag::Hidden)
4848
rgbColors = rgbColors.allBackground();
4949

50-
if ((cellFlags & CellFlag::Blinking) && !blinkingState)
51-
return rgbColors.allBackground();
52-
if ((cellFlags & CellFlag::RapidBlinking) && !rapidBlinkState)
53-
return rgbColors.allBackground();
50+
if (cellFlags & CellFlag::Blinking)
51+
return mix(rgbColors, rgbColors.allBackground(), blinkingState);
52+
if (cellFlags & CellFlag::RapidBlinking)
53+
return mix(rgbColors, rgbColors.allBackground(), rapidBlinkState);
5454

5555
return rgbColors;
5656
}

src/vtbackend/RenderBufferBuilder.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ namespace
5757
bool isCursor,
5858
bool isCursorLine,
5959
bool isHighlighted,
60-
bool blink,
61-
bool rapidBlink) noexcept
60+
float blink,
61+
float rapidBlink) noexcept
6262
{
6363
auto sgrColors = CellUtil::makeColors(
6464
colorPalette, cellFlags, reverseVideo, foregroundColor, backgroundColor, blink, rapidBlink);

src/vtbackend/Settings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ struct Settings
5959
bool syncWindowTitleWithHostWritableStatusDisplay = true;
6060
CursorDisplay cursorDisplay = CursorDisplay::Steady;
6161
CursorShape cursorShape = CursorShape::Block;
62+
BlinkStyle blinkStyle = BlinkStyle::Smooth;
6263

6364
bool usePrivateColorRegisters = false;
6465

src/vtbackend/Terminal.cpp

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,20 +1187,30 @@ void Terminal::updateHoveringHyperlinkState()
11871187
optional<chrono::milliseconds> Terminal::nextRender() const
11881188
{
11891189
auto nextBlink = chrono::milliseconds::max();
1190-
if ((isModeEnabled(DECMode::VisibleCursor) && _settings.cursorDisplay == CursorDisplay::Blink)
1191-
|| isBlinkOnScreen())
1190+
1191+
// Cursor blink scheduling
1192+
if (isModeEnabled(DECMode::VisibleCursor) && _settings.cursorDisplay == CursorDisplay::Blink)
11921193
{
11931194
auto const passedCursor =
11941195
chrono::duration_cast<chrono::milliseconds>(_currentTime - _lastCursorBlink);
1195-
auto const passedSlowBlink = chrono::duration_cast<chrono::milliseconds>(_currentTime - _lastBlink);
1196-
auto const passedRapidBlink =
1197-
chrono::duration_cast<chrono::milliseconds>(_currentTime - _lastRapidBlink);
11981196
if (passedCursor <= _settings.cursorBlinkInterval)
11991197
nextBlink = std::min(nextBlink, _settings.cursorBlinkInterval - passedCursor);
1200-
if (passedSlowBlink <= _slowBlinker.interval)
1201-
nextBlink = std::min(nextBlink, _slowBlinker.interval - passedSlowBlink);
1202-
if (passedRapidBlink <= _rapidBlinker.interval)
1203-
nextBlink = std::min(nextBlink, _rapidBlinker.interval - passedRapidBlink);
1198+
}
1199+
1200+
// Cell blink scheduling
1201+
if (isBlinkOnScreen())
1202+
{
1203+
if (_settings.blinkStyle == BlinkStyle::Classic)
1204+
{
1205+
// Classic mode only needs redraws at toggle transitions.
1206+
nextBlink = std::min(nextBlink, _slowBlinker.nextToggleIn(_currentTime));
1207+
nextBlink = std::min(nextBlink, _rapidBlinker.nextToggleIn(_currentTime));
1208+
}
1209+
else
1210+
{
1211+
// Smooth/Linger modes require continuous animation at ~30fps.
1212+
nextBlink = std::min(nextBlink, chrono::milliseconds { 33 });
1213+
}
12041214
}
12051215

12061216
if (_statusDisplayType == StatusDisplayType::Indicator)
@@ -1227,11 +1237,6 @@ void Terminal::tick(chrono::steady_clock::time_point now) noexcept
12271237

12281238
_currentTime = now;
12291239
updateCursorVisibilityState();
1230-
if (isBlinkOnScreen())
1231-
{
1232-
tie(_rapidBlinker.state, _lastRapidBlink) = nextBlinkState(_rapidBlinker, _lastRapidBlink);
1233-
tie(_slowBlinker.state, _lastBlink) = nextBlinkState(_slowBlinker, _lastBlink);
1234-
}
12351240
}
12361241

12371242
void Terminal::resizeScreen(PageSize totalPageSize, optional<ImageSize> pixels)

src/vtbackend/Terminal.h

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@
3535
#include <atomic>
3636
#include <bitset>
3737
#include <chrono>
38+
#include <cmath>
3839
#include <condition_variable>
3940
#include <deque>
4041
#include <format>
4142
#include <functional>
4243
#include <memory>
4344
#include <mutex>
45+
#include <numbers>
4446
#include <stack>
4547
#include <string_view>
4648
#include <utility>
@@ -735,8 +737,11 @@ class Terminal
735737
}
736738

737739
bool isHighlighted(CellLocation cell) const noexcept;
738-
bool blinkState() const noexcept { return _slowBlinker.state; }
739-
bool rapidBlinkState() const noexcept { return _rapidBlinker.state; }
740+
float blinkState() const noexcept { return _slowBlinker.opacity(_currentTime, _settings.blinkStyle); }
741+
float rapidBlinkState() const noexcept
742+
{
743+
return _rapidBlinker.opacity(_currentTime, _settings.blinkStyle);
744+
}
740745

741746
/// Sets or resets to a new selection.
742747
void setSelector(std::unique_ptr<Selection> selector);
@@ -1047,16 +1052,6 @@ class Terminal
10471052
&& _inputHandler.mode() == ViMode::Insert;
10481053
}
10491054

1050-
template <typename BlinkerState>
1051-
[[nodiscard]] std::pair<bool, std::chrono::steady_clock::time_point> nextBlinkState(
1052-
BlinkerState blinker, std::chrono::steady_clock::time_point lastBlink) const noexcept
1053-
{
1054-
auto const passed = std::chrono::duration_cast<std::chrono::milliseconds>(_currentTime - lastBlink);
1055-
if (passed < blinker.interval)
1056-
return { blinker.state, lastBlink };
1057-
return { !blinker.state, _currentTime };
1058-
}
1059-
10601055
// Reads from PTY.
10611056
struct PtyReadResult
10621057
{
@@ -1104,15 +1099,42 @@ class Terminal
11041099
// {{{ blinking state helpers
11051100
mutable std::chrono::steady_clock::time_point _lastCursorBlink;
11061101
mutable unsigned _cursorBlinkState = 1;
1102+
1103+
/// Computes blink opacity based on the configured BlinkStyle.
11071104
struct BlinkerState
11081105
{
1109-
bool state = false;
1110-
std::chrono::milliseconds interval {};
1106+
std::chrono::milliseconds period {};
1107+
1108+
/// Returns opacity in [0, 1] based on the given blink style.
1109+
[[nodiscard]] float opacity(std::chrono::steady_clock::time_point now,
1110+
BlinkStyle style) const noexcept
1111+
{
1112+
auto const elapsed = std::chrono::duration<float>(now.time_since_epoch());
1113+
auto const p = std::chrono::duration<float>(period);
1114+
auto const smooth =
1115+
(1.0f + std::cos(2.0f * std::numbers::pi_v<float> * elapsed.count() / p.count())) / 2.0f;
1116+
switch (style)
1117+
{
1118+
case BlinkStyle::Classic: return smooth >= 0.5f ? 1.0f : 0.0f;
1119+
case BlinkStyle::Smooth: return smooth;
1120+
case BlinkStyle::Linger: return std::pow(smooth, 1.0f / 3.0f);
1121+
}
1122+
return smooth;
1123+
}
1124+
1125+
/// Returns the time until the next classic toggle transition.
1126+
[[nodiscard]] std::chrono::milliseconds nextToggleIn(
1127+
std::chrono::steady_clock::time_point now) const noexcept
1128+
{
1129+
using namespace std::chrono;
1130+
auto const elapsed = duration_cast<milliseconds>(now.time_since_epoch());
1131+
auto const halfPeriod = period / 2;
1132+
auto const inPeriod = elapsed % period;
1133+
return (inPeriod < halfPeriod) ? (halfPeriod - inPeriod) : (period - inPeriod);
1134+
}
11111135
};
1112-
mutable BlinkerState _slowBlinker { .state = false, .interval = std::chrono::milliseconds { 500 } };
1113-
mutable BlinkerState _rapidBlinker { .state = false, .interval = std::chrono::milliseconds { 300 } };
1114-
mutable std::chrono::steady_clock::time_point _lastBlink;
1115-
mutable std::chrono::steady_clock::time_point _lastRapidBlink;
1136+
BlinkerState _slowBlinker { .period = std::chrono::milliseconds { 1000 } };
1137+
BlinkerState _rapidBlinker { .period = std::chrono::milliseconds { 600 } };
11161138
// }}}
11171139

11181140
// {{{ Displays this terminal manages

0 commit comments

Comments
 (0)