From 5fd33a453649fded60dad0e04172019e7b307e2b Mon Sep 17 00:00:00 2001 From: Aman K Date: Sat, 5 Sep 2026 08:23:01 +0530 Subject: [PATCH 1/3] make the query appear next to the history query instead of the bottom --- .../domains/query/ui/screens/query_history.py | 73 +++++++++++++------ 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/sqlit/domains/query/ui/screens/query_history.py b/sqlit/domains/query/ui/screens/query_history.py index 55062616..a62a39f9 100644 --- a/sqlit/domains/query/ui/screens/query_history.py +++ b/sqlit/domains/query/ui/screens/query_history.py @@ -9,7 +9,7 @@ from textual.app import ComposeResult from rich.text import Text from textual.binding import Binding -from textual.containers import VerticalScroll +from textual.containers import Horizontal, Vertical, VerticalScroll from textual.screen import ModalScreen from textual.widgets import OptionList, Static from textual.widgets.option_list import Option @@ -37,20 +37,38 @@ class QueryHistoryScreen(ModalScreen): } #history-dialog { - width: 90; - max-width: 90%; - height: 80%; - max-height: 90%; + width: 120; + max-width: 95%; + height: 24; + min-height: 16; + max-height: 80%; } - #history-scroll { + #history-filter { + background: $surface; + } + + /* nvim-style split: list sidebar | preview window */ + #history-split { + height: 1fr; + width: 1fr; + } + + #history-list-pane { + width: 32; + min-width: 24; + max-width: 40%; height: 1fr; background: $surface; border: none; + padding: 0; } - #history-filter { + #history-scroll { + height: 1fr; background: $surface; + border: none; + padding: 0; } #history-list { @@ -70,14 +88,19 @@ class QueryHistoryScreen(ModalScreen): padding: 2; } - #history-preview-container { - height: 8; - min-height: 8; - max-height: 8; + #history-preview-pane { + width: 1fr; + height: 1fr; background: $surface-darken-1; border: none; padding: 1; - margin-top: 1; + } + + #history-preview-container { + height: 1fr; + background: $surface-darken-1; + border: none; + padding: 0; } #history-preview { @@ -199,18 +222,20 @@ def compose(self) -> ComposeResult: with Dialog(id="history-dialog", title=title, shortcuts=shortcuts): yield FilterInput(id="history-filter") - with VerticalScroll(id="history-scroll"): - if self._merged_entries: - options = [] - for entry in self._merged_entries: - options.append(self._build_option(entry)) - - yield OptionList(*options, id="history-list") - else: - yield Static(empty_message, id="history-empty") - - with VerticalScroll(id="history-preview-container"): - yield Static("", id="history-preview") + with Horizontal(id="history-split"): + with Vertical(id="history-list-pane"): + with VerticalScroll(id="history-scroll"): + if self._merged_entries: + options = [] + for entry in self._merged_entries: + options.append(self._build_option(entry)) + + yield OptionList(*options, id="history-list") + else: + yield Static(empty_message, id="history-empty") + with Vertical(id="history-preview-pane"): + with VerticalScroll(id="history-preview-container"): + yield Static("", id="history-preview") def on_mount(self) -> None: if self._merged_entries: From a76632e166e47c4678d738b2674e0c20e895582c Mon Sep 17 00:00:00 2001 From: Aman K Date: Sat, 5 Sep 2026 08:23:09 +0530 Subject: [PATCH 2/3] nvim style split layout with tab switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tab pane switching: Added a priority tab binding (action_toggle_pane) that jumps focus between the list and preview panes. The footer now advertises Pane . works during search Pane-aware j/k navigation: j/k now navigate the active pane — moving the OptionList highlight in the list pane and scrolling the preview VerticalScroll line-by-line in the preview pane Filter editing bound to the list pane. / switches panes before opening the filter --- .../domains/query/ui/screens/query_history.py | 101 ++++++++++++++++-- 1 file changed, 92 insertions(+), 9 deletions(-) diff --git a/sqlit/domains/query/ui/screens/query_history.py b/sqlit/domains/query/ui/screens/query_history.py index a62a39f9..641dfa4d 100644 --- a/sqlit/domains/query/ui/screens/query_history.py +++ b/sqlit/domains/query/ui/screens/query_history.py @@ -28,6 +28,7 @@ class QueryHistoryScreen(ModalScreen): Binding("d", "delete", "Delete"), Binding("asterisk", "toggle_star", "Star"), Binding("slash", "open_filter", "Filter"), + Binding("tab", "toggle_pane", "Pane", priority=True), ] CSS = """ @@ -64,6 +65,22 @@ class QueryHistoryScreen(ModalScreen): padding: 0; } + #history-list-pane.active-pane { + border-left: tall $primary; + } + + #history-preview-pane { + width: 1fr; + height: 1fr; + background: $surface-darken-1; + border: none; + padding: 1; + } + + #history-preview-pane.active-pane { + border-left: tall $primary; + } + #history-scroll { height: 1fr; background: $surface; @@ -88,14 +105,6 @@ class QueryHistoryScreen(ModalScreen): padding: 2; } - #history-preview-pane { - width: 1fr; - height: 1fr; - background: $surface-darken-1; - border: none; - padding: 1; - } - #history-preview-container { height: 1fr; background: $surface-darken-1; @@ -133,6 +142,7 @@ def __init__( self._filter_query = "" self._filter_fuzzy = False self._filtered_entries: list[QueryHistoryEntry] = [] + self._active_pane = "list" # "list" or "preview" def _merge_entries(self) -> list[QueryHistoryEntry]: """Merge history entries with starred-only queries. @@ -216,7 +226,7 @@ def compose(self) -> ComposeResult: else: title = f"Query History - {self.connection_name}" empty_message = "No query history for this connection" - shortcuts = [("Select", ""), ("Star", "*"), ("Delete", "D")] + shortcuts = [("Select", ""), ("Star", "*"), ("Delete", "D"), ("Pane", "")] self._merged_entries = self._merge_entries() @@ -250,9 +260,39 @@ def on_mount(self) -> None: filter_input.hide() except Exception: pass + self._set_active_pane("list") if self._auto_open_filter: self.action_open_filter() + def action_toggle_pane(self) -> None: + """Switch focus between the list pane and the preview pane (Tab). + + Works while a search filter is open so the user can jump to the + preview to read a query without closing the filter. + """ + self._set_active_pane("preview" if self._active_pane == "list" else "list") + + def _set_active_pane(self, pane: str) -> None: + """Mark ``pane`` as active, update focus, and refresh pane borders.""" + self._active_pane = pane + try: + list_pane = self.query_one("#history-list-pane", Vertical) + preview_pane = self.query_one("#history-preview-pane", Vertical) + except Exception: + return + list_pane.set_class(pane == "list", "active-pane") + preview_pane.set_class(pane == "preview", "active-pane") + if pane == "list": + try: + self.query_one("#history-list", OptionList).focus() + except Exception: + pass + else: + try: + self.query_one("#history-preview-container", VerticalScroll).focus() + except Exception: + pass + def on_option_list_option_highlighted(self, event: OptionList.OptionHighlighted) -> None: if event.option_list.id == "history-list": idx = event.option_list.highlighted @@ -264,6 +304,14 @@ def _update_preview(self, idx: int) -> None: if idx < len(entries): preview = self.query_one("#history-preview", Static) preview.update(Text(entries[idx].query)) + # Reset the preview scroll to the top so each query starts at + # its first line instead of inheriting the previous query's + # scroll offset. + try: + container = self.query_one("#history-preview-container", VerticalScroll) + container.scroll_home(animate=False) + except Exception: + pass def action_select(self) -> None: entries = self._get_display_entries() @@ -328,6 +376,22 @@ def action_cancel(self) -> None: self.dismiss(None) def on_key(self, event: Any) -> None: + # j/k always navigate the active pane, even while a filter is open: + # in the preview pane they scroll the query; in the list pane they + # move the highlight (no filter) or type into the filter (filter open). + if event.key in ("j", "k") and self._active_pane == "preview": + try: + preview = self.query_one("#history-preview-container", VerticalScroll) + except Exception: + return + if event.key == "j": + preview.scroll_down() + else: + preview.scroll_up() + event.prevent_default() + event.stop() + return + if not self._filter_active: if event.key in ("j", "k"): try: @@ -342,6 +406,14 @@ def on_key(self, event: Any) -> None: event.stop() return + # Filter-text editing (backspace + printable chars) is bound to the + # list pane: the list pane is "type to filter" mode. In the preview + # pane these keys are ignored so reading a query can't accidentally + # mutate the filter; only j/k (scroll), Tab, and action bindings + # (enter/d/*/escape) remain active there. + if self._active_pane != "list": + return + key = event.key if key == "backspace": if self._filter_text: @@ -366,6 +438,17 @@ def on_key(self, event: Any) -> None: def action_open_filter(self) -> None: if not self._merged_entries: return + # `/` is the filter-entry key, and filter text is only editable in + # the list pane. If the user is in the preview pane, jump to the + # list pane first so the opened filter is immediately typeable + # instead of silently swallowing keystrokes. + if self._active_pane != "list": + self._set_active_pane("list") + # Only (re)initialise the filter when it wasn't already open, so + # pressing `/` to hop back to the list pane preserves any text the + # user has already typed. + if self._filter_active: + return self._filter_active = True self._filter_text = "" self._filter_query = "" From 4b2693ec3df4cfc28e6eb9c220cd262cb02f714b Mon Sep 17 00:00:00 2001 From: Aman K Date: Sun, 13 Sep 2026 17:38:59 +0530 Subject: [PATCH 3/3] cleanup the inline comments to make succint and easy to understand --- .../domains/query/ui/screens/query_history.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/sqlit/domains/query/ui/screens/query_history.py b/sqlit/domains/query/ui/screens/query_history.py index 641dfa4d..d09dd90e 100644 --- a/sqlit/domains/query/ui/screens/query_history.py +++ b/sqlit/domains/query/ui/screens/query_history.py @@ -406,11 +406,7 @@ def on_key(self, event: Any) -> None: event.stop() return - # Filter-text editing (backspace + printable chars) is bound to the - # list pane: the list pane is "type to filter" mode. In the preview - # pane these keys are ignored so reading a query can't accidentally - # mutate the filter; only j/k (scroll), Tab, and action bindings - # (enter/d/*/escape) remain active there. + # when in preview pane, disable filter modification if self._active_pane != "list": return @@ -436,17 +432,15 @@ def on_key(self, event: Any) -> None: event.stop() def action_open_filter(self) -> None: + # in case of no history, no filter needed if not self._merged_entries: return - # `/` is the filter-entry key, and filter text is only editable in - # the list pane. If the user is in the preview pane, jump to the - # list pane first so the opened filter is immediately typeable - # instead of silently swallowing keystrokes. + + # when in search mode, default pane is set to list if self._active_pane != "list": self._set_active_pane("list") - # Only (re)initialise the filter when it wasn't already open, so - # pressing `/` to hop back to the list pane preserves any text the - # user has already typed. + + # when some text is alredy in filter, `/` won't reset it if self._filter_active: return self._filter_active = True