33import argparse
44import csv
55import curses
6+ import io
67import logging
78import platform
9+ import re
810import subprocess
911import sys
12+ import urllib .request
1013from typing import List , Optional , Set , Tuple
1114
1215import networkx as nx
@@ -531,25 +534,53 @@ def calculate_floors(
531534 }
532535
533536
537+ def is_gist_url (path : str ) -> bool :
538+ """Check if a path is a GitHub gist URL."""
539+ return bool (re .match (r"https?://gist\.github(?:usercontent)?\.com/" , path ))
540+
541+
542+ def gist_to_raw_url (url : str ) -> str :
543+ """Convert a GitHub gist URL to its raw content URL."""
544+ # Already a raw gist URL
545+ if "gist.githubusercontent.com" in url :
546+ return url
547+ # Convert https://gist.github.com/{user}/{id} or .../raw etc.
548+ match = re .match (r"https?://gist\.github\.com/([^/]+)/([a-f0-9]+)(?:/raw)?/?$" , url )
549+ if match :
550+ return f"https://gist.githubusercontent.com/{ match .group (1 )} /{ match .group (2 )} /raw"
551+ return url
552+
553+
534554def load_edges_from_file (filepath : str ) -> Set [Tuple [str , str ]]:
535555 edges : Set [Tuple [str , str ]] = set ()
536556
537- with open (filepath , "r" , newline = "" ) as f :
538- edges .update (tuple (row ) for row in csv .reader (f ) if row and row [0 ].strip () and not row [0 ].startswith ("#" ))
557+ if is_gist_url (filepath ):
558+ raw_url = gist_to_raw_url (filepath )
559+ response = urllib .request .urlopen (raw_url )
560+ content = response .read ().decode ("utf-8" )
561+ reader = csv .reader (io .StringIO (content ))
562+ edges .update (tuple (row ) for row in reader if row and row [0 ].strip () and not row [0 ].startswith ("#" ))
563+ else :
564+ with open (filepath , "r" , newline = "" ) as f :
565+ edges .update (tuple (row ) for row in csv .reader (f ) if row and row [0 ].strip () and not row [0 ].startswith ("#" ))
539566
540567 return edges
541568
542569
543570def run_interactive (
544571 include_analysis ,
545572 target : str ,
546- ignores_file : str ,
547- skips_file : str ,
573+ ignores_files : List [ str ] ,
574+ skips_files : List [ str ] ,
548575 top_n : int ,
549576 sort_by : str ,
550577):
551578 """Run the interactive curses-based TUI for cut_header."""
552579
580+ # The first file in each list is the one that gets written to
581+ ignores_output_file = ignores_files [0 ]
582+ skips_output_file = skips_files [0 ]
583+
553584 def prepend_to_file (filepath : str , includer : str , included : str ):
554585 """Prepend a CSV row to the given file."""
555586 new_line = f"{ includer } ,{ included } \n "
@@ -747,8 +778,8 @@ def addstr_line(y, x, includer_file, included_file, prevalence, dominated_edges,
747778 action_row += 1
748779
749780 options = [
750- ("i" , "Ignore" , f"prepend to { ignores_file } " ),
751- ("s" , "Remove (skip)" , f"prepend to { skips_file } " ),
781+ ("i" , "Ignore" , f"prepend to { ignores_output_file } " ),
782+ ("s" , "Remove (skip)" , f"prepend to { skips_output_file } " ),
752783 ]
753784 for oi , (key , label , desc ) in enumerate (options ):
754785 prefix = "> " if action_selected == oi else " "
@@ -792,8 +823,12 @@ def curses_main(stdscr):
792823 stdscr .addstr (0 , 0 , "Computing... please wait" , curses .A_BOLD )
793824 stdscr .refresh ()
794825
795- ignores = load_edges_from_file (ignores_file )
796- skips = load_edges_from_file (skips_file )
826+ ignores : Set [Tuple [str , str ]] = set ()
827+ for f in ignores_files :
828+ ignores .update (load_edges_from_file (f ))
829+ skips : Set [Tuple [str , str ]] = set ()
830+ for f in skips_files :
831+ skips .update (load_edges_from_file (f ))
797832 data = compute_data (include_analysis , target , ignores , skips , top_n , sort_by )
798833 stdscr .redrawwin ()
799834 stdscr .refresh ()
@@ -819,22 +854,22 @@ def curses_main(stdscr):
819854 elif key in (curses .KEY_ENTER , 10 , 13 ):
820855 includer , included = selectable_lines [selected_idx ]
821856 if action_selected == 0 : # Ignore
822- prepend_to_file (ignores_file , includer , included )
857+ prepend_to_file (ignores_output_file , includer , included )
823858 acted_on [(includer , included )] = "ignored"
824859 else : # Remove (skip)
825- prepend_to_file (skips_file , includer , included )
860+ prepend_to_file (skips_output_file , includer , included )
826861 acted_on [(includer , included )] = "skipped"
827862 action_mode = False
828863 action_taken = True
829864 elif key == ord ("i" ):
830865 includer , included = selectable_lines [selected_idx ]
831- prepend_to_file (ignores_file , includer , included )
866+ prepend_to_file (ignores_output_file , includer , included )
832867 acted_on [(includer , included )] = "ignored"
833868 action_mode = False
834869 action_taken = True
835870 elif key == ord ("s" ):
836871 includer , included = selectable_lines [selected_idx ]
837- prepend_to_file (skips_file , includer , included )
872+ prepend_to_file (skips_output_file , includer , included )
838873 acted_on [(includer , included )] = "skipped"
839874 action_mode = False
840875 action_taken = True
@@ -940,12 +975,20 @@ def main():
940975 print ("error: interactive mode requires at least one --skips file" )
941976 return 1
942977
978+ if is_gist_url (args .ignores [0 ]):
979+ print ("error: the first --ignores in interactive mode must be a local file, not a gist URL" )
980+ return 1
981+
982+ if is_gist_url (args .skips [0 ]):
983+ print ("error: the first --skips in interactive mode must be a local file, not a gist URL" )
984+ return 1
985+
943986 # NOTE: the first --ignores and --skips files are the ones updated
944987 run_interactive (
945988 include_analysis = include_analysis ,
946989 target = args .target ,
947- ignores_file = args .ignores [ 0 ] ,
948- skips_file = args .skips [ 0 ] ,
990+ ignores_files = args .ignores ,
991+ skips_files = args .skips ,
949992 top_n = args .top ,
950993 sort_by = args .sort_by ,
951994 )
0 commit comments