fatty 0.99.0 → 0.99.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.org +11 -29
  3. data/README.md +2207 -18
  4. data/README.org +2066 -104
  5. data/exe/fatty +236 -93
  6. data/help/help.md +233 -0
  7. data/lib/fatty/action.rb +3 -7
  8. data/lib/fatty/actionable.rb +2 -0
  9. data/lib/fatty/alert.rb +65 -39
  10. data/lib/fatty/ansi/renderer.rb +26 -6
  11. data/lib/fatty/ansi.rb +1 -0
  12. data/lib/fatty/api/alert.rb +15 -0
  13. data/lib/fatty/api/choose.rb +145 -0
  14. data/lib/fatty/api/environment.rb +12 -0
  15. data/lib/fatty/api/keytest.rb +11 -0
  16. data/lib/fatty/api/menu.rb +104 -0
  17. data/lib/fatty/api/output.rb +55 -0
  18. data/lib/fatty/api/progress.rb +24 -0
  19. data/lib/fatty/api/prompt.rb +77 -0
  20. data/lib/fatty/api/status.rb +55 -0
  21. data/lib/fatty/api.rb +38 -0
  22. data/lib/fatty/callback_environment.rb +74 -0
  23. data/lib/fatty/colors/color.rb +366 -232
  24. data/lib/fatty/colors/pairs.rb +2 -0
  25. data/lib/fatty/command.rb +78 -0
  26. data/lib/fatty/config.rb +227 -39
  27. data/lib/fatty/config_files/config.yml +4 -12
  28. data/lib/fatty/config_files/keydefs.yml +13 -23
  29. data/lib/fatty/config_files/themes/capuchin_monk.yml +140 -0
  30. data/lib/fatty/config_files/themes/catppuccin_latte.yml +140 -0
  31. data/lib/fatty/config_files/themes/catppuccin_mocha.yml +140 -0
  32. data/lib/fatty/config_files/themes/cyberpunk.yml +141 -0
  33. data/lib/fatty/config_files/themes/dracula.yml +140 -0
  34. data/lib/fatty/config_files/themes/everforest_dark.yml +140 -0
  35. data/lib/fatty/config_files/themes/gruvbox_dark.yml +140 -0
  36. data/lib/fatty/config_files/themes/gruvbox_light.yml +140 -0
  37. data/lib/fatty/config_files/themes/mono.yml +119 -47
  38. data/lib/fatty/config_files/themes/monokai.yml +140 -0
  39. data/lib/fatty/config_files/themes/nordic.yml +1 -0
  40. data/lib/fatty/config_files/themes/onedark.yml +140 -0
  41. data/lib/fatty/config_files/themes/solarized_dark.yml +1 -0
  42. data/lib/fatty/config_files/themes/solarized_light.yml +140 -0
  43. data/lib/fatty/config_files/themes/terminal.yml +67 -0
  44. data/lib/fatty/config_files/themes/tokyo_night.yml +140 -0
  45. data/lib/fatty/config_files/themes/wordperfect.yml +80 -9
  46. data/lib/fatty/core_ext/hash.rb +19 -0
  47. data/lib/fatty/core_ext/string.rb +19 -12
  48. data/lib/fatty/core_ext.rb +1 -0
  49. data/lib/fatty/curses/context.rb +86 -84
  50. data/lib/fatty/curses/event_source.rb +128 -40
  51. data/lib/fatty/curses/key_decoder.rb +77 -11
  52. data/lib/fatty/env.rb +21 -20
  53. data/lib/fatty/help.rb +1 -29
  54. data/lib/fatty/history/entry.rb +2 -0
  55. data/lib/fatty/history.rb +17 -14
  56. data/lib/fatty/input_buffer.rb +71 -47
  57. data/lib/fatty/input_field.rb +216 -149
  58. data/lib/fatty/key_event.rb +67 -66
  59. data/lib/fatty/key_map.rb +54 -16
  60. data/lib/fatty/keymaps/emacs.rb +40 -6
  61. data/lib/fatty/log_formats/json.rb +2 -0
  62. data/lib/fatty/log_formats/text.rb +2 -0
  63. data/lib/fatty/logger.rb +7 -2
  64. data/lib/fatty/markdown/ansi_renderer.rb +75 -6
  65. data/lib/fatty/markdown/render.rb +2 -3
  66. data/lib/fatty/mouse_event.rb +142 -2
  67. data/lib/fatty/output_buffer.rb +2 -0
  68. data/lib/fatty/pager.rb +158 -47
  69. data/lib/fatty/progress.rb +417 -0
  70. data/lib/fatty/renderer/curses.rb +340 -184
  71. data/lib/fatty/renderer/truecolor.rb +266 -163
  72. data/lib/fatty/renderer.rb +115 -97
  73. data/lib/fatty/screen.rb +2 -0
  74. data/lib/fatty/session/alert_session.rb +39 -21
  75. data/lib/fatty/session/isearch_session.rb +92 -38
  76. data/lib/fatty/session/keytest_session.rb +105 -85
  77. data/lib/fatty/session/modal_session.rb +38 -1
  78. data/lib/fatty/session/output_session.rb +472 -21
  79. data/lib/fatty/session/popup_session.rb +251 -134
  80. data/lib/fatty/session/prompt_session.rb +68 -26
  81. data/lib/fatty/session/search_session.rb +79 -31
  82. data/lib/fatty/session/shell_session.rb +383 -368
  83. data/lib/fatty/session/status_session.rb +167 -0
  84. data/lib/fatty/session.rb +95 -95
  85. data/lib/fatty/terminal/popup_owner.rb +14 -11
  86. data/lib/fatty/terminal.rb +515 -813
  87. data/lib/fatty/themes/loader.rb +4 -2
  88. data/lib/fatty/themes/manager.rb +4 -0
  89. data/lib/fatty/themes/registry.rb +2 -0
  90. data/lib/fatty/themes/resolver.rb +175 -162
  91. data/lib/fatty/version.rb +1 -1
  92. data/lib/fatty/viewport.rb +8 -1
  93. data/lib/fatty.rb +35 -7
  94. metadata +42 -40
  95. data/.envrc +0 -2
  96. data/.simplecov +0 -23
  97. data/.yardopts +0 -4
  98. data/CHANGELOG.md +0 -34
  99. data/Rakefile +0 -15
  100. data/TODO.org +0 -163
  101. data/examples/markdown/native-markdown.md +0 -370
  102. data/examples/markdown/ox-gfm-markdown.md +0 -373
  103. data/examples/markdown/ox-gfm-markdown.org +0 -376
  104. data/fatty.gemspec +0 -42
  105. data/lib/fatty/accept_env.rb +0 -32
  106. data/lib/fatty/config_files/help.md +0 -120
  107. data/lib/fatty/config_files/help.org +0 -124
  108. data/lib/fatty/config_files/themes/wordperfect_light.yml +0 -77
  109. data/lib/fatty/menu_env.rb +0 -22
  110. data/lib/fatty/session/input_session.rb +0 -99
  111. data/lib/fatty/sessions.rb +0 -14
  112. data/lib/fatty/terminal/progress.rb +0 -374
  113. data/lib/fatty/view/alert_view.rb +0 -14
  114. data/lib/fatty/view/cursor_view.rb +0 -18
  115. data/lib/fatty/view/input_view.rb +0 -9
  116. data/lib/fatty/view/output_view.rb +0 -9
  117. data/lib/fatty/view/status_view.rb +0 -14
  118. data/lib/fatty/view.rb +0 -33
  119. data/lib/fatty/views.rb +0 -9
  120. data/sig/fatty.rbs +0 -4
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ class StatusSession < Session
5
+ DEFAULT_MAX_ROWS = 6
6
+
7
+ attr_reader :text, :role
8
+
9
+ def initialize(id: nil)
10
+ super
11
+ @text = nil
12
+ @role = :info
13
+ end
14
+
15
+ #########################################################################################
16
+ # Session Protocol
17
+ #########################################################################################
18
+
19
+ def update(command)
20
+ Fatty.debug(
21
+ "StatusSession before action=#{command.action} text=#{text.inspect}",
22
+ tag: :session,
23
+ )
24
+ log_update(command)
25
+ old_rows = rows
26
+ case command.action
27
+ when :show
28
+ set(command.payload)
29
+ when :clear
30
+ clear
31
+ end
32
+ Fatty.debug(
33
+ "StatusSession after action=#{command.action} text=#{text.inspect}",
34
+ tag: :session,
35
+ )
36
+ new_rows = rows
37
+ commands = []
38
+ if old_rows != new_rows
39
+ commands << Command.terminal(:set_status_rows, rows: new_rows)
40
+ commands << Command.terminal(:refresh_layout)
41
+ end
42
+ commands.compact
43
+ end
44
+
45
+ def view
46
+ return unless visible?
47
+
48
+ renderer.render_status(self)
49
+ end
50
+
51
+ #########################################################################################
52
+ # Other public API methods
53
+ #########################################################################################
54
+
55
+ def state
56
+ [
57
+ text.dup.freeze,
58
+ role.dup,
59
+ renderer.screen.status_rect.row.dup,
60
+ renderer.screen.status_rect.rows.dup,
61
+ renderer.screen.status_rect.cols.dup,
62
+ renderer.theme_version
63
+ ]
64
+ end
65
+
66
+ private
67
+
68
+ # simplecov:disable
69
+
70
+ def rows
71
+ return 0 unless visible?
72
+
73
+ lines.length.clamp(1, max_rows)
74
+ end
75
+
76
+ def max_rows
77
+ Fatty::Config.config.dig(:status, :max_rows)&.to_i || DEFAULT_MAX_ROWS
78
+ end
79
+
80
+ def lines
81
+ renderable_text(@text)
82
+ .lines
83
+ .flat_map { |line| wrap_line(line.chomp, width) }
84
+ .last(max_rows)
85
+ end
86
+
87
+ def renderable_text(value)
88
+ parts = value.is_a?(Array) ? value : [value]
89
+
90
+ parts.map do |part|
91
+ if part.is_a?(Hash) && part.key?(:text)
92
+ part[:text].to_s
93
+ else
94
+ part.to_s
95
+ end
96
+ end.join
97
+ end
98
+
99
+ def visible?
100
+ @text && !@text.empty?
101
+ end
102
+
103
+ def set(payload)
104
+ new_text = chomp_text(payload.fetch(:text))
105
+ new_role = payload[:role] || :info
106
+
107
+ if payload[:append] && visible?
108
+ @text = status_parts(@text, role)
109
+ .push(
110
+ "\n",
111
+ {
112
+ text: new_text,
113
+ role: new_role,
114
+ },
115
+ )
116
+ else
117
+ @text = new_text
118
+ @role = new_role
119
+ end
120
+ end
121
+
122
+ def status_parts(text, role)
123
+ if text.is_a?(Array)
124
+ text.dup
125
+ else
126
+ [
127
+ {
128
+ text: text,
129
+ role: role,
130
+ },
131
+ ]
132
+ end
133
+ end
134
+
135
+ def chomp_text(text)
136
+ case text
137
+ when String
138
+ text.chomp
139
+ when Array
140
+ text = text.dup
141
+ index = text.rindex { |segment| segment.is_a?(String) && !segment.empty? }
142
+ text[index] = text[index].chomp if index
143
+ text
144
+ else
145
+ text
146
+ end
147
+ end
148
+
149
+ def clear
150
+ return unless visible?
151
+
152
+ @text = nil
153
+ @role = :info
154
+ end
155
+
156
+ def wrap_line(line, width)
157
+ text = Fatty::Ansi.strip(line.to_s)
158
+ return [""] if text.empty?
159
+
160
+ text.scan(/.{1,#{[width, 1].max}}/)
161
+ end
162
+
163
+ def width
164
+ terminal&.screen&.cols || 80
165
+ end
166
+ end
167
+ end
data/lib/fatty/session.rb CHANGED
@@ -14,23 +14,14 @@ module Fatty
14
14
  class Session
15
15
  include Actionable
16
16
 
17
- attr_reader :terminal, :views, :keymap, :counter
17
+ attr_reader :id, :terminal, :keymap, :counter
18
18
 
19
- def initialize(keymap: nil, views: [])
19
+ def initialize(id: nil, keymap: nil)
20
+ @id = id || default_id
20
21
  @keymap = keymap
21
- @views = Array(views)
22
22
  @counter = Counter.new
23
23
  end
24
24
 
25
- def inspect
26
- "#{self.class.name}:#{object_id}"
27
- end
28
-
29
- def add_view(view)
30
- @views << view
31
- view
32
- end
33
-
34
25
  # Called once when the session becomes active (e.g. pushed).
35
26
  # Subclasses may override to kick off timers/async work, etc.
36
27
  def init(terminal:)
@@ -38,34 +29,23 @@ module Fatty
38
29
  []
39
30
  end
40
31
 
41
- # Handle a message and return commands.
42
- def update(message)
43
- Fatty.debug("#{self.class}#update(message -> #{message})", tag: :session)
32
+ # Handle an incoming Command, which can carry an action or a KeyEvent. On
33
+ # execution, those can return one or more Commands, which update returns
34
+ # to its caller for further dispatch.
35
+ def update(command)
36
+ # raise NotImplementedError, "#{self.class} must implement \#update"
37
+ end
44
38
 
45
- commands =
46
- case message[0]
47
- when :key
48
- ev = message[1]
49
- action, args = resolve_action(ev)
50
-
51
- Fatty.debug(
52
- "#{self.class}#update: key ev=#{ev.inspect} action=#{action.inspect} args=#{args.inspect}",
53
- tag: :session,
54
- )
55
-
56
- if action
57
- handle_action(action, args, event: ev)
58
- else
59
- update_key(ev)
60
- end
61
- when :cmd
62
- Fatty.debug("#{self.class}#update: cmd message=#{message.inspect}", tag: :session)
63
- update_cmd(message[1], message[2])
64
- else
65
- Fatty.warn("#{self.class}#update: unknown message[0]=#{message[0].inspect}", tag: :session)
66
- []
67
- end
68
- commands
39
+ # By default, does nothing. Subclasses can override, but should not
40
+ # mutate state here.
41
+ def view
42
+ end
43
+
44
+ def close
45
+ end
46
+
47
+ def handle_resize
48
+ []
69
49
  end
70
50
 
71
51
  # Save any state we want saved on quit, error, etc.
@@ -76,22 +56,22 @@ module Fatty
76
56
  false
77
57
  end
78
58
 
79
- def resolve_action(ev)
80
- return [nil, []] unless keymap
59
+ def renderer
60
+ terminal.renderer
61
+ end
81
62
 
82
- keymap.resolve_action(ev, contexts: keymap_contexts)
63
+ def screen
64
+ terminal.screen
83
65
  end
84
66
 
85
- # Subclasses can override to vary contexts dynamically
86
- # (paging/isearch/popup/etc). Must return an Array of symbols in
87
- # precedence order.
88
- def keymap_contexts
89
- [:input]
67
+ desc "Cycle to the next theme"
68
+ action :cycle_theme do
69
+ Command.terminal(:cycle_theme)
90
70
  end
91
71
 
92
- # Subclasses override this to react to resolved actions.
93
- def handle_action(_action, _args, event:)
94
- []
72
+ desc "Choose a theme from a popup"
73
+ action :choose_theme do
74
+ Command.terminal(:choose_theme)
95
75
  end
96
76
 
97
77
  desc "Accumulate a count with a decimal digit"
@@ -109,65 +89,85 @@ module Fatty
109
89
  counter.universal_argument!
110
90
  end
111
91
 
112
- # Render the session.
113
- #
114
- # By default, renders all views belonging to the session, ordered by z-index.
115
- # Subclasses can override, but should not mutate state here.
116
- def view(screen:, renderer:)
117
- views.sort_by(&:z).each do |v|
118
- v.render(screen:, renderer:, terminal:, session: self)
119
- end
120
- end
92
+ private
121
93
 
122
- def close
123
- nil
124
- end
94
+ # simplecov:disable
125
95
 
126
- def handle_resize
127
- []
96
+ def log_update(command)
97
+ payload = command.payload
98
+ text = payload[:text] || "(NO TEXT)"
99
+ msg =
100
+ case command.action
101
+ when :key
102
+ "#{self.class}#update(command -> #{command.inspect}) key: #{payload[:event]}"
103
+ else
104
+ sess_str =
105
+ if command.target == :focused_session
106
+ Terminal.find(:focused_session).to_s
107
+ else
108
+ command.target.to_s
109
+ end
110
+ "#{self.class}#update(command -> #{command.inspect}) session: #{sess_str} text: #{text}"
111
+ end
112
+ Fatty.debug(msg, tag: :session)
128
113
  end
129
114
 
130
- private
131
-
132
- def update_key(_ev)
133
- []
115
+ def normalize_action_result(result)
116
+ Command.normalize_list(result)
134
117
  end
135
118
 
136
- def update_cmd(_name, _payload)
137
- []
119
+ def alert_cmd(role, text, ev: nil)
120
+ details =
121
+ if ev
122
+ {
123
+ key: ev.key,
124
+ ctrl: ev.respond_to?(:ctrl?) ? ev.ctrl? : ev.ctrl,
125
+ meta: ev.respond_to?(:meta?) ? ev.meta? : ev.meta,
126
+ shift: ev.respond_to?(:shift?) ? ev.shift? : ev.shift,
127
+ text: ev.text
128
+ }
129
+ else
130
+ {}
131
+ end
132
+ alert = Alert.new(text: text, role: role, details: details)
133
+ Command.session(:alert, :show, alert: alert)
138
134
  end
139
135
 
140
- def match_all_query_terms?(haystack, query)
141
- Fatty::Search.match_all_terms?(haystack, query)
136
+ def default_id
137
+ self.class.name
138
+ .delete_prefix("Fatty::")
139
+ .gsub(/([a-z])([A-Z])/, '\1_\2')
140
+ .downcase
141
+ .then { |name| :"#{name}_#{object_id}" }
142
142
  end
143
143
 
144
- def safely_close_window(win)
145
- return unless win
146
-
147
- begin
148
- win.erase
149
- rescue RuntimeError => e
150
- raise unless closed_window_error?(e)
151
- end
152
-
153
- begin
154
- win.noutrefresh if win.respond_to?(:noutrefresh)
155
- rescue RuntimeError => e
156
- raise unless closed_window_error?(e)
157
- end
144
+ def resolve_action(ev)
145
+ return [nil, []] unless keymap
158
146
 
159
- begin
160
- win.close
161
- rescue RuntimeError => e
162
- raise unless closed_window_error?(e)
163
- end
147
+ keymap.resolve_action(ev, contexts: keymap_contexts)
148
+ end
164
149
 
165
- nil
150
+ # Subclasses can override to vary contexts dynamically
151
+ # (paging/isearch/popup/etc). Must return an Array of symbols in
152
+ # precedence order.
153
+ def keymap_contexts
154
+ [:input]
166
155
  end
167
156
 
168
- def closed_window_error?(error)
169
- message = error.message
170
- message.include?("closed window") || message.include?("already closed window")
157
+ # Subclasses override this to react to resolved actions.
158
+ def apply_action(_action, _args, event:)
159
+ []
171
160
  end
172
161
  end
173
162
  end
163
+
164
+ require_relative "session/alert_session"
165
+ require_relative "session/status_session"
166
+ require_relative "session/modal_session"
167
+ require_relative "session/popup_session"
168
+ require_relative "session/search_session"
169
+ require_relative "session/isearch_session"
170
+ require_relative "session/output_session"
171
+ require_relative "session/shell_session"
172
+ require_relative 'session/prompt_session'
173
+ require_relative 'session/keytest_session'
@@ -3,23 +3,26 @@
3
3
  module Fatty
4
4
  class Terminal
5
5
  class PopupOwner
6
- attr_reader :on_result, :on_cancel
6
+ attr_reader :on_result, :on_cancel, :on_change
7
7
 
8
- def initialize(on_result: nil, on_cancel: nil)
8
+ def initialize(on_result: nil, on_cancel: nil, on_change: nil)
9
9
  @on_result = on_result
10
10
  @on_cancel = on_cancel
11
+ @on_change = on_change
11
12
  end
12
13
 
13
- def update(msg)
14
- _cmd, name, payload = msg
14
+ def update(command)
15
+ result =
16
+ case command.action
17
+ when :popup_result, :prompt_result
18
+ on_result&.call(command.payload)
19
+ when :popup_cancelled, :prompt_cancelled
20
+ on_cancel&.call
21
+ when :popup_changed
22
+ on_change&.call(command.payload)
23
+ end
15
24
 
16
- case name
17
- when :popup_result, :prompt_result
18
- on_result&.call(payload)
19
- when :popup_cancelled, :prompt_cancelled
20
- on_cancel&.call
21
- end
22
- []
25
+ Command.normalize_list(result)
23
26
  end
24
27
  end
25
28
  end