@@ -610,33 +610,38 @@ def action_ry_export(self: ResultsMixinHost) -> None:
610610
611611 def action_rye_csv (self : ResultsMixinHost ) -> None :
612612 """Export results as CSV to file."""
613- self ._clear_leader_pending ()
614- if not self ._last_result_columns or not self ._last_result_rows :
615- self .notify ("No results to export" , severity = "warning" )
616- return
617- self ._show_export_dialog ("csv" )
613+ self ._open_export_dialog ("csv" )
618614
619615 def action_rye_json (self : ResultsMixinHost ) -> None :
620616 """Export results as JSON to file."""
617+ self ._open_export_dialog ("json" )
618+
619+ def action_rye_markdown (self : ResultsMixinHost ) -> None :
620+ """Export results as Markdown table to file."""
621+ self ._open_export_dialog ("markdown" )
622+
623+ def _open_export_dialog (self : ResultsMixinHost , fmt_key : str ) -> None :
624+ """Validate the result set and show the save dialog for a format key."""
621625 self ._clear_leader_pending ()
622626 if not self ._last_result_columns or not self ._last_result_rows :
623627 self .notify ("No results to export" , severity = "warning" )
624628 return
625- self ._show_export_dialog ("json" )
629+ self ._show_export_dialog (fmt_key )
626630
627- def _show_export_dialog (self : ResultsMixinHost , fmt : str ) -> None :
631+ def _show_export_dialog (self : ResultsMixinHost , fmt_key : str ) -> None :
628632 """Show the file save dialog for export."""
629633 from datetime import datetime
630634
635+ from sqlit .domains .results .formatters import FORMATS
631636 from sqlit .shared .ui .screens .file_picker import FilePickerMode , FilePickerScreen
632637
638+ fmt = FORMATS [fmt_key ]
633639 timestamp = datetime .now ().strftime ("%Y%m%d_%H%M%S" )
634- ext = "csv" if fmt == "csv" else "json"
635- default_filename = f"results_{ timestamp } .{ ext } "
640+ default_filename = f"results_{ timestamp } .{ fmt .extension } "
636641
637642 def handle_result (filename : str | None ) -> None :
638643 if filename :
639- self ._save_export_file (filename , fmt )
644+ self ._save_export_file (filename , fmt_key )
640645
641646 self .push_screen (
642647 FilePickerScreen (
@@ -647,17 +652,15 @@ def handle_result(filename: str | None) -> None:
647652 handle_result ,
648653 )
649654
650- def _save_export_file (self : ResultsMixinHost , filename : str , fmt : str ) -> None :
655+ def _save_export_file (self : ResultsMixinHost , filename : str , fmt_key : str ) -> None :
651656 """Save the export file to disk."""
652657 from pathlib import Path
653658
654- from sqlit .domains .results .formatters import format_csv , format_json
659+ from sqlit .domains .results .formatters import FORMATS
655660
656661 try :
657- if fmt == "csv" :
658- content = format_csv (self ._last_result_columns , self ._last_result_rows )
659- else :
660- content = format_json (self ._last_result_columns , self ._last_result_rows )
662+ fmt = FORMATS [fmt_key ]
663+ content = fmt .formatter (self ._last_result_columns , self ._last_result_rows )
661664
662665 path = Path (filename ).expanduser ()
663666 path .write_text (content , encoding = "utf-8" )
@@ -667,6 +670,127 @@ def _save_export_file(self: ResultsMixinHost, filename: str, fmt: str) -> None:
667670 except Exception as e :
668671 self .notify (f"Failed to save: { e } " , severity = "error" )
669672
673+ # ------------------------------------------------------------------
674+ # Copy-as: ryf menu — pick format, then scope (cell/row/all).
675+ # Values list is column-oriented, so ryf v executes directly.
676+ # ------------------------------------------------------------------
677+
678+ def action_ry_format (self : ResultsMixinHost ) -> None :
679+ """Open the 'Copy as…' submenu (format picker)."""
680+ self ._clear_leader_pending ()
681+ self ._start_leader_pending ("ryf" )
682+
683+ def action_ryf_markdown (self : ResultsMixinHost ) -> None :
684+ """Pick Markdown — open scope submenu."""
685+ self ._clear_leader_pending ()
686+ self ._start_leader_pending ("ryfm" )
687+
688+ def action_ryf_json (self : ResultsMixinHost ) -> None :
689+ """Pick JSON — open scope submenu."""
690+ self ._clear_leader_pending ()
691+ self ._start_leader_pending ("ryfj" )
692+
693+ def action_ryf_csv (self : ResultsMixinHost ) -> None :
694+ """Pick CSV — open scope submenu."""
695+ self ._clear_leader_pending ()
696+ self ._start_leader_pending ("ryfc" )
697+
698+ def action_ryf_values (self : ResultsMixinHost ) -> None :
699+ """Copy the focused column's values as a comma-separated list."""
700+ self ._clear_leader_pending ()
701+ self ._copy_column_values ()
702+
703+ def action_ryfm_cell (self : ResultsMixinHost ) -> None :
704+ self ._copy_scope_as_format ("markdown" , "cell" )
705+
706+ def action_ryfm_row (self : ResultsMixinHost ) -> None :
707+ self ._copy_scope_as_format ("markdown" , "row" )
708+
709+ def action_ryfm_all (self : ResultsMixinHost ) -> None :
710+ self ._copy_scope_as_format ("markdown" , "all" )
711+
712+ def action_ryfj_cell (self : ResultsMixinHost ) -> None :
713+ self ._copy_scope_as_format ("json" , "cell" )
714+
715+ def action_ryfj_row (self : ResultsMixinHost ) -> None :
716+ self ._copy_scope_as_format ("json" , "row" )
717+
718+ def action_ryfj_all (self : ResultsMixinHost ) -> None :
719+ self ._copy_scope_as_format ("json" , "all" )
720+
721+ def action_ryfc_cell (self : ResultsMixinHost ) -> None :
722+ self ._copy_scope_as_format ("csv" , "cell" )
723+
724+ def action_ryfc_row (self : ResultsMixinHost ) -> None :
725+ self ._copy_scope_as_format ("csv" , "row" )
726+
727+ def action_ryfc_all (self : ResultsMixinHost ) -> None :
728+ self ._copy_scope_as_format ("csv" , "all" )
729+
730+ def _copy_scope_as_format (
731+ self : ResultsMixinHost , fmt_key : str , scope : str
732+ ) -> None :
733+ """Copy cell/row/all of the active result set as fmt_key."""
734+ from sqlit .domains .results .formatters import FORMATS
735+
736+ self ._clear_leader_pending ()
737+ table , columns , rows , _stacked = self ._get_active_results_context ()
738+ if not table or table .row_count <= 0 :
739+ self .notify ("No results" , severity = "warning" )
740+ return
741+
742+ fmt = FORMATS [fmt_key ]
743+ try :
744+ if scope == "cell" :
745+ _row_idx , col_idx = table .cursor_coordinate
746+ col_name = columns [col_idx ] if 0 <= col_idx < len (columns ) else ""
747+ value = _strip_table_markup (
748+ table , table .get_cell_at (table .cursor_coordinate )
749+ )
750+ content = fmt .formatter ([col_name ], [(value ,)])
751+ elif scope == "row" :
752+ row_values = [
753+ _strip_table_markup (table , v )
754+ for v in table .get_row_at (table .cursor_row )
755+ ]
756+ content = fmt .formatter (columns , [tuple (row_values )])
757+ else : # all
758+ if not columns and not rows :
759+ self .notify ("No results" , severity = "warning" )
760+ return
761+ content = fmt .formatter (columns , rows )
762+ except Exception :
763+ return
764+
765+ self ._copy_text (content )
766+ self ._flash_table_yank (table , scope )
767+
768+ def _copy_column_values (self : ResultsMixinHost ) -> None :
769+ """Copy every value in the focused column as a SQL-ready list."""
770+ from sqlit .domains .results .formatters import format_values_list
771+
772+ table , columns , rows , _stacked = self ._get_active_results_context ()
773+ if not table or table .row_count <= 0 or not rows :
774+ self .notify ("No results" , severity = "warning" )
775+ return
776+ try :
777+ _row_idx , col_idx = table .cursor_coordinate
778+ except Exception :
779+ return
780+ if col_idx < 0 or col_idx >= len (columns ):
781+ self .notify ("No column selected" , severity = "warning" )
782+ return
783+
784+ values = [
785+ _strip_table_markup (table , row [col_idx ])
786+ for row in rows
787+ if col_idx < len (row )
788+ ]
789+ text = format_values_list (values )
790+ self ._copy_text (text )
791+ self ._flash_table_yank (table , "all" )
792+ self .notify (f"Copied { len (values )} values from '{ columns [col_idx ]} '" )
793+
670794 def action_results_cursor_left (self : ResultsMixinHost ) -> None :
671795 """Move results cursor left (vim h)."""
672796 table , _columns , _rows , _stacked = self ._get_active_results_context ()
0 commit comments