Skip to content

Commit 2f6c996

Browse files
committed
Merge main app updates
1 parent 2ccc7cd commit 2f6c996

File tree

1 file changed

+144
-9
lines changed

1 file changed

+144
-9
lines changed

sqlit/domains/shell/app/main.py

Lines changed: 144 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,15 @@ def __init__(
9898
services: AppServices | None = None,
9999
runtime: RuntimeConfig | None = None,
100100
startup_connection: ConnectionConfig | None = None,
101+
exclusive_connection: bool = False,
101102
):
102103
super().__init__()
103104
self.services = services or build_app_services(runtime or RuntimeConfig.from_env())
104105
from sqlit.core.connection_manager import ConnectionManager
105106

106107
self._connection_manager = ConnectionManager(self.services)
107108
self._startup_connection = startup_connection
109+
self._exclusive_connection = exclusive_connection
108110
self._startup_connect_config: ConnectionConfig | None = None
109111
self._debug_mode = self.services.runtime.debug_mode
110112
self._debug_idle_scheduler = self.services.runtime.debug_idle_scheduler
@@ -161,6 +163,7 @@ def __init__(
161163
self._query_handle: Any | None = None
162164
self._command_mode: bool = False
163165
self._command_buffer: str = ""
166+
self._count_buffer: str = "" # Vim count prefix (e.g., "3" for 3j)
164167
self._ui_stall_watchdog_timer: Timer | None = None
165168
self._ui_stall_watchdog_expected: float | None = None
166169
self._ui_stall_watchdog_interval_s: float = 0.0
@@ -299,6 +302,7 @@ def _get_input_context(self) -> InputContext:
299302
last_result_is_error=last_result_is_error,
300303
has_results=has_results,
301304
stacked_result_count=stacked_result_count,
305+
count_buffer=self._count_buffer,
302306
)
303307

304308
def _debug_screen_label(self, screen: Any | None) -> str:
@@ -425,6 +429,10 @@ def on_key(self, event: Key) -> None:
425429
if self._handle_command_input(event, ctx):
426430
return
427431

432+
# Handle count digit accumulation (e.g., 3j, 10k)
433+
if self._handle_count_digit(event, ctx):
434+
return
435+
428436
action = resolve_action(
429437
event.key,
430438
ctx,
@@ -500,6 +508,61 @@ def _exit_command_mode(self) -> None:
500508
self._command_buffer = ""
501509
self._update_status_bar()
502510

511+
# ========================================================================
512+
# Vim count prefix support
513+
# ========================================================================
514+
515+
def _get_and_clear_count(self) -> int | None:
516+
"""Get the current count prefix and clear it. Returns None if no count."""
517+
if not self._count_buffer:
518+
return None
519+
try:
520+
count = int(self._count_buffer)
521+
# Cap at 9999 for safety
522+
count = min(count, 9999)
523+
except ValueError:
524+
count = None
525+
self._count_buffer = ""
526+
return count
527+
528+
def _append_count_digit(self, digit: str) -> None:
529+
"""Append a digit to the count buffer."""
530+
if len(self._count_buffer) < 4: # Max 4 digits (9999)
531+
self._count_buffer += digit
532+
533+
def _clear_count_buffer(self) -> None:
534+
"""Clear the count buffer."""
535+
self._count_buffer = ""
536+
537+
def _handle_count_digit(self, event: Key, ctx: InputContext) -> bool:
538+
"""Handle digit keys for count prefix accumulation.
539+
540+
Returns True if the digit was consumed as a count prefix.
541+
"""
542+
from sqlit.core.vim import VimMode
543+
544+
# Only in NORMAL mode with query focus
545+
if ctx.focus != "query" or self.vim_mode != VimMode.NORMAL:
546+
return False
547+
548+
# Don't intercept digits during leader pending
549+
if ctx.leader_pending:
550+
return False
551+
552+
char = event.character
553+
if not char or not char.isdigit():
554+
return False
555+
556+
# '0' only appends if count already started (otherwise it's line-start motion)
557+
if char == "0" and not self._count_buffer:
558+
return False
559+
560+
# Digits 1-9 always start/continue count
561+
self._append_count_digit(char)
562+
event.prevent_default()
563+
event.stop()
564+
return True
565+
503566
def _handle_command_input(self, event: Key, ctx: InputContext) -> bool:
504567
from sqlit.core.vim import VimMode
505568

@@ -547,6 +610,12 @@ def _run_command(self, command: str) -> None:
547610
normalized = command.strip()
548611
self._last_command = normalized
549612
self._last_command_at = time.perf_counter()
613+
614+
# Handle go-to-line command (e.g., :25 goes to line 25)
615+
if normalized.isdigit():
616+
self._goto_line(int(normalized))
617+
return
618+
550619
cmd, *args = normalized.split()
551620
cmd = cmd.lower()
552621

@@ -590,6 +659,19 @@ def _run_command(self, command: str) -> None:
590659
if cmd == "set" and args:
591660
target = args[0].lower().replace("-", "_")
592661
value = args[1].lower() if len(args) > 1 else ""
662+
# Line number settings (vim-style)
663+
if target in {"number", "nu"}:
664+
self._set_line_numbers(True)
665+
return
666+
if target in {"nonumber", "nonu"}:
667+
self._set_line_numbers(False)
668+
return
669+
if target in {"relativenumber", "rnu"}:
670+
self._set_relative_line_numbers(True)
671+
return
672+
if target in {"norelativenumber", "nornu"}:
673+
self._set_relative_line_numbers(False)
674+
return
593675
if target in {"process_worker"}:
594676
if not value:
595677
self._execute_command_action("toggle_process_worker")
@@ -666,14 +748,25 @@ def _show_command_list(self) -> None:
666748
"Use 'plaintext' to store passwords in ~/.sqlit/ (protected folder), 'keyring' to use system keyring.",
667749
),
668750
("Appearance", ":theme", "Open theme selection", ""),
669-
(
670-
"Query",
671-
":alert off|delete|write",
672-
"Confirm before risky queries",
673-
"Modes: off, delete, write",
674-
),
751+
("Query", ":alert off|delete|write", "Confirm before risky queries", "Modes: off, delete, write"),
675752
("Query", ":run, :r", "Execute query", ""),
676753
("Query", ":run!, :r!", "Execute query (stay in INSERT)", ""),
754+
(
755+
"Navigation",
756+
":<number>",
757+
"Go to line number",
758+
"Jump to specified line in query editor (e.g., :25 goes to line 25).",
759+
),
760+
(
761+
"Navigation",
762+
"<count>G",
763+
"Go to line with count prefix",
764+
"In NORMAL mode, type a number then G to go to that line (e.g., 25G).",
765+
),
766+
("Editor", ":set number, :set nu", "Show line numbers", ""),
767+
("Editor", ":set nonumber, :set nonu", "Hide line numbers", ""),
768+
("Editor", ":set relativenumber, :set rnu", "Show relative line numbers", ""),
769+
("Editor", ":set norelativenumber, :set nornu", "Show absolute line numbers", ""),
677770
(
678771
"Worker",
679772
":process-worker, :worker",
@@ -686,6 +779,9 @@ def _show_command_list(self) -> None:
686779
"Show process worker status",
687780
"Displays worker mode, active state, and last activity.",
688781
),
782+
("Settings", ":set process_worker_warm on|off", "Warm worker on idle", ""),
783+
("Settings", ":set process_worker_lazy on|off", "Lazy worker start", ""),
784+
("Settings", ":set process_worker_auto_shutdown <seconds>", "Auto-shutdown worker", ""),
689785
(
690786
"Watchdog",
691787
":wd <ms|off>",
@@ -703,9 +799,6 @@ def _show_command_list(self) -> None:
703799
("Debug", ":debug", "Show debug status", ""),
704800
("Debug", ":debug list", "Show debug events", ""),
705801
("Debug", ":debug clear", "Clear debug event log", ""),
706-
("Settings", ":set process_worker_warm on|off", "Warm worker on idle", ""),
707-
("Settings", ":set process_worker_lazy on|off", "Lazy worker start", ""),
708-
("Settings", ":set process_worker_auto_shutdown <seconds>", "Auto-shutdown worker", ""),
709802
]
710803
if hasattr(self, "_replace_results_table"):
711804
self._replace_results_table(columns, rows)
@@ -824,6 +917,48 @@ def _set_process_worker(self, enabled: bool) -> None:
824917
state = "enabled" if enabled else "disabled"
825918
self.notify(f"Process worker {state}")
826919

920+
def _goto_line(self, line_number: int) -> None:
921+
"""Go to a specific line number (1-indexed)."""
922+
try:
923+
lines = self.query_input.text.split("\n")
924+
num_lines = len(lines)
925+
# Convert to 0-indexed and clamp
926+
target_row = min(line_number - 1, num_lines - 1)
927+
target_row = max(0, target_row)
928+
self.query_input.cursor_location = (target_row, 0)
929+
self.query_input.focus()
930+
except Exception:
931+
pass
932+
933+
def _set_line_numbers(self, enabled: bool) -> None:
934+
"""Enable or disable line numbers in the query editor."""
935+
try:
936+
self.query_input.show_line_numbers = enabled
937+
except Exception:
938+
pass
939+
try:
940+
self.services.settings_store.set("show_line_numbers", enabled)
941+
except Exception:
942+
pass
943+
state = "enabled" if enabled else "disabled"
944+
self.notify(f"Line numbers {state}")
945+
946+
def _set_relative_line_numbers(self, enabled: bool) -> None:
947+
"""Enable or disable relative line numbers in the query editor."""
948+
try:
949+
self.query_input.relative_line_numbers = enabled
950+
# Also ensure line numbers are visible when enabling relative
951+
if enabled and not self.query_input.show_line_numbers:
952+
self.query_input.show_line_numbers = True
953+
except Exception:
954+
pass
955+
try:
956+
self.services.settings_store.set("relative_line_numbers", enabled)
957+
except Exception:
958+
pass
959+
state = "enabled" if enabled else "disabled"
960+
self.notify(f"Relative line numbers {state}")
961+
827962
def _set_process_worker_warm_on_idle(self, enabled: bool) -> None:
828963
self.services.runtime.process_worker_warm_on_idle = enabled
829964
try:

0 commit comments

Comments
 (0)