Class: Fatty::Curses::Context

Inherits:
Object
  • Object
show all
Defined in:
lib/fatty/curses/context.rb

Overview

Context represents the active curses environment.

It owns:

- curses initialization and shutdown
- terminal mode configuration
- window lifecycle

Context does NOT:

- read input
- decode keys
- render UI

Those responsibilities belong to EventSource and Renderer.

Context exists so that all curses state is centralized and never leaks into Sessions or Terminal.

Constant Summary collapse

DEFAULT_ESC_DELAY =
0

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeContext

Returns a new instance of Context.



29
30
31
# File 'lib/fatty/curses/context.rb', line 29

def initialize
  @started = false
end

Instance Attribute Details

#alert_winObject (readonly)

Returns the value of attribute alert_win.



26
27
28
# File 'lib/fatty/curses/context.rb', line 26

def alert_win
  @alert_win
end

#colsObject (readonly)

Returns the value of attribute cols.



27
28
29
# File 'lib/fatty/curses/context.rb', line 27

def cols
  @cols
end

#input_winObject (readonly)

Returns the value of attribute input_win.



26
27
28
# File 'lib/fatty/curses/context.rb', line 26

def input_win
  @input_win
end

#output_winObject (readonly)

Returns the value of attribute output_win.



26
27
28
# File 'lib/fatty/curses/context.rb', line 26

def output_win
  @output_win
end

#paletteObject (readonly)

Returns the value of attribute palette.



27
28
29
# File 'lib/fatty/curses/context.rb', line 27

def palette
  @palette
end

#rowsObject (readonly)

Returns the value of attribute rows.



27
28
29
# File 'lib/fatty/curses/context.rb', line 27

def rows
  @rows
end

#status_winObject (readonly)

Returns the value of attribute status_win.



26
27
28
# File 'lib/fatty/curses/context.rb', line 26

def status_win
  @status_win
end

#truecolorObject (readonly)

Returns the value of attribute truecolor.



27
28
29
# File 'lib/fatty/curses/context.rb', line 27

def truecolor
  @truecolor
end

Instance Method Details

#ansi_pair_for(fg, bg, fallback_pair_id:) ⇒ Object

Return a curses color-pair id for an ANSI foreground/background pair.

This owns the dynamic ANSI pair cache. Renderers decide which fg/bg indexes they want; Context decides whether a pair can be allocated.



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/fatty/curses/context.rb', line 198

def ansi_pair_for(fg, bg, fallback_pair_id:)
  return fallback_pair_id unless ::Curses.has_colors?

  @ansi_pairs ||= {}
  @ansi_pair_limit ||= begin
                         ::Curses.color_pairs.to_i
                       rescue StandardError
                         256
                       end

  key = [fg.nil? ? -1 : fg.to_i, bg.nil? ? -1 : bg.to_i]
  cached = @ansi_pairs[key]
  return cached if cached

  start_id = Fatty::Colors::Pairs::ROLE_TO_PAIR.values.max + 1
  @ansi_next_pair_id ||= start_id
  @ansi_next_pair_id = start_id if @ansi_next_pair_id < start_id

  if @ansi_pair_limit.positive? && @ansi_next_pair_id >= @ansi_pair_limit
    Fatty.debug(
      "ansi_pair_for exhausted next=#{@ansi_next_pair_id.inspect} " \
      "limit=#{@ansi_pair_limit.inspect} fg=#{fg.inspect} bg=#{bg.inspect} " \
      "fallback=#{fallback_pair_id.inspect}",
      tag: :theme,
    )
    @ansi_pairs[key] = fallback_pair_id
  else
    pair_id = @ansi_next_pair_id
    @ansi_next_pair_id += 1

    ::Curses.init_pair(
      pair_id,
      fg.nil? ? -1 : fg.to_i,
      bg.nil? ? -1 : bg.to_i,
    )

    @ansi_pairs[key] = pair_id
  end

  @ansi_pairs[key]
end

#apply_layout(screen) ⇒ Object

Allocate or reallocate windows using Screen layout.



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/fatty/curses/context.rb', line 111

def apply_layout(screen)
  ensure_started!

  @rows = screen.rows
  @cols = screen.cols

  close_windows

  out = screen.output_rect
  sts = screen.status_rect
  inp = screen.input_rect
  alr = screen.alert_rect

  @output_win = ::Curses::Window.new(out.rows, out.cols, out.row, out.col)
  @status_win = ::Curses::Window.new(sts.rows, sts.cols, sts.row, sts.col)
  @input_win  = ::Curses::Window.new(inp.rows, inp.cols, inp.row, inp.col)
  @alert_win  = ::Curses::Window.new(alr.rows, alr.cols, alr.row, alr.col)

  # We do our own viewport/paging; allowing curses to scroll introduces
  # “mystery” blank lines if a newline slips into output
  @output_win.scrollok(true)
  @input_win.keypad(true)
  self
end

#apply_palette(palette) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/fatty/curses/context.rb', line 177

def apply_palette(palette)
  reset_ansi_pairs!

  if ::Curses.has_colors?
    ::Curses.start_color
    ::Curses.use_default_colors if ::Curses.respond_to?(:use_default_colors)

    palette.each_value do |entry|
      next unless entry[:pair]

      ::Curses.init_pair(entry[:pair], entry[:fg], entry[:bg])
    end
  end

  @palette = palette
end

#closeObject



136
137
138
139
140
141
# File 'lib/fatty/curses/context.rb', line 136

def close
  close_windows
  disable_bracketed_paste! if @started
  ::Curses.close_screen if @started
  @started = false
end

#configure_escape_delay!Object



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/fatty/curses/context.rb', line 79

def configure_escape_delay!
  delay =
    if ENV["ESCDELAY"]
      ENV["ESCDELAY"].to_i
    else
      Fatty::Config.config.dig(:esc_delay)&.to_i
    end
  delay = DEFAULT_ESC_DELAY if delay.nil? || delay.negative?
  if ::Curses.respond_to?(:set_escdelay)
    ::Curses.set_escdelay(delay)
  else
    ENV["ESCDELAY"] = delay.to_s
  end
  Fatty.info("ESC delay set to #{delay} ms", tag: :input)
end

#environmentObject



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/fatty/curses/context.rb', line 56

def environment
  data = {
    started: started?,
    truecolor: truecolor,
    key_min: ::Curses::KEY_MIN,
    key_max: ::Curses::KEY_MAX,
  }
  if started?
    data.merge(
      lines: ::Curses.lines,
      cols: ::Curses.cols,
      has_colors: ::Curses.has_colors?,
      colors: ::Curses.colors,
      color_pairs: ::Curses.color_pairs,
      can_change_color: ::Curses.can_change_color?,
    )
  else
    data
  end
rescue StandardError
  {}
end

#reset_ansi_pairs!Object



240
241
242
243
244
# File 'lib/fatty/curses/context.rb', line 240

def reset_ansi_pairs!
  @ansi_pairs = {}
  @ansi_next_pair_id = nil
  @ansi_pair_limit = nil
end

#setup_colorsObject



95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/fatty/curses/context.rb', line 95

def setup_colors
  return unless ::Curses.has_colors?

  reset_ansi_pairs!
  ::Curses.start_color
  ::Curses.use_default_colors if ::Curses.respond_to?(:use_default_colors)

  theme_spec = Fatty::Themes::Manager.roles(Fatty::Themes::Manager.current) || {}
  palette = Fatty::Colors::Palette.compile(
    theme_spec,
    available_colors: ::Curses.colors,
  )
  apply_palette(palette)
end

#startObject



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/fatty/curses/context.rb', line 33

def start
  return self if @started

  configure_escape_delay!
  ::Curses.init_screen
  MouseConstants.ensure!

  ::Curses.raw
  ::Curses.noecho
  ::Curses.curs_set(1)
  ::Curses.stdscr.keypad(true)
  ::Curses.mousemask(::Curses::ALL_MOUSE_EVENTS)
  enable_bracketed_paste!
  setup_colors
  @truecolor = truecolor_enabled?
  @started = true
  self
end

#started?Boolean

Returns:

  • (Boolean)


52
53
54
# File 'lib/fatty/curses/context.rb', line 52

def started?
  @started
end

#truecolor_enabled?Boolean

Map a Fatty::Ansi::Style to a curses attribute.

  • If style has no explicit fg/bg, we keep the themed role pair.
  • If style specifies fg/bg, we allocate/init a curses pair on demand.

Note: this is intentionally independent of theme roles; it is for SGR output runs inside the output pane.

Returns:

  • (Boolean)


150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/fatty/curses/context.rb', line 150

def truecolor_enabled?
  cfg = Fatty::Config.config

  setting =
    if cfg.key?(:truecolor)
      cfg[:truecolor]
    else
      "auto"
    end
  @truecolor =
    case setting.to_s.downcase
    when "true", "yes", "on", "1"
      true
    when "false", "no", "off", "0"
      false
    else
      Fatty::Env.truecolor_detected?
    end
  Fatty.info(
    "truecolor=#{@truecolor} setting=#{setting.inspect} " \
      "TERM=#{ENV['TERM'].inspect} COLORTERM=#{ENV['COLORTERM'].inspect} " \
      "TERM_PROGRAM=#{ENV['TERM_PROGRAM'].inspect} TMUX=#{ENV.key?('TMUX')}",
    tag: :theme,
  )
  @truecolor
end