Skip to content
Kward Search API index

Module: Kward::TerminalImageSupport

Defined in:
lib/kward/terminal_image_support.rb

Overview

Terminal capability detection for inline image protocols.

Constant Summary collapse

KITTY =
:kitty
ITERM2 =
:iterm2
PROBE_TIMEOUT =
0.3
KITTY_PROBE =
"#{TerminalSequences.kitty_query}\e[c".freeze
KITTY_OK_RESPONSE =
"\e_Gi=31;OK\e\\".freeze
DA1_RESPONSE_PATTERN =
/\e\[\?[0-9;]*c/.freeze

Class Method Summary collapse

Class Method Details

.da1_response?(response) ⇒ Boolean

Returns:

  • (Boolean)


78
79
80
# File 'lib/kward/terminal_image_support.rb', line 78

def da1_response?(response)
  response.to_s.match?(DA1_RESPONSE_PATTERN)
end

.detect(env: ENV, input: nil, output: nil, timeout: PROBE_TIMEOUT, probe_result: :auto) ⇒ Object



16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/kward/terminal_image_support.rb', line 16

def detect(env: ENV, input: nil, output: nil, timeout: PROBE_TIMEOUT, probe_result: :auto)
  return static_protocol(env) if input.nil? && output.nil? && probe_result == :auto
  return nil unless tty?(input) && tty?(output)

  probe_result = probe_kitty(input, output, env: env, timeout: timeout) if probe_result == :auto
  return KITTY if probe_result == true

  fallback = static_protocol(env)
  return fallback if probe_result.nil? || fallback

  nil
end

.iterm2_hint?(env) ⇒ Boolean

Returns:

  • (Boolean)


91
92
93
94
95
96
97
98
# File 'lib/kward/terminal_image_support.rb', line 91

def iterm2_hint?(env)
  value = normalized_environment(env)
  program = value.fetch("TERM_PROGRAM", "")
  return true if program == "iterm.app" || program == "wezterm"
  return true if value.fetch("TERM_FEATURES", "").include?("f")

  value["WEZTERM_PANE"] && !value["WEZTERM_PANE"].empty?
end

.kitty_hint?(env) ⇒ Boolean

Returns:

  • (Boolean)


82
83
84
85
86
87
88
89
# File 'lib/kward/terminal_image_support.rb', line 82

def kitty_hint?(env)
  value = normalized_environment(env)
  return true if value["KITTY_WINDOW_ID"] && !value["KITTY_WINDOW_ID"].empty?

  program = value.fetch("TERM_PROGRAM", "")
  term = value.fetch("TERM", "")
  program == "kitty" || program == "ghostty" || term.include?("kitty") || term.include?("ghostty")
end

.kitty_probe_success?(response) ⇒ Boolean

Returns:

  • (Boolean)


74
75
76
# File 'lib/kward/terminal_image_support.rb', line 74

def kitty_probe_success?(response)
  da1_response?(response) && response.to_s.include?(KITTY_OK_RESPONSE)
end

.monotonic_nowObject



112
113
114
# File 'lib/kward/terminal_image_support.rb', line 112

def monotonic_now
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
end

.normalized_environment(env) ⇒ Object



100
101
102
103
104
# File 'lib/kward/terminal_image_support.rb', line 100

def normalized_environment(env)
  env.to_h.each_with_object({}) do |(key, value), normalized|
    normalized[key.to_s] = value.to_s.strip.downcase
  end
end

.probe_kitty(input, output, env: ENV, timeout: PROBE_TIMEOUT) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/kward/terminal_image_support.rb', line 36

def probe_kitty(input, output, env: ENV, timeout: PROBE_TIMEOUT)
  return nil unless tty?(input) && tty?(output)
  return nil unless input.respond_to?(:raw)
  return nil unless output.respond_to?(:write) && output.respond_to?(:flush)

  query = env["TMUX"].to_s.empty? ? KITTY_PROBE : TerminalSequences.tmux_passthrough(KITTY_PROBE)
  input.raw do
    output.write(query)
    output.flush
    read_probe_response(input, timeout: timeout)
  end
rescue StandardError
  nil
end

.read_probe_response(input, timeout: PROBE_TIMEOUT) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/kward/terminal_image_support.rb', line 51

def read_probe_response(input, timeout: PROBE_TIMEOUT)
  buffer = +""
  deadline = monotonic_now + timeout.to_f
  loop do
    break if da1_response?(buffer)

    remaining = deadline - monotonic_now
    break if remaining <= 0

    ready = IO.select([input], nil, nil, remaining)
    break unless ready

    chunk = input.read_nonblock(256, exception: false)
    break if chunk == :wait_readable || chunk.nil? || chunk.empty?

    buffer << chunk
  end

  kitty_probe_success?(buffer)
rescue IOError, SystemCallError
  false
end

.static_protocol(env) ⇒ Object



29
30
31
32
33
34
# File 'lib/kward/terminal_image_support.rb', line 29

def static_protocol(env)
  return KITTY if kitty_hint?(env)
  return ITERM2 if iterm2_hint?(env)

  nil
end

.tty?(io) ⇒ Boolean

Returns:

  • (Boolean)


106
107
108
109
110
# File 'lib/kward/terminal_image_support.rb', line 106

def tty?(io)
  io.respond_to?(:tty?) && io.tty?
rescue IOError, SystemCallError
  false
end