archive
This commit is contained in:
182 files changed
+18062
-277
No files matched your search
@@ -0,0 +1,395 @@
|
||||
from collections.abc import Iterator
|
||||
|
||||
from fabric.utils import Gdk, exec_shell_command_async, idle_add, remove_handler
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.label import Label
|
||||
|
||||
from modules.launcher.base import LauncherPlugin
|
||||
from services import TmuxService
|
||||
from utils.functions import terminal_exec_command
|
||||
|
||||
|
||||
class TmuxPlugin(LauncherPlugin):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "tmux"
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
return "terminal-symbolic"
|
||||
|
||||
@property
|
||||
def keywords(self) -> list[str]:
|
||||
return ["tmux"]
|
||||
|
||||
def __init__(self, handler):
|
||||
super().__init__(handler)
|
||||
self._query_handler: int = 0
|
||||
self.current_slots: list = []
|
||||
self.tmux_service = TmuxService()
|
||||
self.sessions: list = []
|
||||
self.terminal = "kitty"
|
||||
|
||||
def on_search(self, text: str) -> None:
|
||||
self.query_tmux(text)
|
||||
|
||||
def stop(self):
|
||||
if self._query_handler:
|
||||
remove_handler(self._query_handler)
|
||||
self._query_handler = 0
|
||||
self.current_slots.clear()
|
||||
|
||||
def query_tmux(self, text: str) -> None:
|
||||
self.handler.start("tmux")
|
||||
self.current_slots.clear()
|
||||
text_stripped = text.strip()
|
||||
|
||||
commands = {
|
||||
"new": self._handle_new_command,
|
||||
"kill": self._handle_kill_command,
|
||||
"rename": self._handle_rename_command,
|
||||
"terminal": self._handle_terminal_command,
|
||||
"help": lambda t: self._show_command_help(),
|
||||
}
|
||||
|
||||
for prefix, handler in commands.items():
|
||||
if text_stripped.startswith(prefix):
|
||||
handler(text)
|
||||
return
|
||||
|
||||
self._load_and_display_sessions(text)
|
||||
|
||||
def _load_and_display_sessions(self, text: str) -> None:
|
||||
result = self.tmux_service.list_sessions()
|
||||
if not result["success"]:
|
||||
self._show_error_message(f"Failed to load sessions: {result['error']}")
|
||||
return
|
||||
|
||||
self.sessions = result["data"]
|
||||
|
||||
if not self.sessions:
|
||||
self._show_no_sessions()
|
||||
return
|
||||
|
||||
filtered_sessions = self._filter_sessions(text)
|
||||
if not filtered_sessions:
|
||||
self._show_no_results()
|
||||
return
|
||||
|
||||
self._query_handler = idle_add(
|
||||
self._bake_next_session_slot, iter(filtered_sessions), pin=True
|
||||
)
|
||||
|
||||
def _filter_sessions(self, text: str) -> Iterator[dict]:
|
||||
if not text.strip():
|
||||
return iter(self.sessions)
|
||||
|
||||
text_lower = text.lower()
|
||||
return iter(
|
||||
session
|
||||
for session in self.sessions
|
||||
if text_lower in session.get("name", "").lower()
|
||||
)
|
||||
|
||||
def _show_no_sessions(self) -> None:
|
||||
self.handler.slot_ready(
|
||||
Button(
|
||||
style_classes="app-slot tmux-no-sessions",
|
||||
child=Box(
|
||||
orientation="h",
|
||||
spacing=12,
|
||||
children=[
|
||||
Label(
|
||||
label="No active tmux sessions",
|
||||
style_classes="tmux-no-sessions-text",
|
||||
h_align="center",
|
||||
),
|
||||
],
|
||||
),
|
||||
tooltip_text="Use 'new session_name' to create a session",
|
||||
),
|
||||
"tmux",
|
||||
)
|
||||
self.handler.done()
|
||||
|
||||
def _show_no_results(self) -> None:
|
||||
self.handler.slot_ready(
|
||||
Button(
|
||||
style_classes="app-slot tmux-no-results",
|
||||
child=Box(
|
||||
orientation="h",
|
||||
spacing=12,
|
||||
children=[
|
||||
Label(
|
||||
label="No sessions match your search",
|
||||
style_classes="tmux-no-results-text",
|
||||
h_align="center",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
"tmux",
|
||||
)
|
||||
self.handler.done()
|
||||
|
||||
def _bake_next_session_slot(self, iterator: Iterator[dict]) -> bool:
|
||||
try:
|
||||
session = next(iterator)
|
||||
except StopIteration:
|
||||
idle_add(self.handler.done)
|
||||
return False
|
||||
|
||||
self._create_session_slot(session)
|
||||
return True
|
||||
|
||||
def _create_session_slot(self, session: dict) -> None:
|
||||
session_name = session.get("name", "")
|
||||
windows = session.get("windows", 0)
|
||||
created = session.get("created", "")
|
||||
attached = session.get("attached", False)
|
||||
|
||||
status_icon = "●" if attached else "○"
|
||||
status_color = "attached" if attached else "detached"
|
||||
|
||||
name_label = Label(
|
||||
label=f"{status_icon} {session_name}",
|
||||
style_classes=f"tmux-session-name tmux-{status_color}",
|
||||
h_align="start",
|
||||
max_chars_width=25,
|
||||
ellipsization="end",
|
||||
)
|
||||
|
||||
info_label = Label(
|
||||
label=f"{windows} windows • Created: {created}",
|
||||
style_classes="tmux-session-info",
|
||||
h_align="start",
|
||||
max_chars_width=40,
|
||||
ellipsization="end",
|
||||
)
|
||||
|
||||
slot_widget = Button(
|
||||
style_classes=f"app-slot tmux-slot tmux-{status_color}",
|
||||
child=Box(
|
||||
orientation="v",
|
||||
spacing=4,
|
||||
children=[
|
||||
name_label,
|
||||
info_label,
|
||||
],
|
||||
),
|
||||
tooltip_text=f"Click to attach\nShift+Enter to kill session\nAlt+Enter to rename session\nAttached: {'Yes' if attached else 'No'}",
|
||||
on_clicked=lambda *_: self._attach_session(session_name),
|
||||
)
|
||||
|
||||
slot_widget.connect(
|
||||
"key-press-event",
|
||||
lambda widget, event, s_name=session_name: self._on_slot_key_press(
|
||||
widget, event, s_name
|
||||
),
|
||||
)
|
||||
|
||||
slot_info = {
|
||||
"widget": slot_widget,
|
||||
"name_label": name_label,
|
||||
"info_label": info_label,
|
||||
"session_name": session_name,
|
||||
"session": session,
|
||||
}
|
||||
self.current_slots.append(slot_info)
|
||||
self.handler.slot_ready(slot_widget, "tmux")
|
||||
|
||||
def _attach_session(self, session_name: str) -> None:
|
||||
cmd = f"tmux attach -t {session_name}"
|
||||
term_cmd = terminal_exec_command(self.terminal, cmd)
|
||||
exec_shell_command_async(term_cmd)
|
||||
self.handler.launched()
|
||||
|
||||
def _kill_session(self, session_name: str) -> None:
|
||||
result = self.tmux_service.kill_session(session_name)
|
||||
if not result["success"]:
|
||||
self._show_error_message(f"Failed to kill session: {result['error']}")
|
||||
return
|
||||
|
||||
self._refresh_session_list()
|
||||
|
||||
def _on_slot_key_press(self, widget, event, session_name: str) -> bool:
|
||||
if not event:
|
||||
return False
|
||||
|
||||
# Shift+Enter: Kill session
|
||||
if (
|
||||
event.keyval == Gdk.KEY_Return or event.keyval == Gdk.KEY_KP_Enter
|
||||
) and event.state & Gdk.ModifierType.SHIFT_MASK:
|
||||
return self._kill_session(session_name) or True
|
||||
|
||||
# Alt+Enter: Rename session (pre-fill input)
|
||||
if (
|
||||
event.keyval == Gdk.KEY_Return or event.keyval == Gdk.KEY_KP_Enter
|
||||
) and event.state & Gdk.ModifierType.MOD1_MASK:
|
||||
# This would require updating the input field, which we can't easily do directly from here
|
||||
# without a callback or reference to the entry.
|
||||
# For now, we can just show a message or handle it if we add that capability.
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
def _handle_new_command(self, text: str) -> None:
|
||||
if not text.strip().endswith(";"):
|
||||
self._show_error_message(
|
||||
"New session command must end with ';'\n\nUsage: new session_name;"
|
||||
)
|
||||
return
|
||||
|
||||
command_text = text.strip()[:-1]
|
||||
parts = command_text.split()
|
||||
|
||||
if len(parts) < 2:
|
||||
self._show_command_help("new")
|
||||
return
|
||||
|
||||
session_name = parts[1]
|
||||
result = self.tmux_service.new_session(session_name)
|
||||
if not result["success"]:
|
||||
self._show_error_message(f"Error creating session: {result['error']}")
|
||||
return
|
||||
|
||||
if hasattr(self.handler, "clear_input"):
|
||||
self.handler.clear_input()
|
||||
self._refresh_session_list()
|
||||
# Optionally attach immediately
|
||||
self._attach_session(session_name)
|
||||
|
||||
def _handle_kill_command(self, text: str) -> None:
|
||||
if text.strip() == "kill":
|
||||
self._load_and_display_sessions("")
|
||||
return
|
||||
|
||||
if not text.strip().endswith(";"):
|
||||
self._show_error_message(
|
||||
"Kill command must end with ';'\n\nUsage: kill session_name;"
|
||||
)
|
||||
return
|
||||
|
||||
command_text = text.strip()[:-1]
|
||||
parts = command_text.split()
|
||||
|
||||
if len(parts) < 2:
|
||||
self._show_command_help("kill")
|
||||
return
|
||||
|
||||
session_name = parts[1]
|
||||
self._kill_session(session_name)
|
||||
|
||||
def _handle_rename_command(self, text: str) -> None:
|
||||
if text.strip() == "rename":
|
||||
self._load_and_display_sessions("")
|
||||
return
|
||||
|
||||
if not text.strip().endswith(";"):
|
||||
self._show_error_message(
|
||||
"Rename command must end with ';'\n\nUsage: rename old_name new_name;"
|
||||
)
|
||||
return
|
||||
|
||||
command_text = text.strip()[:-1]
|
||||
parts = command_text.split()
|
||||
|
||||
if len(parts) < 3:
|
||||
self._show_command_help("rename")
|
||||
return
|
||||
|
||||
old_name = parts[1]
|
||||
new_name = parts[2]
|
||||
|
||||
result = self.tmux_service.rename_session(old_name, new_name)
|
||||
if not result["success"]:
|
||||
self._show_error_message(f"Error renaming session: {result['error']}")
|
||||
return
|
||||
|
||||
if hasattr(self.handler, "clear_input"):
|
||||
self.handler.clear_input()
|
||||
self._refresh_session_list()
|
||||
|
||||
def _handle_terminal_command(self, text: str) -> None:
|
||||
parts = text.strip().split()
|
||||
if len(parts) < 2:
|
||||
self._show_command_help("terminal")
|
||||
return
|
||||
self.terminal = parts[1]
|
||||
self._refresh_session_list()
|
||||
|
||||
def _show_command_help(self, command: str = None) -> None:
|
||||
help_texts = {
|
||||
"new": "Usage: new session_name;\nExample: new dev;",
|
||||
"kill": "Usage: kill session_name;\nExample: kill dev;",
|
||||
"rename": "Usage: rename old_name new_name;\nExample: rename dev work;",
|
||||
"terminal": "Usage: terminal terminal_name\nExample: terminal kitty",
|
||||
}
|
||||
|
||||
help_text = help_texts.get(
|
||||
command,
|
||||
"""Available commands:
|
||||
• new session_name; - Create new session
|
||||
• kill session_name; - Kill session
|
||||
• rename old_name new_name; - Rename session
|
||||
• terminal terminal_name - Set terminal emulator
|
||||
• help - Show this help
|
||||
|
||||
Note: Commands with arguments must end with semicolon (;)
|
||||
Click to attach, Shift+Enter to kill""",
|
||||
)
|
||||
|
||||
self.handler.slot_ready(
|
||||
Button(
|
||||
style_classes="app-slot tmux-help",
|
||||
child=Box(
|
||||
orientation="h",
|
||||
spacing=12,
|
||||
children=[
|
||||
Label(
|
||||
label=help_text,
|
||||
style_classes="tmux-help-text",
|
||||
h_align="center",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
"tmux",
|
||||
)
|
||||
self.handler.done()
|
||||
|
||||
def _show_error_message(self, message: str) -> None:
|
||||
self.handler.slot_ready(
|
||||
Button(
|
||||
style_classes="app-slot tmux-error",
|
||||
child=Box(
|
||||
orientation="h",
|
||||
spacing=12,
|
||||
children=[
|
||||
Label(
|
||||
label=message,
|
||||
style_classes="tmux-error-text",
|
||||
h_align="center",
|
||||
),
|
||||
],
|
||||
),
|
||||
on_clicked=lambda *_: self.handler.launched(),
|
||||
),
|
||||
"tmux",
|
||||
)
|
||||
self.handler.done()
|
||||
|
||||
def _refresh_session_list(self) -> None:
|
||||
self.current_slots.clear()
|
||||
if hasattr(self.handler, "clear_viewport"):
|
||||
self.handler.clear_viewport()
|
||||
idle_add(self._show_session_list)
|
||||
|
||||
def _show_session_list(self) -> None:
|
||||
self.handler.start("tmux")
|
||||
self._load_and_display_sessions("")
|
||||
|
||||
def handle_external(self, command: str, args: str) -> None:
|
||||
# Map external commands if needed
|
||||
pass
|
||||
Reference in new issue
Block a user