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, Views, or Terminal.

Constant Summary collapse

DEFAULT_ESC_DELAY =
25

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_attr(style, fallback_role: :output) ⇒ Object

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

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

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



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/fatty/curses/context.rb', line 168

def ansi_attr(style, fallback_role: :output)
  base_pair_id = Fatty::Colors::Pairs::ROLE_TO_PAIR.fetch(fallback_role)
  base_attr = ::Curses.color_pair(base_pair_id)

  has_explicit = !(style.fg.nil? && style.bg.nil?)
  attr =
    if has_explicit
      pair_id = ansi_pair_id(style.fg, style.bg, fallback_pair_id: base_pair_id)
      ::Curses.color_pair(pair_id)
    else
      base_attr
    end
  attr |= ::Curses::A_BOLD if style.bold
  attr |= ::Curses::A_UNDERLINE if style.underline
  attr |= ::Curses::A_REVERSE if style.reverse
  if style.italic && defined?(::Curses::A_ITALIC)
    attr |= ::Curses::A_ITALIC
  end
  if style.strike && defined?(::Curses::A_HORIZONTAL)
    attr |= ::Curses::A_HORIZONTAL
  end
  attr
end

#apply_layout(screen) ⇒ Object

Allocate or reallocate windows using Screen layout.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/fatty/curses/context.rb', line 84

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



192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/fatty/curses/context.rb', line 192

def apply_palette(palette)
  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



109
110
111
112
113
114
# File 'lib/fatty/curses/context.rb', line 109

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

#configure_escape_delay!Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/fatty/curses/context.rb', line 52

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 <= 0
  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

#setup_colorsObject



68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/fatty/curses/context.rb', line 68

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

  ::Curses.init_screen
  configure_escape_delay!
  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

#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)


123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/fatty/curses/context.rb', line 123

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
      truecolor_env?
    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: :themes,
  )
  @truecolor
end

#truecolor_env?Boolean

Returns:

  • (Boolean)


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

def truecolor_env?
  colorterm = ENV["COLORTERM"].to_s
  term = ENV["TERM"].to_s
  term_program = ENV["TERM_PROGRAM"].to_s

  colorterm.match?(/truecolor|24bit/i) ||
    term.match?(/truecolor|24bit|direct/i) ||
    term.match?(/kitty|wezterm|alacritty|ghostty|foot/i) ||
    term_program.match?(/kitty|wezterm|alacritty|ghostty|iTerm/i)
end