@@ -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