feat: redesign launcher to mimic macOS Spotlight style
Co-authored-by: aider (openai/claude-sonnet-4) <aider@aider.chat>
This commit is contained in:
1 parent
504f2fcd1e
commit
7789ab78a9
2 files changed
+153
-118
No files matched your search
+20
-44
@@ -17,16 +17,16 @@ SEARCH_DEBOUNCE_MS = 150
|
||||
TRIGGER_SEARCH_DEBOUNCE_MS = 50
|
||||
CURSOR_POSITION_DELAY_MS = 10
|
||||
SCROLL_PADDING = 10
|
||||
DEFAULT_ITEM_HEIGHT = 60
|
||||
DEFAULT_ITEM_HEIGHT = 68
|
||||
PAGE_NAVIGATION_STEP = 5
|
||||
LAUNCHER_WIDTH = 550
|
||||
LAUNCHER_HEIGHT = 260
|
||||
LAUNCHER_WIDTH = 640
|
||||
LAUNCHER_HEIGHT = 400
|
||||
|
||||
|
||||
class Launcher(Window):
|
||||
"""
|
||||
Main launcher window with search functionality and plugin system.
|
||||
Similar to Albert Launcher interface.
|
||||
Spotlight-style interface.
|
||||
"""
|
||||
|
||||
# Properties
|
||||
@@ -62,7 +62,7 @@ class Launcher(Window):
|
||||
# Current results and selection
|
||||
self.results: List[Result] = []
|
||||
self.selected_index = 0
|
||||
self.max_results = 10
|
||||
self.max_results = 8 # Spotlight typically shows fewer results
|
||||
|
||||
# Trigger system
|
||||
self.triggered_plugin = None # Currently active triggered plugin
|
||||
@@ -84,10 +84,10 @@ class Launcher(Window):
|
||||
)
|
||||
self.add(main_box)
|
||||
|
||||
# Search entry with overlay for trigger indication
|
||||
# Search entry with Spotlight-style large text
|
||||
self.search_entry = Entry(
|
||||
name="launcher-search",
|
||||
placeholder="Type to search...",
|
||||
placeholder="Spotlight Search",
|
||||
h_expand=True,
|
||||
h_align="fill",
|
||||
notify_text=lambda entry, *_: self._on_search_changed(entry),
|
||||
@@ -106,7 +106,7 @@ class Launcher(Window):
|
||||
|
||||
main_box.add(self.header_box)
|
||||
|
||||
# Results container
|
||||
# Results container with increased height for Spotlight-style
|
||||
self.results_scroll = ScrolledWindow(
|
||||
name="launcher-results-scroll",
|
||||
h_scrollbar_policy="never",
|
||||
@@ -644,7 +644,7 @@ class Launcher(Window):
|
||||
if getattr(self, "_initializing", True) or not hasattr(self, "results_box"):
|
||||
return
|
||||
|
||||
# Update input field with trigger indication (Albert-style)
|
||||
# Update input field with trigger indication (Spotlight-style)
|
||||
self._update_input_action_text()
|
||||
|
||||
# Clear existing results
|
||||
@@ -684,7 +684,7 @@ class Launcher(Window):
|
||||
self.results_scroll.hide()
|
||||
|
||||
def _update_input_action_text(self):
|
||||
"""Update the input field with action text (Albert-style)."""
|
||||
"""Update the input field with action text (Spotlight-style)."""
|
||||
# Check if search_entry is initialized
|
||||
if not hasattr(self, "search_entry") or self.search_entry is None:
|
||||
return
|
||||
@@ -692,51 +692,27 @@ class Launcher(Window):
|
||||
# Get the current input text
|
||||
current_text = self.search_entry.get_text()
|
||||
|
||||
# Spotlight-style placeholder text
|
||||
if self.triggered_plugin:
|
||||
if self.results:
|
||||
# Show the first result as action text
|
||||
first_result = self.results[0]
|
||||
action_text = first_result.title
|
||||
|
||||
# For calculator results, show the evaluation
|
||||
if hasattr(first_result, "data") and "result" in first_result.data:
|
||||
action_text = str(first_result.data["result"])
|
||||
|
||||
# Set placeholder to show the action text
|
||||
if action_text and action_text != current_text:
|
||||
self.search_entry.set_placeholder_text(
|
||||
f"{current_text} → {action_text}"
|
||||
)
|
||||
else:
|
||||
self.search_entry.set_placeholder_text(
|
||||
f"[{self.active_trigger.strip()}] searching..."
|
||||
)
|
||||
# Show more minimal placeholder for triggered mode
|
||||
self.search_entry.set_placeholder_text(f"Search {self.active_trigger.strip()}...")
|
||||
else:
|
||||
# In trigger mode but no results yet
|
||||
if current_text == self.active_trigger.strip():
|
||||
# Just the trigger keyword
|
||||
self.search_entry.set_placeholder_text(
|
||||
f"[{self.active_trigger.strip()}] ready - type to search"
|
||||
)
|
||||
self.search_entry.set_placeholder_text(f"Search {self.active_trigger.strip()}...")
|
||||
else:
|
||||
# Searching within trigger
|
||||
self.search_entry.set_placeholder_text(
|
||||
f"[{self.active_trigger.strip()}] searching..."
|
||||
)
|
||||
self.search_entry.set_placeholder_text(f"Searching {self.active_trigger.strip()}...")
|
||||
else:
|
||||
# Not in trigger mode - show trigger help
|
||||
# Not in trigger mode - show Spotlight-style help
|
||||
if current_text == ":":
|
||||
self.search_entry.set_placeholder_text(
|
||||
"Showing all available triggers."
|
||||
)
|
||||
self.search_entry.set_placeholder_text("Available search triggers")
|
||||
elif current_text:
|
||||
self.search_entry.set_placeholder_text(
|
||||
"Type trigger keyword (calc, app, file, system...)"
|
||||
)
|
||||
self.search_entry.set_placeholder_text("Spotlight Search")
|
||||
else:
|
||||
self.search_entry.set_placeholder_text(
|
||||
"Type trigger keyword: calc, app, file, system..."
|
||||
)
|
||||
self.search_entry.set_placeholder_text("Spotlight Search")
|
||||
|
||||
def _clear_results(self):
|
||||
"""Clear all results."""
|
||||
@@ -1116,7 +1092,7 @@ class Launcher(Window):
|
||||
# Try to get actual allocation, fallback to estimation
|
||||
try:
|
||||
allocation = selected_child.get_allocation()
|
||||
item_height = allocation.height if allocation.height > 0 else 60
|
||||
item_height = allocation.height if allocation.height > 0 else 68
|
||||
item_y = allocation.y
|
||||
except:
|
||||
# Fallback to estimation
|
||||
|
||||
+133
-74
@@ -1,176 +1,235 @@
|
||||
#launcher {
|
||||
background: var(--shadow);
|
||||
padding: 12px;
|
||||
border-radius: 34px;
|
||||
min-width: 550px;
|
||||
transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
background: rgba(30, 30, 30, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
padding: 0;
|
||||
border-radius: 12px;
|
||||
min-width: 640px;
|
||||
max-width: 640px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
#header_box {
|
||||
padding: 20px 20px 0 20px;
|
||||
}
|
||||
|
||||
#close-button,
|
||||
#config-button {
|
||||
background-color: var(--surface);
|
||||
border-radius: 40px;
|
||||
padding: 8px;
|
||||
transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
background-color: transparent;
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
#close-button:hover,
|
||||
#close-button:focus,
|
||||
#config-button:hover,
|
||||
#config-button:focus {
|
||||
background-color: var(--surface-bright);
|
||||
border-radius: 16px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#close-button.focused,
|
||||
#config-button.focused {
|
||||
background-color: var(--primary);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 0 0 2px var(--primary-bright);
|
||||
background-color: rgba(0, 122, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.4);
|
||||
}
|
||||
|
||||
#close-button.focused #close-label {
|
||||
color: var(--background);
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
#config-button.focused #config-label {
|
||||
color: var(--background);
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
#close-button:active {
|
||||
background-color: var(--red-dim);
|
||||
border-radius: 40px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#close-label {
|
||||
color: var(--red-dim);
|
||||
font-size: 24px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#close-button:active #close-label {
|
||||
color: var(--shadow);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
#config-button:active {
|
||||
background-color: var(--primary);
|
||||
border-radius: 40px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#config-label {
|
||||
color: var(--primary);
|
||||
font-size: 24px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#config-button:active #config-label {
|
||||
color: var(--shadow);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
#launcher-icon-label {
|
||||
font-size: 32px;
|
||||
padding: 8px;
|
||||
font-size: 20px;
|
||||
padding: 6px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
#launcher-search {
|
||||
font-weight: bold;
|
||||
background-color: var(--surface);
|
||||
color: var(--foreground);
|
||||
border-radius: 16px;
|
||||
padding: 10px;
|
||||
font-weight: 400;
|
||||
font-size: 36px;
|
||||
background-color: transparent;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 12px 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#launcher-search:focus {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#launcher-search selection {
|
||||
color: var(--background);
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
background-color: rgba(0, 122, 255, 0.8);
|
||||
}
|
||||
|
||||
#launcher-results-scroll {
|
||||
margin-top: 10px;
|
||||
border-radius: 16px;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
#launcher-results-scroll scrollbar {
|
||||
border-radius: 10px;
|
||||
background-color: var(--surface);
|
||||
padding: 4px;
|
||||
margin-left: 6px;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#launcher-results-scroll slider {
|
||||
border-radius: 8px;
|
||||
min-width: 10px;
|
||||
min-height: 38px;
|
||||
background-color: var(--primary);
|
||||
#launcher-results-scroll scrollbar slider {
|
||||
border-radius: 2px;
|
||||
min-width: 4px;
|
||||
min-height: 20px;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#launcher-results-scroll scrollbar:hover slider {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
#launcher-results {
|
||||
background: transparent;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#launcher-result-item {
|
||||
border-radius: 40px;
|
||||
padding: 0 16px;
|
||||
animation: loadSlot 0.5s ease;
|
||||
min-height: 52px;
|
||||
transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
margin: 2px 0;
|
||||
min-height: 64px;
|
||||
transition: all 0.15s ease;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@keyframes loadSlot {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#launcher-result-item:focus,
|
||||
#launcher-result-item:selected,
|
||||
#launcher-result-item:hover,
|
||||
#launcher-result-item.selected {
|
||||
border-radius: 16px;
|
||||
background-color: var(--surface);
|
||||
padding-left: 20px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(0, 122, 255, 0.8);
|
||||
padding: 12px 16px;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
#launcher-result-item.selected #result-item-title {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#launcher-result-item.selected #result-item-subtitle {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
#launcher-result-item.selected #result-item-plugin {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
#result-item-main {
|
||||
min-height: 48px;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
#launcher-result-item {
|
||||
min-height: 48px;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
#result-item-icon {
|
||||
min-width: 48px;
|
||||
min-height: 48px;
|
||||
margin-right: 12px;
|
||||
min-width: 56px;
|
||||
min-height: 56px;
|
||||
margin-right: 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
#result-item-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--foreground);
|
||||
margin-top: 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
margin-top: 4px;
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
#result-item-subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--outline);
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
#result-item-plugin {
|
||||
font-size: 10px;
|
||||
color: var(--outline);
|
||||
font-style: italic;
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.7;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-style: normal;
|
||||
margin-bottom: 4px;
|
||||
opacity: 1;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
#network-password-entry {
|
||||
border: 1px solid var(--outline);
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#network-password-entry:focus {
|
||||
border: 1px solid rgba(0, 122, 255, 0.6);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.2);
|
||||
}
|
||||
Reference in new issue
Block a user