1+ /*
2+ * Copyright 2025 Lambda
3+ *
4+ * This program is free software: you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation, either version 3 of the License, or
7+ * (at your option) any later version.
8+ *
9+ * This program is distributed in the hope that it will be useful,
10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+ * GNU General Public License for more details.
13+ *
14+ * You should have received a copy of the GNU General Public License
15+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
16+ */
17+
18+ package com.lambda.gui.components
19+
20+ import com.lambda.command.CommandRegistry
21+ import com.lambda.config.AbstractSetting
22+ import com.lambda.config.Configuration
23+ import com.lambda.gui.dsl.ImGuiBuilder
24+ import com.lambda.module.Module
25+ import com.lambda.module.ModuleRegistry
26+ import com.lambda.util.StringUtils.findSimilarStrings
27+ import imgui.ImGui
28+ import imgui.flag.ImGuiInputTextFlags
29+ import imgui.flag.ImGuiWindowFlags
30+ import imgui.type.ImString
31+
32+ object QuickSearch {
33+ private val searchInput = ImString (256 )
34+ private var isOpen = false
35+ private var shouldFocus = false
36+ private val maxResults = 50
37+ private val searchThreshold = 3
38+
39+ data class SearchResult (
40+ val name : String ,
41+ val type : SearchResultType ,
42+ val description : String = " " ,
43+ val action : () -> Unit
44+ )
45+
46+ enum class SearchResultType (val displayName : String ) {
47+ MODULE (" Module" ),
48+ SETTING (" Setting" ),
49+ COMMAND (" Command" )
50+ }
51+
52+ fun open () {
53+ isOpen = true
54+ shouldFocus = true
55+ searchInput.clear()
56+ }
57+
58+ fun close () {
59+ isOpen = false
60+ shouldFocus = false
61+ }
62+
63+ fun toggle () {
64+ if (isOpen) close() else open()
65+ }
66+
67+ fun ImGuiBuilder.renderQuickSearch () {
68+ if (! isOpen) return
69+
70+ ImGui .openPopup(" QuickSearch" )
71+
72+ val flags = ImGuiWindowFlags .AlwaysAutoResize or
73+ ImGuiWindowFlags .NoTitleBar or
74+ ImGuiWindowFlags .NoMove or
75+ ImGuiWindowFlags .NoResize
76+
77+ popupModal(" QuickSearch" , flags) {
78+ // Set popup position to center of screen
79+ val displaySize = ImGui .getIO().displaySize
80+ val popupSize = ImGui .getWindowSize()
81+ ImGui .setWindowPos(
82+ (displaySize.x - popupSize.x) * 0.5f ,
83+ (displaySize.y - popupSize.y) * 0.3f
84+ )
85+
86+ text(" Quick Search" )
87+ separator()
88+
89+ // Search input
90+ if (shouldFocus) {
91+ ImGui .setKeyboardFocusHere()
92+ shouldFocus = false
93+ }
94+
95+ if (inputText(" ##search" , searchInput, ImGuiInputTextFlags .AutoSelectAll )) {
96+ // Input changed, search will be updated below
97+ }
98+
99+ // Handle escape key to close (simplified)
100+ if (ImGui .isKeyPressed(256 )) { // ImGuiKey.Escape
101+ close()
102+ ImGui .closeCurrentPopup()
103+ return @popupModal
104+ }
105+
106+ // Handle enter key (simplified)
107+ if (ImGui .isKeyPressed(257 )) { // ImGuiKey.Enter
108+ val results = performSearch(searchInput.get())
109+ if (results.isNotEmpty()) {
110+ results.first().action()
111+ close()
112+ ImGui .closeCurrentPopup()
113+ return @popupModal
114+ }
115+ }
116+
117+ separator()
118+
119+ // Search results
120+ val query = searchInput.get().trim()
121+ if (query.isNotEmpty()) {
122+ val results = performSearch(query)
123+
124+ if (results.isEmpty()) {
125+ textColored(0.7f , 0.7f , 0.7f , 1.0f , " No results found" )
126+ } else {
127+ text(" Results (${results.size} ):" )
128+ child(" SearchResults" , 400f , 300f , true ) {
129+ results.forEach { result ->
130+ if (selectable(" ${result.name} ##${result.type} " )) {
131+ result.action()
132+ close()
133+ ImGui .closeCurrentPopup()
134+ }
135+
136+ if (ImGui .isItemHovered()) {
137+ lambdaTooltip(" ${result.type.displayName} : ${result.name} \n ${result.description} " )
138+ }
139+
140+ sameLine()
141+ textColored(0.6f , 0.6f , 0.6f , 1.0f , " [${result.type.displayName} ]" )
142+ }
143+ }
144+ }
145+ } else {
146+ textColored(0.7f , 0.7f , 0.7f , 1.0f , " Type to search modules, settings, and commands..." )
147+ }
148+
149+ separator()
150+
151+ if (button(" Close" )) {
152+ close()
153+ ImGui .closeCurrentPopup()
154+ }
155+ }
156+ }
157+
158+ private fun performSearch (query : String ): List <SearchResult > {
159+ if (query.isBlank()) return emptyList()
160+
161+ val results = mutableListOf<SearchResult >()
162+ val lowerQuery = query.lowercase()
163+
164+ // Search modules
165+ searchModules(lowerQuery, results)
166+
167+ // Search commands
168+ searchCommands(lowerQuery, results)
169+
170+ // Search settings
171+ searchSettings(lowerQuery, results)
172+
173+ return results.sortedBy { it.name.lowercase() }.take(maxResults)
174+ }
175+
176+ private fun searchModules (query : String , results : MutableList <SearchResult >) {
177+ // Direct name matches first
178+ ModuleRegistry .modules.forEach { module ->
179+ if (module.name.lowercase().contains(query)) {
180+ results.add(SearchResult (
181+ name = module.name,
182+ type = SearchResultType .MODULE ,
183+ description = module.description,
184+ action = { module.toggle() }
185+ ))
186+ }
187+ }
188+
189+ // Fuzzy matches for modules if not too many direct matches
190+ if (results.count { it.type == SearchResultType .MODULE } < 10 ) {
191+ val moduleNames = ModuleRegistry .modules.map { it.name }.toSet()
192+ val similarNames = findSimilarStrings(query, moduleNames, searchThreshold)
193+
194+ similarNames.forEach { name ->
195+ val module = ModuleRegistry .modules.find { it.name == name }
196+ if (module != null && results.none { it.name == name && it.type == SearchResultType .MODULE }) {
197+ results.add(SearchResult (
198+ name = module.name,
199+ type = SearchResultType .MODULE ,
200+ description = module.description,
201+ action = { module.toggle() }
202+ ))
203+ }
204+ }
205+ }
206+ }
207+
208+ private fun searchCommands (query : String , results : MutableList <SearchResult >) {
209+ CommandRegistry .commands.forEach { command ->
210+ if (command.name.lowercase().contains(query) ||
211+ command.aliases.any { it.lowercase().contains(query) }) {
212+ results.add(SearchResult (
213+ name = command.name,
214+ type = SearchResultType .COMMAND ,
215+ description = command.description,
216+ action = {
217+ // For commands, we could open chat with the command prefix
218+ // but that's complex, so for now just show info
219+ }
220+ ))
221+ }
222+ }
223+ }
224+
225+ private fun searchSettings (query : String , results : MutableList <SearchResult >) {
226+ Configuration .configurations.forEach { config ->
227+ config.configurables.forEach { configurable ->
228+ configurable.settings.forEach { setting ->
229+ if (setting.name.lowercase().contains(query)) {
230+ results.add(SearchResult (
231+ name = " ${configurable.name} .${setting.name} " ,
232+ type = SearchResultType .SETTING ,
233+ description = setting.description,
234+ action = {
235+ // For settings, we could navigate to the setting
236+ // but that's complex for now
237+ }
238+ ))
239+ }
240+ }
241+ }
242+ }
243+ }
244+ }
0 commit comments