Class: Chamomile::CommandPalette

Inherits:
Object
  • Object
show all
Defined in:
lib/chamomile/components/command_palette.rb

Overview

A fuzzy-search command palette overlay. Renders as a centered modal over existing content.

Usage:

@palette = Chamomile::CommandPalette.new(
items: [
  { label: "Run migrations", action: :run_migrate, key: "db:migrate" },
  { label: "Start server", action: :server_start, key: "server" },
]
)
# In update:
when KeyMsg
if @palette.visible?
  result = @palette.handle_key(msg)
  return result[:action] if result
end

Defined Under Namespace

Classes: Item

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(items:, placeholder: "Type to filter...") ⇒ CommandPalette

Returns a new instance of CommandPalette.



25
26
27
28
29
30
31
# File 'lib/chamomile/components/command_palette.rb', line 25

def initialize(items:, placeholder: "Type to filter...")
  @all_items   = items.map { |i| Item.new(**i) }
  @placeholder = placeholder
  @query       = ""
  @cursor      = 0
  @visible     = false
end

Instance Attribute Details

#cursorObject (readonly)

Returns the value of attribute cursor.



23
24
25
# File 'lib/chamomile/components/command_palette.rb', line 23

def cursor
  @cursor
end

#queryObject (readonly)

Returns the value of attribute query.



23
24
25
# File 'lib/chamomile/components/command_palette.rb', line 23

def query
  @query
end

Instance Method Details

#filtered_itemsObject



105
106
107
108
109
110
111
112
# File 'lib/chamomile/components/command_palette.rb', line 105

def filtered_items
  return @all_items if @query.empty?

  q = @query.downcase
  @all_items.select do |item|
    item.label.downcase.include?(q) || item.key.to_s.downcase.include?(q)
  end
end

#handle_key(msg) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/chamomile/components/command_palette.rb', line 45

def handle_key(msg)
  return nil unless @visible

  case msg.key
  when :escape
    hide
    nil
  when :enter
    selected = filtered_items[@cursor]
    hide
    selected
  when :up, "k"
    @cursor = [@cursor - 1, 0].max
    nil
  when :down, "j"
    max = filtered_items.size - 1
    @cursor = (@cursor + 1).clamp(0, [max, 0].max)
    nil
  when :backspace
    @query = @query[0..-2] || ""
    @cursor = 0
    nil
  else
    if msg.key.is_a?(String) && msg.key.length == 1
      @query += msg.key
      @cursor = 0
    end
    nil
  end
end

#hideObject



39
40
41
# File 'lib/chamomile/components/command_palette.rb', line 39

def hide
  @visible = false
end

#render(width:, height:) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/chamomile/components/command_palette.rb', line 76

def render(width:, height:)
  return "" unless @visible

  items = filtered_items
  modal_width  = [width - 8, 60].min
  modal_height = [items.size + 4, height - 4, 16].min

  lines = []
  query_display = @query.empty? ? "\e[38;5;240m#{@placeholder}\e[0m" : @query
  lines << "  > #{query_display}"
  lines << ("\u2500" * (modal_width - 2))

  visible_items = items.first(modal_height - 4)
  visible_items.each_with_index do |item, i|
    prefix = i == @cursor ? "\u25b6 " : "  "
    line   = "#{prefix}#{item.label}"
    lines << (i == @cursor ? "\e[7m#{line}\e[0m" : line)
  end

  lines << ("\u2500" * (modal_width - 2))
  lines << "\e[38;5;240m  \u2191\u2193 navigate  Enter select  Esc cancel\e[0m"

  box_lines = lines.map { |l| "\u2502 #{l.ljust(modal_width - 4)} \u2502" }
  top    = "#{"\u2500" * (modal_width - 2)}"
  bottom = "#{"\u2500" * (modal_width - 2)}"

  ([top] + box_lines + [bottom]).join("\n")
end

#showObject



33
34
35
36
37
# File 'lib/chamomile/components/command_palette.rb', line 33

def show
  @visible = true
  @query   = ""
  @cursor  = 0
end

#visible?Boolean

Returns:

  • (Boolean)


43
# File 'lib/chamomile/components/command_palette.rb', line 43

def visible? = @visible