Skip to content

Commit 8a2fa44

Browse files
committed
Polish UX - Add command menu
1 parent 2533338 commit 8a2fa44

21 files changed

Lines changed: 487 additions & 238 deletions

sqlit/app.py

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
except ImportError:
1212
PYODBC_AVAILABLE = False
1313

14-
from textual.app import App, ComposeResult, SystemCommand
14+
from textual.app import App, ComposeResult
1515
from textual.binding import Binding
1616
from textual.containers import Container, Horizontal, Vertical
1717
from textual.widgets import DataTable, Static, TextArea, Tree
@@ -98,6 +98,10 @@ class SSMSTUI(
9898
border-right: none;
9999
}
100100
101+
Screen.explorer-hidden #sidebar {
102+
display: none;
103+
}
104+
101105
#main-container {
102106
width: 100%;
103107
height: 100%;
@@ -172,14 +176,23 @@ class SSMSTUI(
172176
LAYERS = ["autocomplete"]
173177

174178
BINDINGS = [
179+
# Leader combo bindings (checked first when _leader_pending is True)
180+
Binding("e", "leader_toggle_explorer", show=False),
181+
Binding("f", "leader_fullscreen", show=False),
182+
Binding("h", "leader_help", show=False),
183+
Binding("t", "leader_theme", show=False),
184+
Binding("q", "leader_quit", show=False),
185+
Binding("x", "leader_disconnect", show=False),
186+
# Regular bindings
175187
Binding("n", "new_connection", "New", show=False),
176188
Binding("s", "select_table", "Select", show=False),
177189
Binding("R", "refresh_tree", "Refresh", show=False),
190+
Binding("f", "refresh_tree", "Refresh", show=False),
178191
Binding("e", "edit_connection", "Edit", show=False),
179192
Binding("d", "delete_connection", "Delete", show=False),
180193
Binding("delete", "delete_connection", "Delete", show=False),
181194
Binding("x", "disconnect", "Disconnect", show=False),
182-
Binding("ctrl+p", "command_palette", "Commands", show=False),
195+
Binding("space", "leader_key", "Commands", show=False, priority=True),
183196
Binding("ctrl+q", "quit", "Quit", show=False),
184197
Binding("question_mark", "show_help", "Help", show=False),
185198
Binding("e", "focus_explorer", "Explorer", show=False),
@@ -193,14 +206,11 @@ class SSMSTUI(
193206
Binding("n", "new_query", "New", show=False),
194207
Binding("h", "show_history", "History", show=False),
195208
Binding("z", "collapse_tree", "Collapse", show=False),
196-
Binding("f", "toggle_fullscreen", "Fullscreen", show=False),
197209
Binding("v", "view_cell", "View cell", show=False),
198210
Binding("y", "copy_cell", "Copy cell", show=False),
199211
Binding("Y", "copy_row", "Copy row", show=False),
200212
Binding("a", "copy_results", "Copy results", show=False),
201213
Binding("ctrl+c", "cancel_operation", "Cancel", show=False),
202-
Binding("N", "show_notifications", "Notifications", show=False),
203-
Binding("d", "dismiss_notification", "Dismiss", show=False),
204214
]
205215

206216
def __init__(self):
@@ -235,6 +245,9 @@ def __init__(self):
235245
self._last_notification_time: str = ""
236246
self._notification_timer = None
237247
self._notification_history: list = []
248+
self._connection_failed: bool = False
249+
self._leader_timer = None
250+
self._leader_pending: bool = False
238251
self._query_worker = None
239252
self._query_executing: bool = False
240253
self._cancellable_query: Any | None = None
@@ -281,6 +294,23 @@ def _show_footer(self) -> None:
281294

282295
def check_action(self, action: str, parameters: tuple) -> bool | None:
283296
"""Only allow actions when their context is active."""
297+
# Leader combo actions only work when _leader_pending is True
298+
if action.startswith("leader_") and action != "leader_key":
299+
if getattr(self, "_leader_pending", False):
300+
# Cancel timer and clear pending state
301+
if hasattr(self, "_leader_timer") and self._leader_timer is not None:
302+
self._leader_timer.stop()
303+
self._leader_timer = None
304+
self._leader_pending = False
305+
return True
306+
return False
307+
308+
# Block most actions when waiting for leader combo
309+
if getattr(self, "_leader_pending", False):
310+
# Only allow leader_key action during leader pending
311+
if action != "leader_key":
312+
return False
313+
284314
try:
285315
tree = self.query_one("#object-tree", Tree)
286316
query_input = self.query_one("#query-input", TextArea)
@@ -308,7 +338,6 @@ def check_action(self, action: str, parameters: tuple) -> bool | None:
308338
if action in (
309339
"quit",
310340
"exit_insert_mode",
311-
"command_palette",
312341
"execute_query_insert",
313342
):
314343
return True
@@ -340,11 +369,9 @@ def check_action(self, action: str, parameters: tuple) -> bool | None:
340369
elif action == "select_table":
341370
return tree_focused and node_type in ("table", "view")
342371
elif action == "execute_query":
343-
return (
344-
query_focused or results_focused
345-
) and self.current_connection is not None
372+
return query_focused or results_focused
346373
elif action == "execute_query_insert":
347-
return query_focused and self.current_connection is not None
374+
return query_focused
348375
elif action in ("clear_query", "new_query"):
349376
return query_focused and self.vim_mode == VimMode.NORMAL
350377
elif action == "show_history":
@@ -356,15 +383,16 @@ def check_action(self, action: str, parameters: tuple) -> bool | None:
356383
elif action in ("focus_query", "focus_results"):
357384
return True
358385
elif action == "toggle_fullscreen":
359-
return True
386+
# Don't allow toggle_fullscreen when tree focused (f is refresh there)
387+
return not tree_focused
360388
elif action == "test_connections":
361389
return True
362390
elif action in ("view_cell", "copy_cell", "copy_row", "copy_results"):
363-
return results_focused and self.current_connection is not None
391+
return results_focused
364392
elif action in (
365393
"quit",
366394
"show_help",
367-
"command_palette",
395+
"leader_key",
368396
"toggle_dark",
369397
):
370398
return True
@@ -433,6 +461,9 @@ def on_mount(self) -> None:
433461

434462
tree = self.query_one("#object-tree", Tree)
435463
tree.focus()
464+
# Move cursor to first node if available
465+
if tree.root.children:
466+
tree.cursor_line = 0
436467
self._update_section_labels()
437468

438469
# Check for ODBC drivers
@@ -531,15 +562,3 @@ def watch_theme(self, old_theme: str, new_theme: str) -> None:
531562
settings["theme"] = new_theme
532563
save_settings(settings)
533564

534-
def get_system_commands(self, screen):
535-
yield from super().get_system_commands(screen)
536-
yield SystemCommand(
537-
"Refresh Object Explorer",
538-
"Reload the object explorer tree",
539-
self.action_refresh_tree,
540-
)
541-
yield SystemCommand(
542-
"Test connections",
543-
"Attempt to connect to all saved connections",
544-
self.action_test_connections,
545-
)

sqlit/ui/mixins/autocomplete.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ def on_key(self, event) -> None:
282282
"""Handle key events for autocomplete navigation."""
283283
from ...widgets import AutocompleteDropdown, VimMode
284284

285+
# Handle autocomplete navigation
285286
if not self._autocomplete_visible:
286287
return
287288

sqlit/ui/mixins/connection.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,10 @@ def connect_to_server(self, config: "ConnectionConfig") -> None:
5454
self.current_config = None
5555
self.current_adapter = None
5656
self.current_ssh_tunnel = None
57+
self.refresh_tree()
5758

58-
# Show connecting status
59-
self.notify(f"Connecting to {config.name}...")
59+
# Reset connection failed state
60+
self._connection_failed = False
6061

6162
# Use injected factory or default
6263
create_session = self._session_factory or ConnectionSession.create
@@ -67,6 +68,7 @@ def work() -> "ConnectionSession":
6768

6869
def on_success(session: "ConnectionSession") -> None:
6970
"""Handle successful connection on main thread."""
71+
self._connection_failed = False
7072
self._session = session
7173
self.current_connection = session.connection
7274
self.current_config = config
@@ -75,10 +77,15 @@ def on_success(session: "ConnectionSession") -> None:
7577

7678
self.refresh_tree()
7779
self._load_schema_cache()
80+
self._update_status_bar()
7881

7982
def on_error(error: Exception) -> None:
8083
"""Handle connection failure on main thread."""
81-
self.notify(f"Connection failed: {error}", severity="error")
84+
from ..screens import ErrorScreen
85+
86+
self._connection_failed = True
87+
self._update_status_bar()
88+
self.push_screen(ErrorScreen("Connection Failed", str(error)))
8289

8390
def do_work() -> None:
8491
"""Worker function with error handling."""

sqlit/ui/mixins/query.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def action_execute_query_insert(self) -> None:
5454
def _execute_query_common(self, keep_insert_mode: bool) -> None:
5555
"""Common query execution logic."""
5656
if not self.current_connection or not self.current_adapter:
57-
self.notify("Not connected to a database", severity="warning")
57+
self.notify("Connect to a server to execute queries", severity="warning")
5858
return
5959

6060
query_input = self.query_one("#query-input", TextArea)

0 commit comments

Comments
 (0)