This commit is contained in:
2026-06-03 21:26:54 +02:00
parent 05e6b8d061
commit d21e5175d5
125 changed files with 41986 additions and 0 deletions
+553
View File
@@ -0,0 +1,553 @@
import os
import subprocess
from fabric.utils import exec_shell_command_async, idle_add, remove_handler
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.entry import Entry
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from gi.repository import Gdk, GLib, Gtk
import config.data as data
import modules.icons as icons
class TmuxManager(Box):
def __init__(self, **kwargs):
super().__init__(
name="tmux-manager",
visible=False,
all_visible=False,
**kwargs,
)
self.notch = kwargs["notch"]
self.selected_index = -1 # Track the selected item index
self._arranger_handler: int = 0
self.viewport = Box(name="viewport", spacing=4, orientation="v")
self.session_name_entry = Entry(
name="session-name-entry",
placeholder="Create Tmux Session...",
h_expand=True,
h_align="fill",
on_activate=lambda entry, *_: self.create_session(entry.get_text()),
on_key_press_event=self.on_entry_key_press,
)
self.session_name_entry.props.xalign = 0.5
self.scrolled_window = ScrolledWindow(
name="scrolled-window",
spacing=10,
h_expand=True,
v_expand=True,
h_align="fill",
v_align="fill",
child=self.viewport,
propagate_width=False,
propagate_height=False,
)
self.header_box = Box(
name="header_box",
spacing=10,
orientation="h",
children=[
Button(
name="new-session-button",
child=Label(name="new-session-label", markup=icons.add),
tooltip_text="Create New Session",
on_clicked=lambda *_: self.create_session(self.session_name_entry.get_text()),
),
self.session_name_entry,
Button(
name="close-button",
child=Label(name="close-label", markup=icons.cancel),
tooltip_text="Exit",
on_clicked=lambda *_: self.close_manager()
),
],
)
self.tmux_box = Box(
name="tmux-box",
spacing=10,
h_expand=True,
orientation="v",
children=[
self.header_box,
self.scrolled_window,
],
)
self.add(self.tmux_box)
self.show_all()
def close_manager(self):
"""Close the tmux manager"""
self.viewport.children = []
self.selected_index = -1 # Reset selection
self.notch.close_notch()
def open_manager(self):
"""Open the tmux manager and refresh sessions"""
self.refresh_sessions()
self.session_name_entry.set_text("")
self.session_name_entry.grab_focus()
def refresh_sessions(self):
"""Get tmux sessions and populate the viewport"""
remove_handler(self._arranger_handler) if self._arranger_handler else None
self.viewport.children = []
self.selected_index = -1 # Clear selection when viewport changes
# Get tmux sessions
sessions = self.get_tmux_sessions()
if not sessions:
# Create a container box to better center the message
container = Box(
name="no-tmux-container",
orientation="v",
h_align="center",
v_align="center",
h_expand=True,
v_expand=True
)
# Show a message if no sessions
label = Label(
name="no-tmux",
markup=icons.terminal,
h_align="center",
v_align="center",
)
container.add(label)
self.viewport.add(container)
return
# Add session slots to viewport
for session in sessions:
self.viewport.add(self.create_session_slot(session))
def get_tmux_sessions(self):
"""Get list of tmux sessions"""
try:
result = subprocess.run(
["tmux", "list-sessions", "-F", "#{session_name}"],
capture_output=True,
text=True
)
if result.returncode == 0:
return [s.strip() for s in result.stdout.strip().split('\n') if s.strip()]
return []
except Exception as e:
print(f"Error getting tmux sessions: {e}")
return []
def create_session_slot(self, session_name):
"""Create a button for a tmux session"""
# Create an entry for inline editing (initially hidden)
name_entry = Entry(
name="session-name-entry",
text=session_name,
visible=False,
on_activate=lambda entry, *_: self.finish_rename(button, session_name, entry),
on_key_press_event=self.on_rename_key_press,
)
# Create the label showing the session name
name_label = Label(
name="app-label",
label=session_name,
ellipsization="end",
v_align="center",
h_align="center",
)
# Session slot content box
slot_box = Box(
name="slot-box",
orientation="h",
spacing=10,
children=[
Label(
name="tmux-icon",
markup=icons.terminal, # Use existing terminal icon
),
name_label,
name_entry,
],
)
button = Button(
name="slot-button", # reuse existing CSS styling
child=slot_box,
tooltip_text=f"Attach to session: {session_name}",
on_clicked=lambda *_: self.attach_to_session(session_name),
can_focus=True, # Ensure the button can receive focus
)
# Add double-click handler to start renaming
button.connect("button-press-event", self.on_session_click, session_name, name_label, name_entry)
# Add key press handler for 'r' to rename
button.connect("key-press-event", self.on_slot_key_press, session_name, name_label, name_entry)
# Store reference to entry and label in button for later access
button.name_entry = name_entry
button.name_label = name_label
button.session_name = session_name
return button
def on_session_click(self, button, event, session_name, label, entry):
"""Handle clicks on session buttons"""
# Handle double-click to rename
if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS and event.button == 1:
self.start_rename(button, session_name, label, entry)
return True
# Handle right click for context menu
elif event.button == 3:
menu = Gtk.Menu()
# Rename option
rename_item = Gtk.MenuItem(label="Rename")
rename_item.connect("activate", lambda _: self.start_rename(button, session_name, label, entry))
menu.append(rename_item)
# Kill option
kill_item = Gtk.MenuItem(label="Kill Session")
kill_item.connect("activate", lambda _: self.kill_session(session_name))
menu.append(kill_item)
menu.show_all()
menu.popup_at_pointer(event)
return True
return False
def start_rename(self, button, session_name, label, entry):
"""Start inline renaming of a session"""
# Hide label, show entry
label.set_visible(False)
entry.set_visible(True)
# Focus entry and select all text
entry.grab_focus()
entry.select_region(0, -1)
# Mark button as being edited
button.get_style_context().add_class("editing")
def finish_rename(self, button, old_name, entry):
"""Finish renaming a session"""
new_name = entry.get_text().strip()
# Only rename if the name changed and isn't empty
if new_name and new_name != old_name:
self.rename_session(old_name, new_name)
# Reset UI state
self.cancel_rename(button)
def cancel_rename(self, button):
"""Cancel renaming operation"""
# Restore original view
button.name_entry.set_visible(False)
button.name_label.set_visible(True)
# Remove editing style
button.get_style_context().remove_class("editing")
# Return focus to session name entry
self.session_name_entry.grab_focus()
def on_rename_key_press(self, entry, event):
"""Handle key presses in the rename entry"""
if event.keyval == Gdk.KEY_Escape:
# Find the parent button
parent = entry.get_parent()
while parent and not isinstance(parent, Button):
parent = parent.get_parent()
if parent:
self.cancel_rename(parent)
return True
return False
def on_session_right_click(self, button, event, session_name):
"""Handle right-click on a session button to show context menu"""
if event.button == 3: # Right click
menu = Gtk.Menu()
# Rename option
rename_item = Gtk.MenuItem(label="Rename")
rename_item.connect("activate", lambda _: self.start_rename(
button,
session_name,
button.name_label,
button.name_entry
))
menu.append(rename_item)
# Kill option
kill_item = Gtk.MenuItem(label="Kill Session")
kill_item.connect("activate", lambda _: self.kill_session(session_name))
menu.append(kill_item)
menu.show_all()
menu.popup_at_pointer(event)
return True
return False
def on_entry_key_press(self, widget, event):
"""Handle key press events in the entry"""
if event.keyval == Gdk.KEY_Escape:
self.close_manager()
return True
# Custom navigation with UP/DOWN keys removed
return False
def scroll_to_selected(self, button):
"""Scroll to ensure the selected button is visible"""
def scroll():
adj = self.scrolled_window.get_vadjustment()
alloc = button.get_allocation()
if alloc.height == 0:
return False # Retry if allocation isn't ready
y = alloc.y
height = alloc.height
page_size = adj.get_page_size()
current_value = adj.get_value()
# Calculate visible boundaries
visible_top = current_value
visible_bottom = current_value + page_size
if y < visible_top:
# Item above viewport - align to top
adj.set_value(y)
elif y + height > visible_bottom:
# Item below viewport - align to bottom
new_value = y + height - page_size
adj.set_value(new_value)
# No action if already fully visible
return False
GLib.idle_add(scroll)
def create_session(self, session_name):
"""Create a new tmux session"""
if not session_name:
# Get existing session names
existing_sessions = self.get_tmux_sessions()
# Find the next available number
counter = 0
while str(counter) in existing_sessions:
counter += 1
session_name = str(counter)
try:
# Clean the session name (replace spaces with underscores)
clean_name = session_name.strip().replace(" ", "_")
# Create session
subprocess.run(
["tmux", "new-session", "-d", "-s", clean_name],
check=True
)
# Refresh the session list
self.refresh_sessions()
# Clear entry
self.session_name_entry.set_text("")
# Launch a terminal and attach to this session
terminal_cmd = self.get_terminal_command(f"tmux attach-session -t {clean_name}")
exec_shell_command_async(terminal_cmd)
# Close manager
self.close_manager()
except Exception as e:
print(f"Error creating tmux session: {e}")
def attach_to_session(self, session_name):
"""Attach to an existing tmux session"""
try:
# Launch a terminal and attach to this session
terminal_cmd = self.get_terminal_command(f"tmux attach-session -t {session_name}")
exec_shell_command_async(terminal_cmd)
self.close_manager()
except Exception as e:
print(f"Error attaching to tmux session: {e}")
def get_terminal_command(self, cmd):
"""Get terminal command based on configured terminal or available terminals"""
# First try to use the configured terminal command
if hasattr(data, 'TERMINAL_COMMAND') and data.TERMINAL_COMMAND:
parts = data.TERMINAL_COMMAND.split()
terminal = parts[0]
try:
# Check if the configured terminal is available
subprocess.run(["which", terminal], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return f"{data.TERMINAL_COMMAND} {cmd}"
except subprocess.CalledProcessError:
# If configured terminal is not available, fall back to defaults
pass
# Fallback to checking available terminals
terminals = [
("kitty", f"kitty -e {cmd}"),
("alacritty", f"alacritty -e {cmd}"),
("foot", f"foot {cmd}"),
("gnome-terminal", f"gnome-terminal -- {cmd}"),
("konsole", f"konsole -e {cmd}"),
("xfce4-terminal", f"xfce4-terminal -e '{cmd}'"),
]
for term, term_cmd in terminals:
try:
# Check if terminal is available
subprocess.run(["which", term], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return term_cmd
except subprocess.CalledProcessError:
continue
# Default fallback
return f"kitty -e {cmd}"
def rename_session_dialog(self, old_name):
"""Show dialog to rename a session"""
dialog = Gtk.Dialog(
title="Rename Session",
transient_for=None,
flags=0
)
dialog.add_buttons(
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OK, Gtk.ResponseType.OK
)
content_area = dialog.get_content_area()
entry = Gtk.Entry()
entry.set_text(old_name)
entry.set_activates_default(True)
content_area.add(entry)
dialog.set_default_response(Gtk.ResponseType.OK)
dialog.show_all()
response = dialog.run()
if response == Gtk.ResponseType.OK:
new_name = entry.get_text()
if new_name and new_name != old_name:
self.rename_session(old_name, new_name)
dialog.destroy()
def rename_session(self, old_name, new_name):
"""Rename a tmux session"""
try:
# Clean the session name (replace spaces with underscores)
clean_name = new_name.strip().replace(" ", "_")
# Rename session
subprocess.run(
["tmux", "rename-session", "-t", old_name, clean_name],
check=True
)
# Refresh the session list
self.refresh_sessions()
except Exception as e:
print(f"Error renaming tmux session: {e}")
def kill_session(self, session_name):
"""Kill a tmux session"""
try:
# Kill session
subprocess.run(
["tmux", "kill-session", "-t", session_name],
check=True
)
# Refresh the session list
self.refresh_sessions()
# Close the notch after killing session
self.close_manager()
except Exception as e:
print(f"Error killing tmux session: {e}")
# Add new method to handle key presses on session slots
def on_slot_key_press(self, button, event, session_name, label, entry):
"""Handle key presses on session buttons"""
# Print debugging info
print(f"Key pressed: {event.keyval}, State: {event.state}")
# Check if 'r' key was pressed for renaming
if event.keyval == Gdk.KEY_r:
self.start_rename(button, session_name, label, entry)
return True
# Check for 'K' (capital K) which indicates Shift is pressed
elif event.keyval == Gdk.KEY_K:
print("Shift+K detected - killing session without confirmation")
self.kill_session(session_name)
return True
# Check for lowercase 'k'
elif event.keyval == Gdk.KEY_k:
print("Regular k detected - showing confirmation")
self.show_kill_confirmation_menu(button, session_name)
return True
# Check if Delete key was pressed for killing session
elif event.keyval == Gdk.KEY_Delete:
self.show_kill_confirmation_menu(button, session_name)
return True
return False
def show_kill_confirmation_menu(self, button, session_name):
"""Show a confirmation menu for killing a session"""
menu = Gtk.Menu()
# Confirmation message as a disabled menu item
msg_item = Gtk.MenuItem(label=f"Kill session '{session_name}'?")
msg_item.set_sensitive(False)
menu.append(msg_item)
# Separator
menu.append(Gtk.SeparatorMenuItem())
# Confirm option
confirm_item = Gtk.MenuItem(label="Confirm")
confirm_item.connect("activate", lambda _: self.kill_session(session_name))
menu.append(confirm_item)
# Cancel option
cancel_item = Gtk.MenuItem(label="Cancel")
# Close notch on cancel
cancel_item.connect("activate", lambda _: self.close_manager())
menu.append(cancel_item)
menu.show_all()
# Show the menu positioned at the button
menu.popup_at_widget(
button,
Gdk.Gravity.SOUTH_WEST,
Gdk.Gravity.NORTH_WEST,
None
)