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.



36
37
38
# File 'lib/fatty/curses/context.rb', line 36

def initialize
  @started = false
end

Instance Attribute Details

#alert_winObject (readonly)

Returns the value of attribute alert_win.



33
34
35
# File 'lib/fatty/curses/context.rb', line 33

def alert_win
  @alert_win
end

#colsObject (readonly)

Returns the value of attribute cols.



34
35
36
# File 'lib/fatty/curses/context.rb', line 34

def cols
  @cols
end

#input_winObject (readonly)

Returns the value of attribute input_win.



33
34
35
# File 'lib/fatty/curses/context.rb', line 33

def input_win
  @input_win
end

#output_winObject (readonly)

Returns the value of attribute output_win.



33
34
35
# File 'lib/fatty/curses/context.rb', line 33

def output_win
  @output_win
end

#paletteObject (readonly)

Returns the value of attribute palette.



34
35
36
# File 'lib/fatty/curses/context.rb', line 34

def palette
  @palette
end

#rowsObject (readonly)

Returns the value of attribute rows.



34
35
36
# File 'lib/fatty/curses/context.rb', line 34

def rows
  @rows
end

#status_winObject (readonly)

Returns the value of attribute status_win.



33
34
35
# File 'lib/fatty/curses/context.rb', line 33

def status_win
  @status_win
end

#truecolorObject (readonly)

Returns the value of attribute truecolor.



34
35
36
# File 'lib/fatty/curses/context.rb', line 34

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.



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/fatty/curses/context.rb', line 229

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.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/fatty/curses/context.rb', line 118

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



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/fatty/curses/context.rb', line 208

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



161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/fatty/curses/context.rb', line 161

def close
  close_windows
  if @started
    disable_bracketed_paste!
    ::Curses.curs_set(1)
    ::Curses.noraw
    ::Curses.echo
    ::Curses.nl
    ::Curses.close_screen
  end
  @started = false
end

#configure_escape_delay!Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/fatty/curses/context.rb', line 86

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



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/fatty/curses/context.rb', line 63

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



271
272
273
274
275
# File 'lib/fatty/curses/context.rb', line 271

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

#resumeObject



153
154
155
156
157
158
159
# File 'lib/fatty/curses/context.rb', line 153

def resume
  return unless @started

  ::Curses.refresh
  enable_bracketed_paste!
  nil
end

#setup_colorsObject



102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/fatty/curses/context.rb', line 102

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



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/fatty/curses/context.rb', line 40

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)


59
60
61
# File 'lib/fatty/curses/context.rb', line 59

def started?
  @started
end

#suspendObject



143
144
145
146
147
148
149
150
151
# File 'lib/fatty/curses/context.rb', line 143

def suspend
  return unless @started

  ::Curses.def_prog_mode
  disable_bracketed_paste!
  ::Curses.curs_set(1)
  Native.endwin
  nil
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)


181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/fatty/curses/context.rb', line 181

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