+160
kill-processes
+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()