Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sqlit/core/keymap.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ def _build_leader_commands(self) -> list[LeaderCommandDef]:
# View
LeaderCommandDef("e", "toggle_explorer", "Toggle Explorer", "View"),
LeaderCommandDef("f", "toggle_fullscreen", "Toggle Maximize", "View"),
LeaderCommandDef("w", "toggle_compact_columns", "Compact Columns", "View"),
# Connection
LeaderCommandDef("c", "show_connection_picker", "Connect", "Connection"),
LeaderCommandDef("x", "disconnect", "Disconnect", "Connection", guard="has_connection"),
Expand Down
7 changes: 7 additions & 0 deletions sqlit/domains/results/ui/mixins/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,13 @@ def handle_pick(index: int | None) -> None:

self.push_screen(ColumnPickerScreen(labels), handle_pick)

def action_toggle_compact_columns(self: ResultsMixinHost) -> None:
"""Toggle sizing result columns to their average value width instead of the widest."""
enabled = not self.results_compact_columns
self.results_compact_columns = enabled
for table in self.results_area.query(SqlitDataTable):
table.set_compact_columns(enabled)

def action_clear_results(self: ResultsMixinHost) -> None:
"""Clear the results table."""
if self.results_area.has_class("stacked-mode"):
Expand Down
1 change: 1 addition & 0 deletions sqlit/domains/shell/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ def __init__(
self._last_result_rows: list[tuple[Any, ...]] = []
self._last_result_row_count: int = 0
self._results_table_counter: int = 0
self.results_compact_columns: bool = False
self._internal_clipboard: str = ""
# Undo/redo history for query editor
self._undo_history: Any = None # Lazy init UndoHistory
Expand Down
4 changes: 4 additions & 0 deletions sqlit/domains/shell/state/machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ def lk(action: str, menu: str, fallback: str) -> str:
s.binding(f"{leader_key}{lk('change_theme', 'leader', 't')}", "Change theme")
s.binding(f"{leader_key}{lk('toggle_fullscreen', 'leader', 'f')}", "Toggle fullscreen pane")
s.binding(f"{leader_key}{lk('toggle_explorer', 'leader', 'e')}", "Toggle explorer visibility")
s.binding(
f"{leader_key}{lk('toggle_compact_columns', 'leader', 'w')}",
"Toggle compact result columns (average width)",
)
sections.append(s)

# NAVIGATION
Expand Down
1 change: 1 addition & 0 deletions sqlit/domains/shell/state/main_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def _setup_actions(self) -> None:
self.allows("show_connection_picker")
self.allows("disconnect", guard=lambda app: app.has_connection)
self.allows("toggle_fullscreen", help="Toggle fullscreen")
self.allows("toggle_compact_columns", help="Toggle compact result columns")
self.allows("change_theme")
self.allows("toggle_process_worker", help="Toggle process worker")
self.allows("leader_key", key="<space>", label="Commands")
Expand Down
3 changes: 3 additions & 0 deletions sqlit/domains/shell/ui/mixins/ui_leader.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ def action_leader_toggle_explorer(self: UINavigationMixinHost) -> None:
def action_leader_toggle_fullscreen(self: UINavigationMixinHost) -> None:
self._execute_leader_command("toggle_fullscreen")

def action_leader_toggle_compact_columns(self: UINavigationMixinHost) -> None:
self._execute_leader_command("toggle_compact_columns")

def action_leader_show_connection_picker(self: UINavigationMixinHost) -> None:
self._execute_leader_command("show_connection_picker")

Expand Down
1 change: 1 addition & 0 deletions sqlit/shared/ui/protocols/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ResultsStateProtocol(Protocol):
_last_query_table: dict[str, Any] | None
_pending_result_table_info: dict[str, Any] | None
_results_table_counter: int
results_compact_columns: bool
_results_filter_visible: bool
_results_filter_text: str
_results_filter_matches: list[int]
Expand Down
63 changes: 63 additions & 0 deletions sqlit/shared/ui/widgets_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import math
from collections.abc import Iterable
from datetime import date, datetime, time, timedelta
from decimal import Decimal
Expand All @@ -18,6 +19,7 @@
from textual.events import Key
from textual.strip import Strip
from textual_fastdatatable import DataTable as FastDataTable
from textual_fastdatatable.column import CELL_X_PADDING, Column


def normalize_arrow_value(value: Any) -> Any:
Expand Down Expand Up @@ -64,9 +66,70 @@ class SqlitDataTable(FastDataTable):
# Track if a manual tooltip is being shown (via 'v' key)
_manual_tooltip_active: bool = False

# Compact columns size each column to its average value width rather than
# its widest, sampling this many rows so large results toggle instantly.
COMPACT_SAMPLE_ROWS = 200
COMPACT_MIN_WIDTH = 3

_compact_columns: bool = False

def __init__(self, *, data: Any | None = None, **kwargs: Any) -> None:
super().__init__(data=_stringify_uuid_data(data), **kwargs)

def on_mount(self) -> None:
# The compact toggle is app-wide, so tables built for later queries
# pick it up too.
if getattr(self.app, "results_compact_columns", False):
self.set_compact_columns(True)

@property
def ordered_columns(self) -> list[Column]:
fresh = self._ordered_columns is None
columns = super().ordered_columns
if fresh and self._compact_columns:
self._apply_compact_widths(columns)
return columns

@property
def compact_columns(self) -> bool:
return self._compact_columns

def set_compact_columns(self, enabled: bool) -> None:
"""Switch between average-width (compact) and full-width columns."""
if enabled == self._compact_columns:
return
self._compact_columns = enabled
# Columns are rebuilt from the backend on next access, which re-applies
# (or drops) the compact widths.
self._ordered_columns = None
self._clear_caches()
self._require_update_dimensions = True
self.refresh()

def _apply_compact_widths(self, columns: list[Column]) -> None:
backend = self.backend
if backend is None or backend.row_count == 0:
return
row_count = backend.row_count
# Measure with Rich directly: DataTable._measure only exists in newer
# textual-fastdatatable releases.
console = self.app.console
options = console.options

def measure(renderable: Any) -> int:
return console.measure(renderable, options=options).maximum

step = max(1, row_count // self.COMPACT_SAMPLE_ROWS)
sample = range(0, row_count, step)
for index, column in enumerate(columns):
widths = [measure(self._format_cell(backend.get_cell_at(row, index), column)) for row in sample]
average = math.ceil(sum(widths) / len(widths))
# Keep the header readable; only the data is squeezed to its average.
floor = max(self.COMPACT_MIN_WIDTH, measure(column.label))
full_width = column.render_width - CELL_X_PADDING
column.width = min(full_width, max(floor, average))
column.auto_width = False

def add_rows(self, rows: Iterable[Iterable[Any]]) -> list[int]:
return super().add_rows(_stringify_uuid_rows(rows))

Expand Down
68 changes: 68 additions & 0 deletions tests/ui/test_compact_columns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Leader+w sizes result columns to the average value width instead of the widest."""

from __future__ import annotations

import pytest

from sqlit.domains.shell.app.main import SSMSTUI

from .mocks import (
MockConnectionStore,
MockSettingsStore,
build_test_services,
create_test_connection,
)

# One long outlier among short values: full width follows the outlier,
# compact width follows the typical value.
ROWS = [(i, "ab", "x" * 60 if i == 0 else "short") for i in range(10)]


def _widths(app: SSMSTUI) -> list[int]:
return [column.render_width for column in app.results_table.ordered_columns]


async def _show_results(app: SSMSTUI, pilot) -> None:
await app._display_query_results(
columns=["id", "code", "description"], rows=ROWS, row_count=len(ROWS),
truncated=False, elapsed_ms=0,
)
await pilot.pause()


@pytest.mark.asyncio
async def test_leader_w_toggles_compact_columns_and_persists_for_new_results() -> None:
connection = create_test_connection("test-db", "sqlite")
services = build_test_services(
connection_store=MockConnectionStore([connection]),
settings_store=MockSettingsStore({"theme": "tokyo-night"}),
)
app = SSMSTUI(services=services)

async with app.run_test(size=(120, 40)) as pilot:
await pilot.pause()
await _show_results(app, pilot)
app.results_table.focus()
await pilot.pause()
full = _widths(app)
assert full[2] == 60 + 2

await pilot.press("space", "w")
await pilot.pause()
assert app.results_compact_columns
compact = _widths(app)
# description: (60 + 9 * 5) / 10 -> 11 cells; short data keeps its header readable.
assert compact[2] == 11 + 2
assert compact[1] == len("code") + 2
assert compact[0] == full[0] # never wider than its full width
assert app.results_table.virtual_size.width == sum(compact)

# A new query result keeps the compact layout.
await _show_results(app, pilot)
assert _widths(app) == compact

app.results_table.focus()
await pilot.press("space", "w")
await pilot.pause()
assert not app.results_compact_columns
assert _widths(app) == full
Loading