fantasy-cli 1.2.6

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 (112) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +456 -0
  4. data/bin/gsd +8 -0
  5. data/bin/gsd-core-darwin-amd64 +0 -0
  6. data/bin/gsd-core-darwin-arm64 +0 -0
  7. data/bin/gsd-core-linux-amd64 +0 -0
  8. data/bin/gsd-core-linux-arm64 +0 -0
  9. data/bin/gsd-core-windows-amd64.exe +0 -0
  10. data/bin/gsd-core-windows-arm64.exe +0 -0
  11. data/bin/gsd-core.exe +0 -0
  12. data/lib/gsd/agents/coordinator.rb +195 -0
  13. data/lib/gsd/agents/task_manager.rb +158 -0
  14. data/lib/gsd/agents/worker.rb +162 -0
  15. data/lib/gsd/agents.rb +30 -0
  16. data/lib/gsd/ai/chat.rb +486 -0
  17. data/lib/gsd/ai/cli.rb +248 -0
  18. data/lib/gsd/ai/command_parser.rb +97 -0
  19. data/lib/gsd/ai/commands/base.rb +42 -0
  20. data/lib/gsd/ai/commands/clear.rb +20 -0
  21. data/lib/gsd/ai/commands/context.rb +30 -0
  22. data/lib/gsd/ai/commands/cost.rb +30 -0
  23. data/lib/gsd/ai/commands/export.rb +42 -0
  24. data/lib/gsd/ai/commands/help.rb +61 -0
  25. data/lib/gsd/ai/commands/model.rb +67 -0
  26. data/lib/gsd/ai/commands/reset.rb +22 -0
  27. data/lib/gsd/ai/config.rb +256 -0
  28. data/lib/gsd/ai/context.rb +324 -0
  29. data/lib/gsd/ai/cost_tracker.rb +361 -0
  30. data/lib/gsd/ai/git_context.rb +169 -0
  31. data/lib/gsd/ai/history.rb +384 -0
  32. data/lib/gsd/ai/providers/anthropic.rb +429 -0
  33. data/lib/gsd/ai/providers/base.rb +282 -0
  34. data/lib/gsd/ai/providers/lmstudio.rb +279 -0
  35. data/lib/gsd/ai/providers/ollama.rb +336 -0
  36. data/lib/gsd/ai/providers/openai.rb +396 -0
  37. data/lib/gsd/ai/providers/openrouter.rb +429 -0
  38. data/lib/gsd/ai/reference_resolver.rb +225 -0
  39. data/lib/gsd/ai/repl.rb +349 -0
  40. data/lib/gsd/ai/streaming.rb +438 -0
  41. data/lib/gsd/ai/ui.rb +429 -0
  42. data/lib/gsd/buddy/cli.rb +284 -0
  43. data/lib/gsd/buddy/gacha.rb +148 -0
  44. data/lib/gsd/buddy/renderer.rb +108 -0
  45. data/lib/gsd/buddy/species.rb +190 -0
  46. data/lib/gsd/buddy/stats.rb +156 -0
  47. data/lib/gsd/buddy.rb +28 -0
  48. data/lib/gsd/cli.rb +455 -0
  49. data/lib/gsd/commands.rb +198 -0
  50. data/lib/gsd/config.rb +183 -0
  51. data/lib/gsd/error.rb +188 -0
  52. data/lib/gsd/frontmatter.rb +123 -0
  53. data/lib/gsd/go/bridge.rb +173 -0
  54. data/lib/gsd/history.rb +76 -0
  55. data/lib/gsd/milestone.rb +75 -0
  56. data/lib/gsd/output.rb +184 -0
  57. data/lib/gsd/phase.rb +102 -0
  58. data/lib/gsd/plugins/base.rb +92 -0
  59. data/lib/gsd/plugins/cli.rb +330 -0
  60. data/lib/gsd/plugins/config.rb +164 -0
  61. data/lib/gsd/plugins/hooks.rb +132 -0
  62. data/lib/gsd/plugins/installer.rb +158 -0
  63. data/lib/gsd/plugins/loader.rb +122 -0
  64. data/lib/gsd/plugins/manager.rb +187 -0
  65. data/lib/gsd/plugins/marketplace.rb +142 -0
  66. data/lib/gsd/plugins/sandbox.rb +114 -0
  67. data/lib/gsd/plugins/search.rb +131 -0
  68. data/lib/gsd/plugins/validator.rb +157 -0
  69. data/lib/gsd/plugins.rb +48 -0
  70. data/lib/gsd/profile.rb +127 -0
  71. data/lib/gsd/research.rb +85 -0
  72. data/lib/gsd/roadmap.rb +90 -0
  73. data/lib/gsd/skills/bundled/commit.md +58 -0
  74. data/lib/gsd/skills/bundled/debug.md +28 -0
  75. data/lib/gsd/skills/bundled/explain.md +41 -0
  76. data/lib/gsd/skills/bundled/plan.md +42 -0
  77. data/lib/gsd/skills/bundled/verify.md +26 -0
  78. data/lib/gsd/skills/loader.rb +189 -0
  79. data/lib/gsd/state.rb +102 -0
  80. data/lib/gsd/template.rb +106 -0
  81. data/lib/gsd/tools/ask_user_question.rb +179 -0
  82. data/lib/gsd/tools/base.rb +204 -0
  83. data/lib/gsd/tools/bash.rb +246 -0
  84. data/lib/gsd/tools/file_edit.rb +297 -0
  85. data/lib/gsd/tools/file_read.rb +199 -0
  86. data/lib/gsd/tools/file_write.rb +153 -0
  87. data/lib/gsd/tools/glob.rb +202 -0
  88. data/lib/gsd/tools/grep.rb +227 -0
  89. data/lib/gsd/tools/gsd_frontmatter.rb +165 -0
  90. data/lib/gsd/tools/gsd_phase.rb +140 -0
  91. data/lib/gsd/tools/gsd_roadmap.rb +108 -0
  92. data/lib/gsd/tools/gsd_state.rb +143 -0
  93. data/lib/gsd/tools/gsd_template.rb +157 -0
  94. data/lib/gsd/tools/gsd_verify.rb +159 -0
  95. data/lib/gsd/tools/registry.rb +103 -0
  96. data/lib/gsd/tools/task.rb +235 -0
  97. data/lib/gsd/tools/todo_write.rb +290 -0
  98. data/lib/gsd/tools/web.rb +260 -0
  99. data/lib/gsd/tui/app.rb +366 -0
  100. data/lib/gsd/tui/auto_complete.rb +79 -0
  101. data/lib/gsd/tui/colors.rb +111 -0
  102. data/lib/gsd/tui/command_palette.rb +126 -0
  103. data/lib/gsd/tui/header.rb +38 -0
  104. data/lib/gsd/tui/input_box.rb +199 -0
  105. data/lib/gsd/tui/spinner.rb +40 -0
  106. data/lib/gsd/tui/status_bar.rb +51 -0
  107. data/lib/gsd/tui.rb +17 -0
  108. data/lib/gsd/validator.rb +216 -0
  109. data/lib/gsd/verify.rb +175 -0
  110. data/lib/gsd/version.rb +5 -0
  111. data/lib/gsd/workstream.rb +91 -0
  112. metadata +231 -0
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gsd
4
+ module TUI
5
+ # Colors - ANSI Color Constants & Themes
6
+ module Colors
7
+ RESET = "\e[0m"
8
+ CLEAR_SCREEN = "\e[2J"
9
+ HOME = "\e[H"
10
+ CURSOR_HIDE = "\e[?25l"
11
+ CURSOR_SHOW = "\e[?25h"
12
+
13
+ # Standard
14
+ BLACK = "\e[30m"
15
+ RED = "\e[31m"
16
+ GREEN = "\e[32m"
17
+ YELLOW = "\e[33m"
18
+ BLUE = "\e[34m"
19
+ MAGENTA = "\e[35m"
20
+ CYAN = "\e[36m"
21
+ WHITE = "\e[37m"
22
+
23
+ # Bright
24
+ BRIGHT_BLACK = "\e[90m"
25
+ BRIGHT_RED = "\e[91m"
26
+ BRIGHT_GREEN = "\e[92m"
27
+ BRIGHT_YELLOW = "\e[93m"
28
+ BRIGHT_BLUE = "\e[94m"
29
+ BRIGHT_MAGENTA = "\e[95m"
30
+ BRIGHT_CYAN = "\e[96m"
31
+ BRIGHT_WHITE = "\e[97m"
32
+
33
+ # Backgrounds
34
+ BG_BLACK = "\e[40m"
35
+ BG_RED = "\e[41m"
36
+ BG_GREEN = "\e[42m"
37
+ BG_YELLOW = "\e[43m"
38
+ BG_BLUE = "\e[44m"
39
+ BG_MAGENTA = "\e[45m"
40
+ BG_CYAN = "\e[46m"
41
+ BG_WHITE = "\e[47m"
42
+
43
+ THEMES = {
44
+ fantasy: {
45
+ name: 'Fantasy',
46
+ accent: BRIGHT_MAGENTA,
47
+ text: WHITE,
48
+ dim: BRIGHT_BLACK,
49
+ success: BRIGHT_GREEN,
50
+ warning: BRIGHT_YELLOW,
51
+ error: BRIGHT_RED,
52
+ bg: BG_BLACK,
53
+ bg_accent: BG_MAGENTA
54
+ },
55
+ kilo: {
56
+ name: 'Kilo',
57
+ accent: BRIGHT_YELLOW,
58
+ text: WHITE,
59
+ dim: BRIGHT_BLACK,
60
+ success: BRIGHT_GREEN,
61
+ warning: BRIGHT_YELLOW,
62
+ error: BRIGHT_RED,
63
+ bg: BG_BLACK,
64
+ bg_accent: BG_YELLOW
65
+ },
66
+ dark: {
67
+ name: 'Dark',
68
+ accent: BRIGHT_CYAN,
69
+ text: WHITE,
70
+ dim: BRIGHT_BLACK,
71
+ success: BRIGHT_GREEN,
72
+ warning: BRIGHT_YELLOW,
73
+ error: BRIGHT_RED,
74
+ bg: BG_BLACK,
75
+ bg_accent: BG_CYAN
76
+ },
77
+ light: {
78
+ name: 'Light',
79
+ accent: BLUE,
80
+ text: BLACK,
81
+ dim: BRIGHT_BLACK,
82
+ success: GREEN,
83
+ warning: YELLOW,
84
+ error: RED,
85
+ bg: BG_WHITE,
86
+ bg_accent: BLUE
87
+ },
88
+ nord: {
89
+ name: 'Nord',
90
+ accent: BRIGHT_BLUE,
91
+ text: WHITE,
92
+ dim: BRIGHT_BLACK,
93
+ success: BRIGHT_GREEN,
94
+ warning: BRIGHT_YELLOW,
95
+ error: BRIGHT_RED,
96
+ bg: BG_BLACK,
97
+ bg_accent: BG_BLUE
98
+ }
99
+ }.freeze
100
+
101
+ def self.theme(name = :fantasy)
102
+ THEMES[name] || THEMES[:fantasy]
103
+ end
104
+
105
+ # Returns a colored string helper
106
+ def self.colorize(text, color_code)
107
+ "#{color_code}#{text}#{RESET}"
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gsd/tui/colors'
4
+
5
+ module Gsd
6
+ module TUI
7
+ class CommandPalette
8
+ COMMANDS = [
9
+ { name: 'ai chat', icon: '🤖', desc: 'Open AI chat interface', action: :ai },
10
+ { name: 'plugins list', icon: '📦', desc: 'List installed plugins', action: :plugins_list },
11
+ { name: 'theme: fantasy', icon: '✨', desc: 'Switch to fantasy theme', action: :theme_fantasy },
12
+ { name: 'theme: kilo', icon: '💛', desc: 'Switch to kilo theme', action: :theme_kilo },
13
+ { name: 'theme: dark', icon: '🌑', desc: 'Switch to dark theme', action: :theme_dark },
14
+ { name: 'theme: light', icon: '☀️', desc: 'Switch to light theme', action: :theme_light },
15
+ { name: 'theme: nord', icon: '❄️', desc: 'Switch to nord theme', action: :theme_nord },
16
+ { name: 'quit', icon: '👋', desc: 'Exit Fantasy CLI', action: :quit }
17
+ ].freeze
18
+
19
+ def initialize
20
+ @visible = false
21
+ @filter = String.new
22
+ @selected_index = 0
23
+ end
24
+
25
+ def show
26
+ @visible = true
27
+ @filter = String.new
28
+ @selected_index = 0
29
+ end
30
+
31
+ def hide
32
+ @visible = false
33
+ @filter = String.new
34
+ end
35
+
36
+ def visible?
37
+ @visible
38
+ end
39
+
40
+ def add_char(char)
41
+ @filter += char
42
+ @selected_index = 0
43
+ end
44
+
45
+ def backspace
46
+ @filter = @filter[0...-1]
47
+ @selected_index = 0
48
+ end
49
+
50
+ def next
51
+ return if filtered_commands.empty?
52
+
53
+ @selected_index = (@selected_index + 1) % filtered_commands.length
54
+ end
55
+
56
+ def prev
57
+ return if filtered_commands.empty?
58
+
59
+ @selected_index = (@selected_index - 1) % filtered_commands.length
60
+ @selected_index = filtered_commands.length - 1 if @selected_index.negative?
61
+ end
62
+
63
+ def filtered_commands
64
+ if @filter.empty?
65
+ COMMANDS
66
+ else
67
+ COMMANDS.select { |cmd| cmd[:name].include?(@filter.downcase) }
68
+ end
69
+ end
70
+
71
+ def execute
72
+ return nil if filtered_commands.empty?
73
+
74
+ cmd = filtered_commands[@selected_index]
75
+ hide
76
+ cmd[:action]
77
+ end
78
+
79
+ def render(width: 60)
80
+ return '' unless @visible
81
+
82
+ t = Colors.theme
83
+ accent = t[:accent]
84
+ bg = t[:bg_accent]
85
+ white = t[:text]
86
+ dim = t[:dim]
87
+ reset = Colors::RESET
88
+
89
+ lines = []
90
+ # Top border with rounded corners
91
+ lines << "#{accent}╭#{'─' * (width + 2)}╮#{reset}"
92
+
93
+ # Title
94
+ title = ' 🔍 Command Palette '
95
+ padding = (width - title.length) / 2
96
+ lines << "#{accent}│#{reset}#{' ' * padding}#{accent}#{title}#{reset}#{' ' * (width - padding - title.length)}#{accent}│#{reset}"
97
+
98
+ # Filter input
99
+ filter_text = " > #{@filter}█"
100
+ lines << "#{accent}│#{reset}#{filter_text.ljust(width + 2)}#{accent}│#{reset}"
101
+
102
+ # Separator
103
+ lines << "#{accent}├#{'─' * (width + 2)}┤#{reset}"
104
+
105
+ # Commands
106
+ filtered_commands.first(6).each_with_index do |cmd, i|
107
+ lines << if i == @selected_index
108
+ "#{accent}│#{reset}#{bg} #{cmd[:icon]} #{cmd[:name].ljust(14)} #{cmd[:desc][0...32]} #{reset}#{accent}│#{reset}"
109
+ else
110
+ "#{accent}│#{reset} #{white}#{cmd[:icon]} #{cmd[:name].ljust(14)}#{dim} #{cmd[:desc][0...32]}#{reset} #{accent}│#{reset}"
111
+ end
112
+ end
113
+
114
+ # Fill remaining space
115
+ remaining = 6 - filtered_commands.length
116
+ remaining.times do
117
+ lines << "#{accent}│#{' ' * (width + 2)}#{accent}│#{reset}"
118
+ end
119
+
120
+ # Bottom border
121
+ lines << "#{accent}╰#{'─' * (width + 2)}╯#{reset}"
122
+ lines.join("\n")
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gsd/tui/colors'
4
+
5
+ module Gsd
6
+ module TUI
7
+ class Header
8
+ FANTASY_LOGO = <<~LOGO
9
+ ███████╗ █████╗ ██████╗ ██╗ ██╗██╗
10
+ ██╔════╝██╔══██╗██╔══██╗██║ ██║██║
11
+ █████╗ ███████║██████╔╝██║ ██║██║
12
+ ██╔══╝ ██╔══██║██╔══██╗██║ ██║██║
13
+ ██║ ██║ ██║██║ ██║╚██████╔╝██║
14
+ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝
15
+ LOGO
16
+
17
+ def initialize(style: :pixel)
18
+ @style = style
19
+ end
20
+
21
+ def render
22
+ t = Colors.theme
23
+ accent = t[:accent]
24
+ dim = t[:dim]
25
+ reset = Colors::RESET
26
+
27
+ lines = []
28
+ lines << ''
29
+ lines << "#{accent}#{FANTASY_LOGO.chomp}#{reset}"
30
+ lines << ''
31
+ lines << "#{dim}Terminal User Interface#{reset}"
32
+ lines << "#{dim}v1.2.0#{reset}"
33
+ lines << ''
34
+ lines.join("\n")
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gsd/tui/colors'
4
+
5
+ module Gsd
6
+ module TUI
7
+ # InputBox com suporte a autocomplete para @referências
8
+ class InputBox
9
+ attr_reader :text, :cursor_pos, :placeholder
10
+ attr_accessor :autocomplete_items, :autocomplete_selected, :autocomplete_active
11
+
12
+ MAX_AUTOCOMPLETE_ITEMS = 10
13
+
14
+ def initialize(placeholder: 'Ask anything...', width: 60, cwd: Dir.pwd)
15
+ @text = String.new
16
+ @cursor_pos = 0
17
+ @placeholder = placeholder
18
+ @width = width
19
+ @focused = true
20
+ @cwd = cwd
21
+
22
+ # Autocomplete state
23
+ @autocomplete_active = false
24
+ @autocomplete_items = []
25
+ @autocomplete_selected = 0
26
+ @autocomplete_query = ''
27
+ @autocomplete_cache = nil
28
+ @autocomplete_cache_time = nil
29
+ end
30
+
31
+ def focus
32
+ @focused = true
33
+ end
34
+
35
+ def unfocus
36
+ @focused = false
37
+ end
38
+
39
+ def add_char(char)
40
+ @text.insert(@cursor_pos, char)
41
+ @cursor_pos += 1
42
+ check_autocomplete_trigger
43
+ end
44
+
45
+ def backspace
46
+ return if @cursor_pos.zero?
47
+
48
+ @text.slice!(@cursor_pos - 1)
49
+ @cursor_pos -= 1
50
+ check_autocomplete_trigger
51
+ end
52
+
53
+ def move_left
54
+ @cursor_pos = [@cursor_pos - 1, 0].max
55
+ end
56
+
57
+ def move_right
58
+ @cursor_pos = [@cursor_pos + 1, @text.length].min
59
+ end
60
+
61
+ def clear
62
+ @text = String.new
63
+ @cursor_pos = 0
64
+ end
65
+
66
+ def submit
67
+ result = @text.dup
68
+ clear
69
+ result
70
+ end
71
+
72
+ def render
73
+ t = Colors.theme
74
+ accent = t[:accent]
75
+ dim = t[:dim]
76
+ white = t[:text]
77
+ reset = Colors::RESET
78
+
79
+ top_border = "#{accent}┌#{'─' * @width}┐#{reset}"
80
+ bottom_border = "#{accent}└#{'─' * @width}┘#{reset}"
81
+
82
+ content = build_content(accent, dim, white, reset)
83
+
84
+ clean_len = content.gsub(/\e\[\d+m/, '').length
85
+ padding = ' ' * [@width - clean_len, 0].max
86
+
87
+ lines = []
88
+ lines << top_border
89
+ lines << "#{accent}│#{reset}#{content}#{padding}#{accent}│#{reset}"
90
+ lines << bottom_border
91
+
92
+ # Adiciona autocomplete se ativo
93
+ if @autocomplete_active && @autocomplete_items.any?
94
+ lines << render_autocomplete(accent, dim, white, reset)
95
+ end
96
+
97
+ lines.join("\n")
98
+ end
99
+
100
+ private
101
+
102
+ def check_autocomplete_trigger
103
+ # Verifica se estamos digitando após @
104
+ before = @text[0...@cursor_pos]
105
+
106
+ if before.include?('@')
107
+ at_pos = before.rindex('@')
108
+ query = before[(at_pos + 1)..]
109
+
110
+ # Só ativa se não tiver espaço na query
111
+ if query && !query.include?(' ')
112
+ @autocomplete_query = query
113
+ @autocomplete_active = true
114
+ update_autocomplete_items
115
+ else
116
+ close_autocomplete
117
+ end
118
+ else
119
+ close_autocomplete
120
+ end
121
+ end
122
+
123
+ def update_autocomplete_items
124
+ items = collect_file_items(@autocomplete_query)
125
+ @autocomplete_items = items.first(MAX_AUTOCOMPLETE_ITEMS)
126
+ @autocomplete_selected = 0
127
+ end
128
+
129
+ def collect_file_items(query)
130
+ return [] unless File.directory?(@cwd)
131
+
132
+ # Cache por 5 segundos
133
+ cache_key = [@cwd, query].join('|')
134
+ if @autocomplete_cache == cache_key && @autocomplete_cache_time && (Time.now - @autocomplete_cache_time) < 5
135
+ return @autocomplete_cached_items || []
136
+ end
137
+
138
+ pattern = File.join(@cwd, '*')
139
+ entries = Dir.glob(pattern, File::FNM_DOTMATCH)
140
+ .reject { |e| File.basename(e).start_with?('.') }
141
+ .map do |path|
142
+ name = File.basename(path)
143
+ type = File.directory?(path) ? :directory : :file
144
+ { name: name, type: type, path: path }
145
+ end
146
+
147
+ # Filtra por query se não vazia
148
+ if query && !query.empty?
149
+ entries = entries.select { |e| e[:name].downcase.include?(query.downcase) }
150
+ end
151
+
152
+ # Ordena: diretórios primeiro, depois arquivos
153
+ entries.sort_by { |e| [e[:type] == :directory ? 0 : 1, e[:name]] }
154
+
155
+ @autocomplete_cache = cache_key
156
+ @autocomplete_cache_time = Time.now
157
+ @autocomplete_cached_items = entries
158
+
159
+ entries
160
+ rescue
161
+ []
162
+ end
163
+
164
+ def render_autocomplete(accent, dim, white, reset)
165
+ return '' if @autocomplete_items.empty?
166
+
167
+ lines = []
168
+ max_display = [MAX_AUTOCOMPLETE_ITEMS, @autocomplete_items.length].min
169
+
170
+ max_display.times do |i|
171
+ item = @autocomplete_items[i]
172
+ prefix = item[:type] == :directory ? '📁' : '📄'
173
+ name = item[:name]
174
+
175
+ if i == @autocomplete_selected
176
+ lines << " #{accent}> #{prefix} #{name}#{reset}"
177
+ else
178
+ lines << " #{dim} #{prefix} #{name}#{reset}"
179
+ end
180
+ end
181
+
182
+ lines.join("\n")
183
+ end
184
+
185
+ def build_content(accent, dim, white, reset)
186
+ prompt = "#{accent}❯#{reset} "
187
+ if @text.empty?
188
+ "#{prompt}#{dim}#{@placeholder}#{reset}"
189
+ else
190
+ before = @text[0...@cursor_pos] || ''
191
+ cursor_char = @text[@cursor_pos] || ' '
192
+ after = @text[(@cursor_pos + 1)..] || ''
193
+
194
+ "#{prompt}#{white}#{before}#{reset}#{accent}#{cursor_char}#{reset}#{white}#{after}"
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gsd/tui/colors'
4
+
5
+ module Gsd
6
+ module TUI
7
+ class Spinner
8
+ FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].freeze
9
+
10
+ def initialize
11
+ @frame_index = 0
12
+ @active = false
13
+ end
14
+
15
+ def start
16
+ @active = true
17
+ end
18
+
19
+ def stop
20
+ @active = false
21
+ @frame_index = 0
22
+ end
23
+
24
+ def active?
25
+ @active
26
+ end
27
+
28
+ def render
29
+ return '' unless @active
30
+
31
+ t = Colors.theme
32
+ "#{t[:accent]}#{FRAMES[@frame_index]}#{Colors::RESET} "
33
+ end
34
+
35
+ def next_frame
36
+ @frame_index = (@frame_index + 1) % FRAMES.length
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gsd/tui/colors'
4
+
5
+ module Gsd
6
+ module TUI
7
+ class StatusBar
8
+ def initialize
9
+ @mode = 'NORMAL'
10
+ @branch = 'master'
11
+ end
12
+
13
+ def update_mode(mode)
14
+ @mode = mode
15
+ end
16
+
17
+ def update_branch(branch)
18
+ @branch = branch
19
+ end
20
+
21
+ def render(width: 80)
22
+ t = Colors.theme
23
+ dim = t[:dim]
24
+ accent = t[:accent]
25
+ white = t[:text]
26
+ bg = t[:bg]
27
+ bg_accent = t[:bg_accent]
28
+ reset = Colors::RESET
29
+
30
+ time = Time.now.strftime('%H:%M:%S')
31
+
32
+ # Left side: Branch and Mode
33
+ left = " #{dim}~/#{@branch}:#{accent}#{@branch}#{reset} "
34
+
35
+ # Right side: Time
36
+ right = " #{dim}#{time}#{reset} "
37
+
38
+ # Center: Shortcuts
39
+ center = "#{dim}tab#{reset} #{white}agents#{reset} #{dim}ctrl+p#{reset} #{white}commands#{reset} #{dim}ctrl+q#{reset} #{white}quit#{reset}"
40
+
41
+ # Calculate padding
42
+ clean_center = center.gsub(/\e\[\d+m/, '').length
43
+ padding = width - left.length - right.length - clean_center - 2
44
+ padding = [padding, 0].max
45
+
46
+ # Status bar line
47
+ "#{bg_accent}#{white} #{@mode} #{reset}#{bg}#{left}#{' ' * padding}#{center} #{right}#{reset}"
48
+ end
49
+ end
50
+ end
51
+ end
data/lib/gsd/tui.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Fantasy CLI - TUI Module
4
+
5
+ module Gsd
6
+ module TUI
7
+ end
8
+ end
9
+
10
+ require 'gsd/tui/colors'
11
+ require 'gsd/tui/header'
12
+ require 'gsd/tui/input_box'
13
+ require 'gsd/tui/status_bar'
14
+ require 'gsd/tui/auto_complete'
15
+ require 'gsd/tui/command_palette'
16
+ require 'gsd/tui/spinner'
17
+ require 'gsd/tui/app'