Skip to content

Commit 037b901

Browse files
committed
UX improvements for status bar
1 parent b2e07a9 commit 037b901

File tree

7 files changed

+102
-53
lines changed

7 files changed

+102
-53
lines changed

sqlit/domains/query/ui/mixins/query_editing_operators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ def _enter_insert_mode(self: QueryMixinHost) -> None:
461461
self.query_input.read_only = False
462462
self.query_input.focus()
463463
self._update_footer_bindings()
464-
self._update_status_bar()
464+
self._update_vim_mode_visuals()
465465

466466
def action_change_line(self: QueryMixinHost) -> None:
467467
"""Change the current line (cc)."""

sqlit/domains/query/ui/mixins/query_execution.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ def _restore_insert_mode(self: QueryMixinHost) -> None:
325325
self.query_input.read_only = False
326326
self.query_input.focus()
327327
self._update_footer_bindings()
328-
self._update_status_bar()
328+
self._update_vim_mode_visuals()
329329

330330
def action_cancel_query(self: QueryMixinHost) -> None:
331331
"""Cancel the currently running query."""

sqlit/domains/results/ui/mixins/results.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ def sql_value(v: object) -> str:
570570
from sqlit.core.vim import VimMode
571571
self.vim_mode = VimMode.INSERT
572572
self.query_input.read_only = False
573-
self._update_status_bar()
573+
self._update_vim_mode_visuals()
574574
self._update_footer_bindings()
575575

576576
# Stacked results navigation

sqlit/domains/shell/app/main.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,19 @@
112112
border-title-color: $primary;
113113
}
114114

115+
/* Vim mode cursor styling */
116+
#query-area.vim-normal TextArea > .text-area--cursor {
117+
/* Block cursor for NORMAL mode - warm beige */
118+
background: #D8C499;
119+
color: $surface;
120+
}
121+
122+
#query-area.vim-insert TextArea > .text-area--cursor {
123+
/* Cursor for INSERT mode - soft green */
124+
background: #91C58D;
125+
color: $surface;
126+
}
127+
115128

116129
#results-area DataTable {
117130
height: 1fr;

sqlit/domains/shell/app/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@ def compose(self) -> ComposeResult:
401401
yield QueryTextArea(
402402
"",
403403
language="sql",
404+
theme="css",
404405
id="query-input",
405406
read_only=True,
406407
)

sqlit/domains/shell/ui/mixins/ui_navigation.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def action_focus_query(self: UINavigationMixinHost) -> None:
5858
self.vim_mode = VimMode.NORMAL
5959
self.query_input.read_only = True
6060
self.query_input.focus()
61-
self._update_status_bar()
61+
self._update_vim_mode_visuals()
6262

6363
def action_focus_results(self: UINavigationMixinHost) -> None:
6464
"""Focus the Results pane."""
@@ -94,7 +94,7 @@ def action_enter_insert_mode(self: UINavigationMixinHost) -> None:
9494
if self.query_input.has_focus and self.vim_mode == VimMode.NORMAL:
9595
self.vim_mode = VimMode.INSERT
9696
self.query_input.read_only = False
97-
self._update_status_bar()
97+
self._update_vim_mode_visuals()
9898
self._update_footer_bindings()
9999

100100
def action_exit_insert_mode(self: UINavigationMixinHost) -> None:
@@ -105,7 +105,7 @@ def action_exit_insert_mode(self: UINavigationMixinHost) -> None:
105105
self.vim_mode = VimMode.NORMAL
106106
self.query_input.read_only = True
107107
self._hide_autocomplete()
108-
self._update_status_bar()
108+
self._update_vim_mode_visuals()
109109
self._update_footer_bindings()
110110

111111
def action_toggle_explorer(self: UINavigationMixinHost) -> None:
@@ -183,7 +183,7 @@ def on_descendant_focus(self: UINavigationMixinHost, event: Any) -> None:
183183
except Exception:
184184
pass
185185
self._update_footer_bindings()
186-
self._update_status_bar()
186+
self._update_vim_mode_visuals()
187187

188188
def on_descendant_blur(self: UINavigationMixinHost, event: Any) -> None:
189189
"""Handle blur to update section labels."""

sqlit/domains/shell/ui/mixins/ui_status.py

Lines changed: 81 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,36 @@ def set_title(pane: Any, key: str, label: str, *, active: bool) -> None:
113113
set_title(pane_query, "q", "Query", active=active_pane == "query")
114114
set_title(pane_results, "r", "Results", active=active_pane == "results")
115115

116+
def _update_vim_mode_visuals(self: UINavigationMixinHost) -> None:
117+
"""Update all visual indicators based on current vim mode.
118+
119+
This updates:
120+
- Border color on query pane (orange for NORMAL, green for INSERT)
121+
- Cursor color (via CSS class)
122+
- Status bar mode indicator
123+
124+
Only shows vim mode indicators when query pane has focus.
125+
"""
126+
from sqlit.core.vim import VimMode
127+
128+
try:
129+
query_area = self.query_one("#query-area")
130+
has_query_focus = self.query_input.has_focus
131+
except Exception:
132+
return
133+
134+
# Update CSS classes for border and cursor color
135+
# Only show vim mode colors when query pane has focus
136+
query_area.remove_class("vim-normal", "vim-insert")
137+
if has_query_focus:
138+
if self.vim_mode == VimMode.NORMAL:
139+
query_area.add_class("vim-normal")
140+
else:
141+
query_area.add_class("vim-insert")
142+
143+
# Also update the status bar
144+
self._update_status_bar()
145+
116146
def _update_status_bar(self: UINavigationMixinHost) -> None:
117147
"""Update status bar with connection and vim mode info."""
118148
from sqlit.core.vim import VimMode
@@ -132,19 +162,16 @@ def _update_status_bar(self: UINavigationMixinHost) -> None:
132162

133163
connecting_config = getattr(self, "_connecting_config", None)
134164

135-
if getattr(self, "_query_executing", False):
136-
conn_info = ""
137-
elif connecting_config is not None:
165+
if connecting_config is not None:
138166
connect_spinner = getattr(self, "_connect_spinner", None)
139167
spinner = connect_spinner.frame if connect_spinner else SPINNER_FRAMES[0]
140168
source_emoji = connecting_config.get_source_emoji()
141169
conn_info = f"[#FBBF24]{spinner} Connecting to {source_emoji}{connecting_config.name}[/]"
142170
elif getattr(self, "_connection_failed", False):
143171
conn_info = "[#ff6b6b]Connection failed[/]"
144172
elif self.current_config:
145-
display_info = get_connection_display_info(self.current_config)
146173
source_emoji = self.current_config.get_source_emoji()
147-
conn_info = f"[#4ADE80]Connected to {source_emoji}{self.current_config.name}[/] ({display_info})"
174+
conn_info = f"[#4ADE80]Connected to {source_emoji}{self.current_config.name}[/]"
148175
if direct_active:
149176
conn_info += " [dim](direct, not saved)[/]"
150177
else:
@@ -159,21 +186,6 @@ def _update_status_bar(self: UINavigationMixinHost) -> None:
159186
if getattr(self, "_debug_mode", False) or getattr(self, "_debug_idle_scheduler", False):
160187
status_parts.append(f"[bold cyan]{schema_spinner.frame} Indexing...[/]")
161188

162-
# Check if query is executing
163-
query_spinner = getattr(self, "_query_spinner", None)
164-
if query_spinner and query_spinner.running:
165-
import time
166-
167-
from sqlit.shared.core.utils import format_duration_ms
168-
169-
start_time = getattr(self, "_query_start_time", None)
170-
if start_time:
171-
elapsed_ms = (time.perf_counter() - start_time) * 1000
172-
elapsed_str = format_duration_ms(elapsed_ms, always_seconds=True)
173-
status_parts.append(f"[bold yellow]{query_spinner.frame} Executing [{elapsed_str}][/] [dim]^z to cancel[/]")
174-
else:
175-
status_parts.append(f"[bold yellow]{query_spinner.frame} Executing[/] [dim]^z to cancel[/]")
176-
177189
# Check if in a transaction
178190
if getattr(self, "in_transaction", False):
179191
status_parts.append("[bold magenta]⚡ TRANSACTION[/]")
@@ -182,18 +194,23 @@ def _update_status_bar(self: UINavigationMixinHost) -> None:
182194
if status_str:
183195
status_str += " "
184196

185-
# Build left side content
197+
# Build left side content - mode indicator is always preserved
198+
mode_str = ""
199+
mode_plain = ""
186200
try:
187201
if self.query_input.has_focus:
188202
if self.vim_mode == VimMode.NORMAL:
189-
mode_str = f"[bold orange1]-- {self.vim_mode.value} --[/]"
203+
# Warm beige background for NORMAL mode
204+
mode_str = "[bold #1e1e1e on #D8C499] NORMAL [/] "
205+
mode_plain = " NORMAL "
190206
else:
191-
mode_str = f"[dim]-- {self.vim_mode.value} --[/]"
192-
left_content = f"{status_str}{mode_str} {conn_info}"
193-
else:
194-
left_content = f"{status_str}{conn_info}"
207+
# Soft green background for INSERT mode
208+
mode_str = "[bold #1e1e1e on #91C58D] INSERT [/] "
209+
mode_plain = " INSERT "
195210
except Exception:
196-
left_content = f"{status_str}{conn_info}"
211+
pass
212+
213+
left_content = f"{status_str}{mode_str}{conn_info}"
197214

198215
notification = getattr(self, "_last_notification", "")
199216
timestamp = getattr(self, "_last_notification_time", "")
@@ -212,11 +229,43 @@ def _update_status_bar(self: UINavigationMixinHost) -> None:
212229
right_str = launch_str
213230
right_plain = launch_plain
214231

215-
if notification:
216-
# Normal/warning notifications on right side
217-
import re
232+
import re
218233

219-
left_plain = re.sub(r"\[.*?\]", "", left_content)
234+
try:
235+
total_width = self.size.width - 2
236+
except Exception:
237+
total_width = 80
238+
239+
left_plain = re.sub(r"\[.*?\]", "", left_content)
240+
241+
# Build right side content - executing status takes priority over notification
242+
if getattr(self, "_query_executing", False):
243+
query_spinner = getattr(self, "_query_spinner", None)
244+
if query_spinner and query_spinner.running:
245+
import time
246+
247+
from sqlit.shared.core.utils import format_duration_ms
248+
249+
start_time = getattr(self, "_query_start_time", None)
250+
if start_time:
251+
elapsed_ms = (time.perf_counter() - start_time) * 1000
252+
elapsed_str = format_duration_ms(elapsed_ms, always_seconds=True)
253+
right_content = f"[bold yellow]{query_spinner.frame} Executing [{elapsed_str}][/] [dim]^z to cancel[/]"
254+
right_content_plain = f" Executing [{elapsed_str}] ^z to cancel"
255+
else:
256+
right_content = f"[bold yellow]{query_spinner.frame} Executing[/] [dim]^z to cancel[/]"
257+
right_content_plain = " Executing ^z to cancel"
258+
else:
259+
right_content = "[bold yellow]Executing...[/]"
260+
right_content_plain = "Executing..."
261+
262+
gap = total_width - len(left_plain) - len(right_content_plain)
263+
if gap > 2:
264+
status.update(f"{left_content}{' ' * gap}{right_content}")
265+
else:
266+
status.update(f"{left_content} {right_content}")
267+
elif notification:
268+
# Show notification right-aligned
220269
time_prefix = f"[dim]{timestamp}[/] " if timestamp else ""
221270

222271
if severity == "warning":
@@ -225,26 +274,12 @@ def _update_status_bar(self: UINavigationMixinHost) -> None:
225274
notif_str = f"{time_prefix}{notification}"
226275

227276
notif_plain = f"{timestamp} {notification}" if timestamp else notification
228-
229-
try:
230-
total_width = self.size.width - 2
231-
except Exception:
232-
total_width = 80
233-
234277
gap = total_width - len(left_plain) - len(notif_plain)
235278
if gap > 2:
236279
status.update(f"{left_content}{' ' * gap}{notif_str}")
237280
else:
238-
status.update(notif_str)
281+
status.update(f"{left_content} {notif_str}")
239282
elif right_str:
240-
import re
241-
242-
left_plain = re.sub(r"\[.*?\]", "", left_content)
243-
try:
244-
total_width = self.size.width - 2
245-
except Exception:
246-
total_width = 80
247-
248283
gap = total_width - len(left_plain) - len(right_plain)
249284
if gap > 2:
250285
status.update(f"{left_content}{' ' * gap}{right_str}")

0 commit comments

Comments
 (0)