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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +456 -0
- data/bin/gsd +8 -0
- data/bin/gsd-core-darwin-amd64 +0 -0
- data/bin/gsd-core-darwin-arm64 +0 -0
- data/bin/gsd-core-linux-amd64 +0 -0
- data/bin/gsd-core-linux-arm64 +0 -0
- data/bin/gsd-core-windows-amd64.exe +0 -0
- data/bin/gsd-core-windows-arm64.exe +0 -0
- data/bin/gsd-core.exe +0 -0
- data/lib/gsd/agents/coordinator.rb +195 -0
- data/lib/gsd/agents/task_manager.rb +158 -0
- data/lib/gsd/agents/worker.rb +162 -0
- data/lib/gsd/agents.rb +30 -0
- data/lib/gsd/ai/chat.rb +486 -0
- data/lib/gsd/ai/cli.rb +248 -0
- data/lib/gsd/ai/command_parser.rb +97 -0
- data/lib/gsd/ai/commands/base.rb +42 -0
- data/lib/gsd/ai/commands/clear.rb +20 -0
- data/lib/gsd/ai/commands/context.rb +30 -0
- data/lib/gsd/ai/commands/cost.rb +30 -0
- data/lib/gsd/ai/commands/export.rb +42 -0
- data/lib/gsd/ai/commands/help.rb +61 -0
- data/lib/gsd/ai/commands/model.rb +67 -0
- data/lib/gsd/ai/commands/reset.rb +22 -0
- data/lib/gsd/ai/config.rb +256 -0
- data/lib/gsd/ai/context.rb +324 -0
- data/lib/gsd/ai/cost_tracker.rb +361 -0
- data/lib/gsd/ai/git_context.rb +169 -0
- data/lib/gsd/ai/history.rb +384 -0
- data/lib/gsd/ai/providers/anthropic.rb +429 -0
- data/lib/gsd/ai/providers/base.rb +282 -0
- data/lib/gsd/ai/providers/lmstudio.rb +279 -0
- data/lib/gsd/ai/providers/ollama.rb +336 -0
- data/lib/gsd/ai/providers/openai.rb +396 -0
- data/lib/gsd/ai/providers/openrouter.rb +429 -0
- data/lib/gsd/ai/reference_resolver.rb +225 -0
- data/lib/gsd/ai/repl.rb +349 -0
- data/lib/gsd/ai/streaming.rb +438 -0
- data/lib/gsd/ai/ui.rb +429 -0
- data/lib/gsd/buddy/cli.rb +284 -0
- data/lib/gsd/buddy/gacha.rb +148 -0
- data/lib/gsd/buddy/renderer.rb +108 -0
- data/lib/gsd/buddy/species.rb +190 -0
- data/lib/gsd/buddy/stats.rb +156 -0
- data/lib/gsd/buddy.rb +28 -0
- data/lib/gsd/cli.rb +455 -0
- data/lib/gsd/commands.rb +198 -0
- data/lib/gsd/config.rb +183 -0
- data/lib/gsd/error.rb +188 -0
- data/lib/gsd/frontmatter.rb +123 -0
- data/lib/gsd/go/bridge.rb +173 -0
- data/lib/gsd/history.rb +76 -0
- data/lib/gsd/milestone.rb +75 -0
- data/lib/gsd/output.rb +184 -0
- data/lib/gsd/phase.rb +102 -0
- data/lib/gsd/plugins/base.rb +92 -0
- data/lib/gsd/plugins/cli.rb +330 -0
- data/lib/gsd/plugins/config.rb +164 -0
- data/lib/gsd/plugins/hooks.rb +132 -0
- data/lib/gsd/plugins/installer.rb +158 -0
- data/lib/gsd/plugins/loader.rb +122 -0
- data/lib/gsd/plugins/manager.rb +187 -0
- data/lib/gsd/plugins/marketplace.rb +142 -0
- data/lib/gsd/plugins/sandbox.rb +114 -0
- data/lib/gsd/plugins/search.rb +131 -0
- data/lib/gsd/plugins/validator.rb +157 -0
- data/lib/gsd/plugins.rb +48 -0
- data/lib/gsd/profile.rb +127 -0
- data/lib/gsd/research.rb +85 -0
- data/lib/gsd/roadmap.rb +90 -0
- data/lib/gsd/skills/bundled/commit.md +58 -0
- data/lib/gsd/skills/bundled/debug.md +28 -0
- data/lib/gsd/skills/bundled/explain.md +41 -0
- data/lib/gsd/skills/bundled/plan.md +42 -0
- data/lib/gsd/skills/bundled/verify.md +26 -0
- data/lib/gsd/skills/loader.rb +189 -0
- data/lib/gsd/state.rb +102 -0
- data/lib/gsd/template.rb +106 -0
- data/lib/gsd/tools/ask_user_question.rb +179 -0
- data/lib/gsd/tools/base.rb +204 -0
- data/lib/gsd/tools/bash.rb +246 -0
- data/lib/gsd/tools/file_edit.rb +297 -0
- data/lib/gsd/tools/file_read.rb +199 -0
- data/lib/gsd/tools/file_write.rb +153 -0
- data/lib/gsd/tools/glob.rb +202 -0
- data/lib/gsd/tools/grep.rb +227 -0
- data/lib/gsd/tools/gsd_frontmatter.rb +165 -0
- data/lib/gsd/tools/gsd_phase.rb +140 -0
- data/lib/gsd/tools/gsd_roadmap.rb +108 -0
- data/lib/gsd/tools/gsd_state.rb +143 -0
- data/lib/gsd/tools/gsd_template.rb +157 -0
- data/lib/gsd/tools/gsd_verify.rb +159 -0
- data/lib/gsd/tools/registry.rb +103 -0
- data/lib/gsd/tools/task.rb +235 -0
- data/lib/gsd/tools/todo_write.rb +290 -0
- data/lib/gsd/tools/web.rb +260 -0
- data/lib/gsd/tui/app.rb +366 -0
- data/lib/gsd/tui/auto_complete.rb +79 -0
- data/lib/gsd/tui/colors.rb +111 -0
- data/lib/gsd/tui/command_palette.rb +126 -0
- data/lib/gsd/tui/header.rb +38 -0
- data/lib/gsd/tui/input_box.rb +199 -0
- data/lib/gsd/tui/spinner.rb +40 -0
- data/lib/gsd/tui/status_bar.rb +51 -0
- data/lib/gsd/tui.rb +17 -0
- data/lib/gsd/validator.rb +216 -0
- data/lib/gsd/verify.rb +175 -0
- data/lib/gsd/version.rb +5 -0
- data/lib/gsd/workstream.rb +91 -0
- 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'
|