Skip to content

Commit

Permalink
sixel: Implement DEC-accurate calculation of cursor position for jumping
Browse files Browse the repository at this point in the history
Right now only known to be implemented by development version of
Windows Terminal.
Until we can detect that this is the terminal we're running, requires
to set environment variable `TIMG_SIXEL_FULL_CELL_JUMP=1`

Issues: #145
  • Loading branch information
hzeller committed Jan 11, 2025
1 parent e5931b9 commit dafeed3
Show file tree
Hide file tree
Showing 9 changed files with 60 additions and 20 deletions.
10 changes: 7 additions & 3 deletions man/timg.1
Original file line number Diff line number Diff line change
Expand Up @@ -499,9 +499,13 @@ connection that can\[cq]t keep up with playing videos.
Or if you have a very slow CPU.
.TP
\f[B]TIMG_SIXEL_NEWLINE_WORKAROUND\f[R]
Set this environment variable to 1 if you are on a Sixel terminal and
notice that videos `scroll' or grid\-view items are not perfectly
aligned vertically.
Set this environment variable if you are on a Sixel terminal and notice
that videos `scroll' or grid\-view items are not perfectly aligned
vertically.
Sometimes this manifests only for pictures of a particular height.
Valid values are 0, 1, 2, 3 which address various subtle differences in
which sixel terminals behave.
Default 0.
.SH EXAMPLES
Some example invocations including scrolling text or streaming an online
video are put together at \c
Expand Down
7 changes: 5 additions & 2 deletions man/timg.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,12 @@ Exit code is
slow CPU.

**TIMG_SIXEL_NEWLINE_WORKAROUND**
: Set this environment variable to 1 if you are on a Sixel terminal and
: Set this environment variable if you are on a Sixel terminal and
notice that videos 'scroll' or grid-view items are not perfectly aligned
vertically.
vertically. Sometimes this manifests only for pictures of a particular
height.
Valid values are 0, 1, 2, 3 which address various subtle differences in
which sixel terminals behave. Default 0.

# EXAMPLES

Expand Down
24 changes: 17 additions & 7 deletions src/sixel-canvas.cc
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ namespace timg {

SixelCanvas::SixelCanvas(BufferedWriteSequencer *ws, ThreadPool *thread_pool,
bool required_cursor_placement_workaround,
const DisplayOptions &opts)
: TerminalCanvas(ws), options_(opts), executor_(thread_pool) {
bool full_cell_jump, const DisplayOptions &opts)
: TerminalCanvas(ws),
options_(opts),
full_cell_jump_(full_cell_jump),
executor_(thread_pool) {
// Terminals might have different understanding where the curosr is placed
// after an image is sent.
// Apparently the original dec terminal placed it afterwards, but some
Expand Down Expand Up @@ -153,11 +156,18 @@ void SixelCanvas::Send(int x, int dy, const Framebuffer &fb_orig,
int SixelCanvas::cell_height_for_pixels(int pixels) const {
assert(pixels <= 0); // Currently only use-case
pixels = -pixels;
// Unlike the other exact pixel canvases where we have to round to the
// next even cell_y_px, here we first need to round to the next even 6
// first.
return -((round_to_sixel(pixels) + options_.cell_y_px - 1) /
options_.cell_y_px);
if (full_cell_jump_) {
// https://github.com/hzeller/timg/issues/145#issuecomment-2579962760
// As DEC intended.
return -((round_to_sixel(pixels) - 6) / options_.cell_y_px + 1);
}
else {
// Unlike the other exact pixel canvases where we have to round to the
// next even cell_y_px, here we first need to round to the next even 6
// first.
return -((round_to_sixel(pixels) + options_.cell_y_px - 1) /
options_.cell_y_px);
}
}

} // namespace timg
3 changes: 2 additions & 1 deletion src/sixel-canvas.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class ThreadPool;
class SixelCanvas final : public TerminalCanvas {
public:
SixelCanvas(BufferedWriteSequencer *ws, ThreadPool *thread_pool,
bool required_cursor_placement_workaround,
bool required_cursor_placement_workaround, bool full_cell_jump,
const DisplayOptions &opts);

int cell_height_for_pixels(int pixels) const final;
Expand All @@ -38,6 +38,7 @@ class SixelCanvas final : public TerminalCanvas {

private:
const DisplayOptions &options_;
const bool full_cell_jump_;
ThreadPool *const executor_;
const char *cursor_move_before_;
const char *cursor_move_after_;
Expand Down
11 changes: 9 additions & 2 deletions src/term-query.cc
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,9 @@ const char *QueryBackgroundColor() {
TermGraphicsInfo QuerySupportedGraphicsProtocol() {
TermGraphicsInfo result;
result.preferred_graphics = GraphicsProtocol::kNone;
result.known_broken_sixel_cursor_placement =
timg::GetBoolenEnv("TIMG_SIXEL_NEWLINE_WORKAROUND");
const int sixel_env_bits = timg::GetIntEnv("TIMG_SIXEL_NEWLINE_WORKAROUND");
result.known_broken_sixel_cursor_placement = sixel_env_bits & 0x1;
result.sixel_full_cell_jump = sixel_env_bits & 0x20;
result.in_tmux = false;

// Environment variables can be changed, so guesses from environment
Expand Down Expand Up @@ -299,6 +300,12 @@ TermGraphicsInfo QuerySupportedGraphicsProtocol() {
if (find_str(data, len, "tmux")) {
result.in_tmux = true;
}
if (find_str(data, len, "WindowsTerminal")) {
// TODO: check again once name is established
// https://github.com/microsoft/terminal/issues/18382
result.known_broken_sixel_cursor_placement = true;
result.sixel_full_cell_jump = true;
}
// We finish once we found the response to DSR5
return find_str(data, len, TERM_CSI "0");
});
Expand Down
1 change: 1 addition & 0 deletions src/term-query.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ enum class GraphicsProtocol {
struct TermGraphicsInfo {
GraphicsProtocol preferred_graphics;
bool known_broken_sixel_cursor_placement; // see SixelCanvas impl. doc
bool sixel_full_cell_jump;
bool in_tmux;
};

Expand Down
13 changes: 8 additions & 5 deletions src/timg.cc
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ struct PresentationOptions {
// Rendering
Pixelation pixelation = Pixelation::kNotChosen;
bool sixel_cursor_workaround = false;
bool sixel_full_cell_jump = false;
bool tmux_workaround = false;
bool terminal_use_upper_block = false;
bool use_256_color = false; // For terminals that don't do 24 bit color
Expand Down Expand Up @@ -331,9 +332,9 @@ static int PresentImages(LoadedImageSources *loaded_sources,
#ifdef WITH_TIMG_SIXEL
case Pixelation::kSixelGraphics:
compression_pool.reset(new ThreadPool(sequencer->max_queue_len() + 1));
canvas.reset(new timg::SixelCanvas(sequencer, compression_pool.get(),
present.sixel_cursor_workaround,
display_opts));
canvas.reset(new timg::SixelCanvas(
sequencer, compression_pool.get(), present.sixel_cursor_workaround,
present.sixel_full_cell_jump, display_opts));
break;
#endif
case Pixelation::kHalfBlock:
Expand Down Expand Up @@ -770,8 +771,8 @@ int main(int argc, char *argv[]) {
if (present.pixelation == Pixelation::kNotChosen) {
present.pixelation = Pixelation::kQuarterBlock; // Good default.
if (term.font_width_px > 0 && term.font_height_px > 0) {
auto graphics_info = timg::QuerySupportedGraphicsProtocol();
present.tmux_workaround = graphics_info.in_tmux;
const auto graphics_info = timg::QuerySupportedGraphicsProtocol();
present.tmux_workaround = graphics_info.in_tmux;
switch (graphics_info.preferred_graphics) {
case timg::GraphicsProtocol::kIterm2:
present.pixelation = Pixelation::kiTerm2Graphics;
Expand All @@ -784,6 +785,8 @@ int main(int argc, char *argv[]) {
present.pixelation = Pixelation::kSixelGraphics;
present.sixel_cursor_workaround =
graphics_info.known_broken_sixel_cursor_placement;
present.sixel_full_cell_jump =
graphics_info.sixel_full_cell_jump;
#else
present.pixelation = Pixelation::kQuarterBlock;
#endif
Expand Down
8 changes: 8 additions & 0 deletions src/utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ float GetFloatEnv(const char *env_var, float default_value) {
return (*err == '\0' ? result : default_value);
}

int GetIntEnv(const char *env_name, int default_value) {
const char *const value = getenv(env_name);
if (!value) return default_value;
char *err = nullptr;
const int result = strtol(value, &err, 10);
return (*err == '\0' ? result : default_value);
}

std::string HumanReadableByteValue(int64_t byte_count) {
float print_bytes = byte_count;
const char *unit = "Bytes";
Expand Down
3 changes: 3 additions & 0 deletions src/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ bool GetBoolenEnv(const char *env_name, bool default_value = false);
// Get float value from named environment variable.
float GetFloatEnv(const char *env_var, float default_value);

// Get float value from named environment variable.
int GetIntEnv(const char *env_name, int default_value = 0);

// Given number of bytes, return a human-readable version of that
// (e.g. "13.2 MiB").
std::string HumanReadableByteValue(int64_t byte_count);
Expand Down

0 comments on commit dafeed3

Please sign in to comment.