for assorted things

init kill processes

Changed files
+160
+160
kill-processes
··· 1 + #!/usr/bin/env -S uv run --script --quiet 2 + # /// script 3 + # requires-python = ">=3.12" 4 + # dependencies = ["textual"] 5 + # /// 6 + import os 7 + import signal 8 + import subprocess 9 + from textual.app import App, ComposeResult 10 + from textual.widgets import ( 11 + Header, 12 + Footer, 13 + Static, 14 + Input, 15 + ListView, 16 + ListItem, 17 + Label, 18 + Button, 19 + ) 20 + from textual.containers import VerticalScroll, Vertical, Container, Horizontal 21 + from textual.screen import ModalScreen 22 + 23 + MIN_PATTERN = int(os.environ.get("PROCESS_TUI_MIN_PATTERN", 3)) 24 + MAX_RESULTS = int(os.environ.get("PROCESS_TUI_MAX_RESULTS", 30)) 25 + 26 + 27 + class ProcessList(Static): 28 + def compose(self) -> ComposeResult: 29 + self.list_view = ListView() 30 + yield self.list_view 31 + 32 + def update_processes(self, pattern: str = ""): 33 + if not pattern: 34 + self.list_view.clear() 35 + self.list_view.append( 36 + ListItem(Label("Enter a pattern to search for processes.")) 37 + ) 38 + return 39 + if len(pattern) < MIN_PATTERN: 40 + self.list_view.clear() 41 + self.list_view.append( 42 + ListItem(Label(f"Pattern must be at least {MIN_PATTERN} characters.")) 43 + ) 44 + return 45 + try: 46 + result = subprocess.run( 47 + ["pgrep", "-f", pattern], capture_output=True, text=True 48 + ) 49 + pids = [pid for pid in result.stdout.strip().split() if pid] 50 + except FileNotFoundError: 51 + self.list_view.clear() 52 + self.list_view.append(ListItem(Label("pgrep not found"))) 53 + return 54 + if len(pids) > MAX_RESULTS: 55 + self.list_view.clear() 56 + self.list_view.append( 57 + ListItem( 58 + Label( 59 + f"Too many results ({len(pids)}). Please refine your pattern or set PROCESS_TUI_MAX_RESULTS." 60 + ) 61 + ) 62 + ) 63 + return 64 + self.list_view.clear() 65 + if not pids: 66 + self.list_view.append(ListItem(Label("No processes found."))) 67 + return 68 + for pid in pids: 69 + try: 70 + cmd = subprocess.run( 71 + ["ps", "-p", pid, "-o", "args="], capture_output=True, text=True 72 + ).stdout.strip() 73 + item = ListItem(Label(f"[b]PID:[/b] {pid}\n[dim]{cmd}[/dim]")) 74 + except Exception: 75 + item = ListItem( 76 + Label(f"[b]PID:[/b] {pid}\n[red](unable to get command)[/red]") 77 + ) 78 + self.list_view.append(item) 79 + 80 + 81 + class ConfirmKillScreen(ModalScreen[bool]): 82 + def __init__(self, pattern: str): 83 + super().__init__() 84 + self.pattern = pattern 85 + 86 + def compose(self) -> ComposeResult: 87 + with Container(): 88 + yield Label(f"Kill all processes matching '{self.pattern}'?", id="question") 89 + with Horizontal(): 90 + yield Button("No", id="no", variant="primary") 91 + yield Button("Yes", id="yes", variant="error") 92 + 93 + def on_button_pressed(self, event: Button.Pressed) -> None: 94 + self.dismiss(event.button.id == "yes") 95 + 96 + 97 + class ProcessTUI(App): 98 + CSS = """ 99 + VerticalScroll { 100 + padding: 1; 101 + } 102 + Input { 103 + margin: 1; 104 + } 105 + ListView { 106 + margin: 1; 107 + } 108 + #question { 109 + content-align: center middle; 110 + margin-bottom: 1; 111 + } 112 + """ 113 + 114 + def compose(self) -> ComposeResult: 115 + yield Header() 116 + with Vertical(): 117 + self.input = Input( 118 + placeholder="Enter pattern to match processes (press Enter to kill)" 119 + ) 120 + yield self.input 121 + with VerticalScroll(): 122 + self.proc_list = ProcessList() 123 + yield self.proc_list 124 + yield Footer() 125 + 126 + def on_mount(self): 127 + self.proc_list.update_processes() 128 + 129 + def on_input_submitted(self, event: Input.Submitted): 130 + pattern = event.value.strip() 131 + if not pattern or len(pattern) < MIN_PATTERN: 132 + return 133 + 134 + def handle_confirm(result: bool | None): 135 + if result: 136 + try: 137 + result = subprocess.run( 138 + ["pgrep", "-f", pattern], capture_output=True, text=True 139 + ) 140 + pids = [int(pid) for pid in result.stdout.strip().split() if pid] 141 + for pid in pids: 142 + try: 143 + os.kill(pid, signal.SIGTERM) 144 + except Exception: 145 + pass 146 + except Exception: 147 + pass 148 + self.input.disabled = False 149 + self.proc_list.update_processes() 150 + 151 + self.input.disabled = True 152 + self.push_screen(ConfirmKillScreen(pattern), handle_confirm) 153 + 154 + def on_input_changed(self, event: Input.Changed): 155 + if not self.input.disabled: 156 + self.proc_list.update_processes(event.value.strip()) 157 + 158 + 159 + if __name__ == "__main__": 160 + ProcessTUI().run()