Skip to content

Commit 63eb012

Browse files
committed
Add max width on columns, improve cell result view
1 parent 45df428 commit 63eb012

9 files changed

Lines changed: 540 additions & 8 deletions

File tree

sqlit/app.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from .widgets import (
4949
AutocompleteDropdown,
5050
ContextFooter,
51+
InlineValueView,
5152
ResultsFilterInput,
5253
SqlitDataTable,
5354
TreeFilterInput,
@@ -191,6 +192,15 @@ class SSMSTUI(
191192
height: 1fr;
192193
}
193194
195+
/* Hide results table when value view is visible */
196+
#results-area.value-view-active DataTable {
197+
display: none;
198+
}
199+
200+
#results-area.value-view-active #results-filter {
201+
display: none;
202+
}
203+
194204
/* FastDataTable header styling */
195205
DataTable > .datatable--header {
196206
background: $surface-lighten-1;
@@ -540,6 +550,7 @@ def compose(self) -> ComposeResult:
540550
with Container(id="results-area"):
541551
yield ResultsFilterInput(id="results-filter")
542552
yield Lazy(SqlitDataTable(id="results-table", zebra_stripes=True, show_header=False))
553+
yield InlineValueView(id="value-view")
543554

544555
yield Static("", id="idle-scheduler-bar")
545556
yield Static("Not connected", id="status-bar")

sqlit/cli.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ def main() -> int:
144144
metavar="COUNT",
145145
help="Generate fake data with COUNT rows for mock queries (requires --mock, uses Faker if installed).",
146146
)
147+
parser.add_argument(
148+
"--demo-long-text",
149+
action="store_true",
150+
help="Generate data with long varchar columns to test truncation (use with --mock).",
151+
)
147152
parser.add_argument(
148153
"--max-rows",
149154
type=int,
@@ -287,6 +292,10 @@ def main() -> int:
287292
os.environ["SQLIT_DEMO_ROWS"] = str(args.demo_rows)
288293
else:
289294
os.environ.pop("SQLIT_DEMO_ROWS", None)
295+
if args.demo_long_text:
296+
os.environ["SQLIT_DEMO_LONG_TEXT"] = "1"
297+
else:
298+
os.environ.pop("SQLIT_DEMO_LONG_TEXT", None)
290299
if args.max_rows and args.max_rows > 0:
291300
os.environ["SQLIT_MAX_ROWS"] = str(args.max_rows)
292301
else:

sqlit/mocks.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@
1717
sqlit --mock=perf-test --demo-rows=1000 # 1k rows
1818
sqlit --mock=perf-test --demo-rows=10000 # 10k rows
1919
sqlit --mock=sqlite-demo --demo-rows=5000 # Any profile works
20+
21+
Long Text Testing:
22+
Use --demo-long-text to generate data with long varchar columns.
23+
Useful for testing how the UI handles text truncation.
24+
25+
Examples:
26+
sqlit --mock=sqlite-demo --demo-long-text # 10 rows with long text
27+
sqlit --mock=sqlite-demo --demo-long-text --demo-rows=50 # 50 rows with long text
2028
"""
2129

2230
from __future__ import annotations
@@ -74,6 +82,45 @@ def _generate_fake_data(row_count: int) -> tuple[list[str], list[tuple]]:
7482
return columns, rows
7583

7684

85+
def _generate_long_text_data(row_count: int) -> tuple[list[str], list[tuple]]:
86+
"""Generate data with long varchar columns for testing truncation.
87+
88+
Creates columns with varying text lengths to test UI truncation behavior.
89+
Useful for verifying how long text fields are displayed/truncated.
90+
91+
Args:
92+
row_count: Number of rows to generate.
93+
94+
Returns:
95+
Tuple of (columns, rows).
96+
"""
97+
# Column lengths designed to test truncation boundaries
98+
text_lengths = {
99+
"short_text": 15, # Short, no truncation expected
100+
"medium_text": 50, # Around typical column width
101+
"long_text": 150, # Definitely needs truncation
102+
"very_long_text": 500, # Very long content
103+
"description": 300, # Realistic long field
104+
}
105+
106+
columns = ["id", "name"] + list(text_lengths.keys())
107+
rows = []
108+
109+
for i in range(row_count):
110+
row: list[object] = [i + 1, f"Row {i + 1}"]
111+
for col_name, length in text_lengths.items():
112+
# Generate text with visible pattern showing row number and column
113+
base = f"[R{i + 1}:{col_name[:6]}]"
114+
# Fill with Lorem-style content
115+
filler = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
116+
text = base + (filler * ((length // len(filler)) + 1))
117+
text = text[:length]
118+
row.append(text)
119+
rows.append(tuple(row))
120+
121+
return columns, rows
122+
123+
77124
class MockConnection:
78125
"""Mock database connection object."""
79126

@@ -299,6 +346,18 @@ def execute_query(self, conn: Any, query: str, max_rows: int | None = None) -> t
299346
if self._query_delay > 0:
300347
time.sleep(self._query_delay)
301348

349+
# Check if demo long text mode is enabled (for testing truncation)
350+
if os.environ.get("SQLIT_DEMO_LONG_TEXT"):
351+
demo_rows_env = os.environ.get("SQLIT_DEMO_ROWS", "10")
352+
try:
353+
demo_row_count = int(demo_rows_env)
354+
except ValueError:
355+
demo_row_count = 10
356+
cols, rows = _generate_long_text_data(demo_row_count)
357+
if max_rows and len(rows) > max_rows:
358+
return cols, rows[:max_rows], True
359+
return cols, rows, False
360+
302361
# Check if demo rows mode is enabled
303362
demo_rows_env = os.environ.get("SQLIT_DEMO_ROWS", "")
304363
if demo_rows_env:

sqlit/state_machine.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,32 @@ def is_active(self, app: SSMSTUI) -> bool:
867867
return False
868868

869869

870+
class ValueViewActiveState(State):
871+
"""Inline value view is active (viewing a cell's full content)."""
872+
873+
help_category = "Value View"
874+
875+
def _setup_actions(self) -> None:
876+
self.allows("close_value_view", key="escape", label="Close", help="Close value view")
877+
self.allows("close_value_view", key="q", label="Close", help="Close value view")
878+
self.allows("copy_value_view", key="y", label="Copy", help="Copy value")
879+
880+
def get_display_bindings(self, app: SSMSTUI) -> tuple[list[DisplayBinding], list[DisplayBinding]]:
881+
left: list[DisplayBinding] = [
882+
DisplayBinding(key="esc", label="Close", action="close_value_view"),
883+
DisplayBinding(key="y", label="Copy", action="copy_value_view"),
884+
]
885+
return left, []
886+
887+
def is_active(self, app: SSMSTUI) -> bool:
888+
try:
889+
from .widgets import InlineValueView
890+
value_view = app.query_one("#value-view", InlineValueView)
891+
return value_view.is_visible
892+
except Exception:
893+
return False
894+
895+
870896
class ResultsFocusedState(State):
871897
"""Results table has focus."""
872898

@@ -952,6 +978,7 @@ def __init__(self) -> None:
952978

953979
self.results_focused = ResultsFocusedState(parent=self.main_screen)
954980
self.results_filter_active = ResultsFilterActiveState(parent=self.main_screen)
981+
self.value_view_active = ValueViewActiveState(parent=self.main_screen)
955982

956983
self._states = [
957984
self.modal_active,
@@ -969,6 +996,7 @@ def __init__(self) -> None:
969996
self.query_normal,
970997
self.query_focused,
971998
self.results_filter_active, # Before results_focused (more specific when filter active)
999+
self.value_view_active, # Before results_focused (more specific when viewing cell)
9721000
self.results_focused,
9731001
self.main_screen,
9741002
self.root,

sqlit/ui/mixins/query.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
MAX_FETCH_ROWS = 100000
2222
MAX_RENDER_ROWS = 100000
2323

24+
# Column content truncation (full value shown in tooltip and copied to clipboard)
25+
MAX_COLUMN_CONTENT_WIDTH = 100
26+
2427

2528
class QueryMixin:
2629
"""Mixin providing query execution functionality.
@@ -217,7 +220,12 @@ def _replace_results_table(self: AppProtocol, columns: list[str], rows: list[tup
217220
arrow_columns = {col: [] for col in columns}
218221
arrow_table = pa.table(arrow_columns)
219222
backend = ArrowBackend(arrow_table)
220-
new_table = SqlitDataTable(id=new_id, zebra_stripes=True, backend=backend)
223+
new_table = SqlitDataTable(
224+
id=new_id,
225+
zebra_stripes=True,
226+
backend=backend,
227+
max_column_content_width=MAX_COLUMN_CONTENT_WIDTH,
228+
)
221229
container.mount(new_table, after=old_table)
222230
old_table.remove()
223231
return
@@ -238,7 +246,12 @@ def _replace_results_table(self: AppProtocol, columns: list[str], rows: list[tup
238246
backend = ArrowBackend(arrow_table)
239247

240248
# Create and mount new table, then remove old
241-
new_table = SqlitDataTable(id=new_id, zebra_stripes=True, backend=backend)
249+
new_table = SqlitDataTable(
250+
id=new_id,
251+
zebra_stripes=True,
252+
backend=backend,
253+
max_column_content_width=MAX_COLUMN_CONTENT_WIDTH,
254+
)
242255
container.mount(new_table, after=old_table)
243256
old_table.remove()
244257

@@ -266,7 +279,12 @@ def _replace_results_table_raw(self: AppProtocol, columns: list[str], rows: list
266279
arrow_columns = {col: [] for col in columns}
267280
arrow_table = pa.table(arrow_columns)
268281
backend = ArrowBackend(arrow_table)
269-
new_table = SqlitDataTable(id=new_id, zebra_stripes=True, backend=backend)
282+
new_table = SqlitDataTable(
283+
id=new_id,
284+
zebra_stripes=True,
285+
backend=backend,
286+
max_column_content_width=MAX_COLUMN_CONTENT_WIDTH,
287+
)
270288
container.mount(new_table, after=old_table)
271289
old_table.remove()
272290
return
@@ -279,7 +297,12 @@ def _replace_results_table_raw(self: AppProtocol, columns: list[str], rows: list
279297
backend = ArrowBackend(arrow_table)
280298

281299
# Create and mount new table, then remove old
282-
new_table = SqlitDataTable(id=new_id, zebra_stripes=True, backend=backend)
300+
new_table = SqlitDataTable(
301+
id=new_id,
302+
zebra_stripes=True,
303+
backend=backend,
304+
max_column_content_width=MAX_COLUMN_CONTENT_WIDTH,
305+
)
283306
container.mount(new_table, after=old_table)
284307
old_table.remove()
285308

sqlit/ui/mixins/results.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,55 @@ def fmt(value: object) -> str:
7474
return "\n".join(lines)
7575

7676
def action_view_cell(self: AppProtocol) -> None:
77-
"""View the full value of the selected cell."""
78-
from ..screens import ValueViewScreen
77+
"""View the full value of the selected cell inline."""
78+
from ...widgets import InlineValueView
7979

8080
table = self.results_table
8181
if table.row_count <= 0:
8282
self.notify("No results", severity="warning")
8383
return
8484
try:
85+
cursor_row, cursor_col = table.cursor_coordinate
8586
value = table.get_cell_at(table.cursor_coordinate)
8687
except Exception:
8788
return
88-
self.push_screen(ValueViewScreen(str(value) if value is not None else "NULL", title="Cell Value"))
89+
90+
# Get column name if available
91+
column_name = ""
92+
if self._last_result_columns and cursor_col < len(self._last_result_columns):
93+
column_name = self._last_result_columns[cursor_col]
94+
95+
# Show inline value view
96+
try:
97+
value_view = self.query_one("#value-view", InlineValueView)
98+
value_view.set_value(str(value) if value is not None else "NULL", column_name)
99+
value_view.show()
100+
except Exception:
101+
pass
102+
103+
def action_close_value_view(self: AppProtocol) -> None:
104+
"""Close the inline value view and return to results table."""
105+
from ...widgets import InlineValueView
106+
107+
try:
108+
value_view = self.query_one("#value-view", InlineValueView)
109+
if value_view.is_visible:
110+
value_view.hide()
111+
self.results_table.focus()
112+
except Exception:
113+
pass
114+
115+
def action_copy_value_view(self: AppProtocol) -> None:
116+
"""Copy the value from the inline value view."""
117+
from ...widgets import InlineValueView, flash_widget
118+
119+
try:
120+
value_view = self.query_one("#value-view", InlineValueView)
121+
if value_view.is_visible:
122+
self._copy_text(value_view.value)
123+
flash_widget(value_view)
124+
except Exception:
125+
pass
89126

90127
def action_copy_cell(self: AppProtocol) -> None:
91128
"""Copy the selected cell to clipboard (or internal clipboard)."""

0 commit comments

Comments
 (0)