woofshahenzup

Pymenu.py

Sep 4th, 2025
44
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 56.77 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. import gi
  3. gi.require_version('Gtk', '3.0')
  4. from gi.repository import Gtk, Gdk, GdkPixbuf, Gio, GLib
  5. import xml.etree.ElementTree as ET
  6. import os
  7. import subprocess
  8. import sys
  9. import shlex
  10. import json
  11. import urllib.parse
  12.  
  13. # Import the pango module using GObject Introspection
  14. gi.require_version('Pango', '1.0')
  15. from gi.repository import Pango
  16.  
  17. PROFILE_PIC = "/root/.face"
  18. PROFILE_MANAGER = "/usr/local/bin/ProfileManager.py"
  19. SHUTDOWN_CMD = "/usr/local/bin/apagado-avatar.py"
  20. CONFIG_FILE = "/root/.config/pymenu.json"
  21.  
  22. class ConfigManager:
  23. """Manages reading and writing the application's JSON configuration."""
  24. def __init__(self, config_file=CONFIG_FILE):
  25. self.config_file = config_file
  26. self.config = self.load_config()
  27.  
  28. def get_default_config(self):
  29. """Return the default configuration matching the current script's aesthetics."""
  30. return {
  31. "window": {
  32. "width": 700,
  33. "height": 850,
  34. "halign": "center",
  35. "icon_size": 32,
  36. "profile_pic_size": 128
  37. },
  38. "font": {
  39. "family": "sans-serif",
  40. "size_categories": 15000,
  41. "size_names": 10000,
  42. "size_header": 16000
  43. },
  44. "colors": {
  45. "background_opacity": 0.7,
  46. "background": "rgba(0, 0, 0, 0.7)",
  47. "border": "rgba(255, 255, 255, 0.1)",
  48. "text_normal": "#D8DEE9",
  49. "text_header_os": "#D8DEE9",
  50. "text_header_kernel": "#D0883A",
  51. "text_header_hostname": "#88C0D0",
  52. "hover_background": "rgba(255, 255, 255, 0.1)",
  53. "selected_background": "rgba(255, 255, 255, 0.2)",
  54. "selected_text": "#ECEFF4",
  55. "button_normal_background": "rgba(0,0,0,0.6)",
  56. "button_text": "#ECEFF4",
  57. "categories_background": "rgba(0,0,0,0.4)"
  58. },
  59. "paths": {
  60. "profile_pic": "/root/.face",
  61. "profile_manager": "/usr/local/bin/ProfileManager.py",
  62. "shutdown_cmd": "/usr/local/bin/apagado-avatar.py"
  63. }
  64. }
  65.  
  66. def load_config(self):
  67. """Load configuration from the JSON file or create a default one."""
  68. if not os.path.exists(self.config_file):
  69. print(f"Config file not found. Creating default config at {self.config_file}")
  70. self.save_config(self.get_default_config())
  71. return self.get_default_config()
  72.  
  73. try:
  74. with open(self.config_file, 'r') as f:
  75. config = json.load(f)
  76. # Merge with default config to ensure all keys exist
  77. default_config = self.get_default_config()
  78. for key in default_config:
  79. if key not in config:
  80. config[key] = default_config[key]
  81. elif isinstance(config[key], dict) and isinstance(default_config[key], dict):
  82. for sub_key in default_config[key]:
  83. if sub_key not in config[key]:
  84. config[key][sub_key] = default_config[key][sub_key]
  85. return config
  86. except (IOError, json.JSONDecodeError) as e:
  87. print(f"Error loading config file: {e}. Using default settings.")
  88. return self.get_default_config()
  89.  
  90. def save_config(self, config_data):
  91. """Save configuration to the JSON file."""
  92. config_dir = os.path.dirname(self.config_file)
  93. if not os.path.exists(config_dir):
  94. os.makedirs(config_dir, exist_ok=True)
  95.  
  96. with open(self.config_file, 'w') as f:
  97. json.dump(config_data, f, indent=4)
  98.  
  99.  
  100. class JWMMenuParser:
  101. def __init__(self, jwm_file="/root/.jwmrc"):
  102. self.jwm_file = jwm_file
  103. self.applications = {}
  104. self.icon_paths = []
  105. self.tray_config = None
  106.  
  107. def parse_tray_config(self):
  108. """Parse JWM config (jwmrc-tray if it exists) to get tray position and size"""
  109. tray_info = {
  110. 'height': 30,
  111. 'width': 1300,
  112. 'valign': 'bottom',
  113. 'halign': 'center',
  114. 'layer': 'above',
  115. 'autohide': 'off'
  116. }
  117.  
  118. try:
  119. tray_file = os.path.expanduser("~/.jwmrc-tray")
  120. if os.path.exists(tray_file):
  121. target_file = tray_file
  122. else:
  123. target_file = self.jwm_file
  124.  
  125. if not os.path.exists(target_file):
  126. print(f"JWM file not found: {target_file}")
  127. return tray_info
  128.  
  129. tree = ET.parse(target_file)
  130. root = tree.getroot()
  131.  
  132. tray_element = root.find('.//Tray')
  133. if tray_element is not None:
  134. tray_info['height'] = int(tray_element.get('height', '30'))
  135. tray_info['width'] = int(tray_element.get('width', '1300'))
  136. tray_info['valign'] = tray_element.get('valign', 'bottom').lower()
  137. tray_info['halign'] = tray_element.get('halign', 'center').lower()
  138. tray_info['layer'] = tray_element.get('layer', 'above').lower()
  139. tray_info['autohide'] = tray_element.get('autohide', 'off').lower()
  140.  
  141. print(f"Tray config detected from {target_file}: {tray_info}")
  142.  
  143. except Exception as e:
  144. print(f"Error parsing tray config: {e}")
  145.  
  146. self.tray_config = tray_info
  147. return tray_info
  148.  
  149. def parse_jwm_menu(self):
  150. """Parse JWM menu file and extract applications"""
  151. try:
  152. if not os.path.exists(self.jwm_file):
  153. print(f"JWM file not found: {self.jwm_file}")
  154. return self.get_fallback_applications()
  155.  
  156. tree = ET.parse(self.jwm_file)
  157. root = tree.getroot()
  158.  
  159. self.icon_paths = self.extract_icon_paths(root)
  160.  
  161. applications = {}
  162. for menu in root.findall('.//Menu'):
  163. label = menu.get('label', 'Unknown')
  164. if label:
  165. apps = self.extract_programs_from_menu(menu)
  166. if apps:
  167. applications[label] = apps
  168.  
  169. root_programs = []
  170. for program in root.findall('./Program'):
  171. label = program.get('label', '')
  172. icon = program.get('icon', '')
  173. tooltip = program.get('tooltip', '')
  174. command = program.text.strip() if program.text else ''
  175.  
  176. if label and command:
  177. app_info = {
  178. 'Name': label,
  179. 'Exec': command,
  180. 'Icon': icon,
  181. 'Comment': tooltip or label,
  182. 'Terminal': 'terminal' in command.lower() or 'urxvt' in command.lower(),
  183. 'Categories': []
  184. }
  185.  
  186. if label.lower() in ['help', 'ayuda']:
  187. if 'Help' not in applications:
  188. applications['Help'] = []
  189. applications['Help'].append(app_info)
  190. elif label.lower() in ['leave', 'salir', 'exit', 'logout']:
  191. if 'Leave' not in applications:
  192. applications['Leave'] = []
  193. applications['Leave'].append(app_info)
  194. else:
  195. root_programs.append(app_info)
  196.  
  197. if root_programs:
  198. applications['System'] = applications.get('System', []) + root_programs
  199.  
  200. return applications if applications else self.get_fallback_applications()
  201.  
  202. except Exception as e:
  203. print(f"Error parsing JWM menu: {e}")
  204. return self.get_fallback_applications()
  205.  
  206. def extract_icon_paths(self, root):
  207. """Extract icon paths from JWM config"""
  208. paths = []
  209. for iconpath in root.findall('.//IconPath'):
  210. if iconpath.text:
  211. paths.append(iconpath.text.strip())
  212.  
  213. if not paths:
  214. paths = [
  215. "/usr/share/pixmaps",
  216. "/usr/share/icons/hicolor/48x48/apps",
  217. "/usr/share/icons/hicolor/32x32/apps",
  218. "/usr/share/icons/hicolor/64x64/apps",
  219. "/usr/local/lib/X11/pixmaps",
  220. "/usr/share/pixmaps/puppy"
  221. ]
  222. return paths
  223.  
  224. def extract_programs_from_menu(self, menu_element):
  225. """Extract program entries from a menu element"""
  226. programs = []
  227. for program in menu_element.findall('./Program'):
  228. label = program.get('label', '')
  229. icon = program.get('icon', '')
  230. tooltip = program.get('tooltip', '')
  231. command = program.text.strip() if program.text else ''
  232.  
  233. if label and command:
  234. app_info = {
  235. 'Name': label,
  236. 'Exec': command,
  237. 'Icon': icon,
  238. 'Comment': tooltip or label,
  239. 'Terminal': 'terminal' in command.lower() or 'urxvt' in command.lower(),
  240. 'Categories': []
  241. }
  242. programs.append(app_info)
  243.  
  244. return programs
  245.  
  246. def get_fallback_applications(self):
  247. """Fallback applications if JWM parsing fails"""
  248. return {
  249. 'System': [
  250. {'Name': 'Terminal', 'Exec': 'lxterminal', 'Icon': 'terminal', 'Comment': 'Terminal emulator', 'Terminal': False, 'Categories': []},
  251. {'Name': 'File Manager', 'Exec': 'rox', 'Icon': 'folder', 'Comment': 'File manager', 'Terminal': False, 'Categories': []},
  252. ],
  253. 'Internet': [
  254. {'Name': 'Firefox', 'Exec': 'firefox', 'Icon': 'firefox', 'Comment': 'Web browser', 'Terminal': False, 'Categories': []},
  255. ]
  256. }
  257.  
  258. class ArcMenuLauncher(Gtk.Window):
  259. def __init__(self, icon_size=None, jwm_file=None, x=None, y=None):
  260. super().__init__(title="ArcMenu Launcher")
  261.  
  262. self.config_manager = ConfigManager()
  263. self.config = self.config_manager.config
  264.  
  265. # Use icon_size from config, or fallback to default
  266. self.icon_size = self.config['window'].get('icon_size', 32)
  267.  
  268. self.parser = JWMMenuParser(jwm_file or "/root/.jwmrc")
  269.  
  270. self.tray_config = self.parser.parse_tray_config()
  271. self.applications = self.parser.parse_jwm_menu()
  272. self.apps_flowbox = None
  273. self.categories_listbox = None
  274. self.search_entry = None
  275. self.profile_image = None
  276.  
  277. self.icon_cache = {}
  278. self.current_category = "All"
  279. self.hover_timeout = None
  280. self.restore_timeout = None
  281. self.mouse_in_menu = False
  282.  
  283. self.selected_category = None
  284. self.hovered_category = None
  285. self.selected_category_row = None
  286.  
  287. self.pos_x = x
  288. self.pos_y = y
  289.  
  290. screen = Gdk.Screen.get_default()
  291. visual = screen.get_rgba_visual()
  292. if visual and screen.is_composited():
  293. self.set_visual(visual)
  294. self.set_app_paintable(True)
  295.  
  296. self.apply_css()
  297. self.setup_window()
  298. self.create_interface()
  299.  
  300. jwm_file_path = jwm_file or "/root/.jwmrc"
  301. self.jwm_file = Gio.File.new_for_path(jwm_file_path)
  302. self.file_monitor = self.jwm_file.monitor_file(Gio.FileMonitorFlags.NONE, None)
  303. self.file_monitor.connect("changed", self.on_jwm_file_changed)
  304. print(f"Now monitoring JWM file for changes: {jwm_file_path}")
  305.  
  306. def apply_css(self):
  307. """Loads and applies CSS from the configuration."""
  308. colors = self.config['colors']
  309.  
  310. css = f"""
  311. GtkWindow, GtkEventBox {{
  312. background-color: {colors['background']};
  313. border-radius: 0px;
  314. box-shadow: none;
  315. border: none;
  316. }}
  317.  
  318. .menu-window {{
  319. background-color: {colors['background']};
  320. border-radius: 12px;
  321. box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.3);
  322. border: 1px solid {colors['border']};
  323. padding: 5px 10px 10px 10px;
  324. }}
  325.  
  326. listbox {{
  327. padding: 2px;
  328. }}
  329.  
  330. listbox row {{
  331. background-color: {self.config['colors'].get('categories_background', 'rgba(0,0,0,0.4)')};
  332. color: {self.config['colors']['text_normal']};
  333. border-radius: 6px;
  334. padding: 2px;
  335. margin: 1px;
  336. min-height: 26px;
  337. }}
  338.  
  339. listbox row:selected {{
  340. background-color: {colors['selected_background']};
  341. color: {colors['selected_text']};
  342. }}
  343.  
  344. listbox row:hover {{
  345. background-color: {colors['hover_background']};
  346. }}
  347.  
  348. button {{
  349. border-radius: 8px;
  350. padding: 5px;
  351. background-color: {colors['button_normal_background']};
  352. color: {colors['button_text']};
  353. border: none;
  354. }}
  355.  
  356. listbox row.selected-category {{
  357. background-color: {colors['selected_background']};
  358. color: {colors['selected_text']};
  359. }}
  360.  
  361. button:hover {{
  362. background-color: {colors['hover_background']};
  363. }}
  364.  
  365. .app-box {{
  366. min-width: {self.icon_size + 30}px;
  367. }}
  368. """
  369. style_provider = Gtk.CssProvider()
  370. style_provider.load_from_data(css.encode('utf-8'))
  371. Gtk.StyleContext.add_provider_for_screen(
  372. Gdk.Screen.get_default(),
  373. style_provider,
  374. Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
  375. )
  376.  
  377. def on_jwm_file_changed(self, monitor, file, other_file, event_type):
  378. """Reload the menu when the JWM file is modified"""
  379. if event_type == Gio.FileMonitorEvent.CHANGES_DONE_HINT:
  380. print("JWM file changed, reloading menu...")
  381. self.applications = self.parser.parse_jwm_menu()
  382. for child in self.get_children():
  383. self.remove(child)
  384. self.create_interface()
  385. self.show_all()
  386. self.present()
  387.  
  388. def get_hostname(self):
  389. """Get the system hostname from /etc/hostname"""
  390. try:
  391. with open("/etc/hostname", "r") as f:
  392. hostname = f.read().strip()
  393. if hostname:
  394. return hostname
  395. except Exception as e:
  396. print(f"Error reading hostname: {e}")
  397. return "Unknown Host"
  398.  
  399. def get_os_info(self):
  400. """Get OS name and kernel version"""
  401. try:
  402. os_name = "Unknown OS"
  403. for path in ['/etc/os-release', '/usr/lib/os-release']:
  404. if os.path.exists(path):
  405. with open(path, 'r') as f:
  406. for line in f:
  407. if line.startswith('PRETTY_NAME='):
  408. os_name = line.split('=', 1)[1].strip().strip('"')
  409. break
  410. break
  411. else:
  412. result = subprocess.run(['uname', '-sr'], capture_output=True, text=True)
  413. if result.returncode == 0:
  414. os_name = result.stdout.strip()
  415.  
  416. result = subprocess.run(['uname', '-r'], capture_output=True, text=True)
  417. kernel = result.stdout.strip() if result.returncode == 0 else "Unknown"
  418.  
  419. return os_name, kernel
  420.  
  421. except Exception as e:
  422. print(f"Error getting OS info: {e}")
  423. return "Unknown OS", "Unknown"
  424.  
  425. def calculate_menu_position(self):
  426. """Calculate menu position based on config and screen size"""
  427. display = Gdk.Display.get_default()
  428. monitor = display.get_primary_monitor()
  429. geometry = monitor.get_geometry()
  430.  
  431. screen_width = geometry.width
  432. screen_height = geometry.height
  433.  
  434. menu_width = self.config['window']['width']
  435. menu_height = self.config['window']['height']
  436.  
  437. # New logic to read 'halign' from config
  438. menu_halign = self.config['window'].get('halign', None)
  439.  
  440. if menu_halign == 'left':
  441. x = 10
  442. elif menu_halign == 'right':
  443. x = screen_width - menu_width - 10
  444. else: # Defaults to 'center' or existing JWM logic
  445. tray_halign = self.tray_config['halign']
  446. if tray_halign == 'left':
  447. x = 10
  448. elif tray_halign == 'right':
  449. x = screen_width - menu_width - 10
  450. else:
  451. x = (screen_width - menu_width) // 2
  452.  
  453. tray_height = self.tray_config['height']
  454. tray_valign = self.tray_config['valign']
  455.  
  456. if tray_valign == 'top':
  457. y = tray_height
  458. elif tray_valign in ['bottom', 'buttom']:
  459. y = screen_height - tray_height - menu_height
  460. else:
  461. y = (screen_height - menu_height) // 2
  462.  
  463. x = max(0, min(x, screen_width - menu_width))
  464. y = max(0, min(y, screen_height - menu_height))
  465.  
  466. return int(x), int(y)
  467.  
  468. def setup_window(self):
  469. """Configure main window"""
  470. win_size = self.config['window']
  471. self.set_default_size(win_size['width'], win_size['height'])
  472.  
  473. if self.pos_x is not None and self.pos_y is not None:
  474. self.move(int(self.pos_x), int(self.pos_y))
  475. else:
  476. x, y = self.calculate_menu_position()
  477. self.move(x, y)
  478. print(f"Positioning menu at ({x}, {y}) based on tray config: {self.tray_config}")
  479.  
  480. self.set_resizable(False)
  481. self.set_decorated(False)
  482. self.set_app_paintable(True)
  483. self.set_type_hint(Gdk.WindowTypeHint.DOCK)
  484. self.set_skip_taskbar_hint(True)
  485. self.set_skip_pager_hint(True)
  486. self.connect("key-press-event", self.on_key_press)
  487. self.connect("focus-out-event", self.on_focus_out)
  488. self.connect("button-press-event", self.on_button_press)
  489.  
  490. self.show_all()
  491. self.present()
  492. self.grab_focus()
  493. self.set_keep_above(True)
  494. GLib.timeout_add(500, lambda: self.set_keep_above(False))
  495.  
  496. try:
  497. self.set_icon_name("applications-system")
  498. except:
  499. pass
  500.  
  501. def on_button_press(self, widget, event):
  502. """Allows window to be dragged if ALT key is pressed and closes on right-click"""
  503. # Close the window on right-click (button 3)
  504. if event.button == 2:
  505. Gtk.main_quit()
  506. return True
  507.  
  508. # Allows window to be dragged if ALT key is pressed
  509. if event.button == 1 and (event.state & Gdk.ModifierType.MOD1_MASK):
  510. self.begin_move_drag(event.button, int(event.x_root), int(event.y_root), event.time)
  511. return True
  512.  
  513. return False
  514.  
  515. return False
  516.  
  517. def on_key_press(self, widget, event):
  518. """Close window with Escape key"""
  519. if event.keyval == Gdk.KEY_Escape:
  520. Gtk.main_quit()
  521. return True
  522. return False
  523.  
  524. def on_focus_out(self, widget, event):
  525. """Close window when it loses focus"""
  526. Gtk.main_quit()
  527. return False
  528.  
  529. def create_interface(self):
  530. """Create the main interface"""
  531. main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
  532. main_box.get_style_context().add_class('menu-window')
  533. self.add(main_box)
  534.  
  535. header_box = self.create_header()
  536. main_box.pack_start(header_box, False, False, 0)
  537.  
  538. main_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)
  539.  
  540. content_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
  541. main_box.pack_start(content_box, True, True, 0)
  542.  
  543. content_box.pack_start(self.create_categories_sidebar(), False, False, 0)
  544. content_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.VERTICAL), False, False, 0)
  545. content_box.pack_start(self.create_applications_area(), True, True, 0)
  546.  
  547. main_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)
  548.  
  549. bottom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
  550. bottom_box.set_margin_top(6)
  551. bottom_box.set_margin_bottom(6)
  552. bottom_box.set_margin_start(10)
  553. bottom_box.set_margin_end(10)
  554.  
  555. self.search_entry = Gtk.SearchEntry()
  556. self.search_entry.set_placeholder_text("Search applications...")
  557. self.search_entry.connect("search-changed", self.on_search_changed)
  558. self.search_entry.set_size_request(200, 30)
  559. bottom_box.pack_start(self.search_entry, True, True, 0)
  560.  
  561. # ---- Botón de apagado ----
  562. shutdown_button = Gtk.Button()
  563. shutdown_icon_label = Gtk.Label()
  564. shutdown_icon_label.set_markup('<span font="Terminess Nerd Font Propo 16">󰤁</span>')
  565. shutdown_button.add(shutdown_icon_label)
  566. shutdown_button.set_tooltip_text("Shutdown") # Tooltip agregado
  567. shutdown_button.connect("clicked", self.on_shutdown_clicked)
  568. bottom_box.pack_end(shutdown_button, False, False, 0)
  569.  
  570. # ---- Nuevo botón de navegador ----
  571. browser_button = Gtk.Button()
  572. browser_icon_label = Gtk.Label()
  573. browser_icon_label.set_markup('<span font="Terminess Nerd Font Propo 16">󰜏</span>')
  574. browser_button.add(browser_icon_label)
  575. browser_button.set_tooltip_text("Search in the web") # Tooltip agregado
  576. browser_button.connect("clicked", self.on_browser_search_clicked)
  577. bottom_box.pack_end(browser_button, False, False, 0)
  578.  
  579. # ---- Botón de configuración ----
  580. config_button = Gtk.Button()
  581. config_icon_label = Gtk.Label()
  582. config_icon_label.set_markup('<span font="Terminess Nerd Font Propo 16"></span>') # O la polea: 
  583. config_button.add(config_icon_label)
  584. config_button.set_tooltip_text("Pymenu config") # Tooltip agregado
  585. config_button.connect("clicked", self.on_config_clicked)
  586. bottom_box.pack_end(config_button, False, False, 0)
  587.  
  588. main_box.pack_end(bottom_box, False, False, 0)
  589.  
  590. self.show_all()
  591. self.search_entry.grab_focus()
  592.  
  593.  
  594. def create_header(self):
  595. """Create the top header with profile picture, OS, kernel, and hostname"""
  596. header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=115)
  597. header_box.set_margin_top(1)
  598. header_box.set_margin_bottom(1)
  599. header_box.set_margin_start(5)
  600. header_box.set_margin_end(5)
  601.  
  602. profile_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
  603. profile_box.set_valign(Gtk.Align.CENTER)
  604.  
  605. profile_button = Gtk.Button()
  606. profile_button.set_relief(Gtk.ReliefStyle.NONE)
  607.  
  608. self.profile_image = Gtk.Image()
  609. profile_button.add(self.profile_image)
  610.  
  611. def load_profile_image():
  612. profile_pic_path = self.config['paths']['profile_pic']
  613. profile_pic_size = self.config['window'].get('profile_pic_size', 128)
  614. try:
  615. pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(profile_pic_path, profile_pic_size, profile_pic_size, True)
  616. self.profile_image.set_from_pixbuf(pixbuf)
  617. except Exception as e:
  618. print(f"Failed to load profile picture: {e}")
  619. self.profile_image.set_from_icon_name("avatar-default", Gtk.IconSize.DIALOG)
  620.  
  621. load_profile_image()
  622.  
  623. def on_profile_clicked(button):
  624. try:
  625. GLib.timeout_add(100, lambda: Gtk.main_quit())
  626. profile_manager_path = self.config['paths']['profile_manager']
  627. if os.path.exists(profile_manager_path):
  628. subprocess.Popen([profile_manager_path],
  629. stdout=subprocess.DEVNULL,
  630. stderr=subprocess.DEVNULL)
  631. else:
  632. subprocess.Popen(["python3", profile_manager_path],
  633. stdout=subprocess.DEVNULL,
  634. stderr=subprocess.DEVNULL)
  635. print(f"Launching Profile Manager: {profile_manager_path}")
  636. except Exception as e:
  637. print(f"Error opening Profile Manager: {e}")
  638.  
  639. profile_button.connect("clicked", on_profile_clicked)
  640. profile_box.pack_start(profile_button, False, False, 0)
  641. header_box.pack_start(profile_box, False, False, 0)
  642.  
  643. system_info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
  644. system_info_box.set_valign(Gtk.Align.CENTER)
  645.  
  646. os_name, kernel = self.get_os_info()
  647. hostname = self.get_hostname()
  648.  
  649. header_font_string = self.config['font']['family']
  650. header_font_description = Pango.FontDescription(header_font_string)
  651.  
  652. os_label = Gtk.Label()
  653. os_label.set_markup(f'<span color="{self.config["colors"]["text_header_os"]}"><b>{os_name}</b></span>')
  654. os_label.override_font(header_font_description)
  655. os_label.set_halign(Gtk.Align.START)
  656. os_label.set_ellipsize(3)
  657. os_label.set_max_width_chars(30)
  658. system_info_box.pack_start(os_label, False, False, 0)
  659.  
  660. kernel_label = Gtk.Label()
  661. kernel_label.set_markup(f'<span color="{self.config["colors"]["text_header_kernel"]}"> {kernel}</span>')
  662. kernel_label.override_font(header_font_description)
  663. kernel_label.set_halign(Gtk.Align.START)
  664. kernel_label.set_ellipsize(3)
  665. kernel_label.set_max_width_chars(30)
  666. system_info_box.pack_start(kernel_label, False, False, 0)
  667.  
  668. hostname_label = Gtk.Label()
  669. hostname_label.set_markup(f'<span color="{self.config["colors"]["text_header_hostname"]}"> {hostname}</span>')
  670. hostname_label.override_font(header_font_description)
  671. hostname_label.set_halign(Gtk.Align.START)
  672. hostname_label.set_ellipsize(3)
  673. hostname_label.set_max_width_chars(30)
  674. system_info_box.pack_start(hostname_label, False, False, 0)
  675.  
  676. header_box.pack_start(system_info_box, True, True, 0)
  677.  
  678. profile_file = Gio.File.new_for_path(self.config['paths']['profile_pic'])
  679. monitor = profile_file.monitor_file(Gio.FileMonitorFlags.NONE, None)
  680.  
  681. def on_file_changed(monitor, file, other_file, event_type):
  682. if event_type in (Gio.FileMonitorEvent.CHANGED, Gio.FileMonitorEvent.CREATED):
  683. GLib.idle_add(load_profile_image)
  684.  
  685. monitor.connect("changed", on_file_changed)
  686.  
  687. return header_box
  688.  
  689.  
  690. def create_categories_sidebar(self):
  691. """Create categories sidebar with improved hover functionality"""
  692. scrolled = Gtk.ScrolledWindow()
  693. scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
  694. scrolled.set_size_request(175, -1)
  695.  
  696. self.categories_listbox = Gtk.ListBox()
  697. self.categories_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
  698. self.categories_listbox.connect("row-activated", self.on_category_clicked)
  699.  
  700. category_icons = {
  701. 'Desktop': 'preferences-desktop',
  702. 'System': 'applications-system',
  703. 'Setup': 'preferences-system',
  704. 'Utility': 'applications-utilities',
  705. 'Filesystem': 'folder',
  706. 'Graphic': 'applications-graphics',
  707. 'Document': 'x-office-document',
  708. 'Business': 'x-office-spreadsheet',
  709. 'Personal': 'x-office-calendar',
  710. 'Network': 'applications-internet',
  711. 'Internet': 'applications-internet',
  712. 'Multimedia': 'applications-multimedia',
  713. 'Fun': 'applications-games',
  714. 'Help': 'help-browser',
  715. 'Leave': 'system-shutdown',
  716. }
  717.  
  718. preferred_order = ['Desktop', 'System', 'Setup', 'Utility', 'Filesystem',
  719. 'Graphic', 'Document', 'Business', 'Personal',
  720. 'Network', 'Internet', 'Multimedia', 'Fun', 'Help', 'Leave']
  721.  
  722. added_categories = set()
  723. for category in preferred_order:
  724. if category in self.applications and self.applications[category]:
  725. self.add_category_row(category, category_icons.get(category, 'applications-other'))
  726. added_categories.add(category)
  727.  
  728. for category in sorted(self.applications.keys()):
  729. if category not in added_categories and self.applications[category]:
  730. self.add_category_row(category, category_icons.get(category, 'applications-other'))
  731.  
  732. scrolled.add(self.categories_listbox)
  733.  
  734. first_row = self.categories_listbox.get_row_at_index(0)
  735. if first_row:
  736. self.categories_listbox.select_row(first_row)
  737. category = self.get_row_category(first_row)
  738. self.selected_category = category
  739. self.current_category = category
  740. first_row.get_style_context().add_class("selected-category")
  741. self.selected_category_row = first_row
  742. self.show_category_applications(category)
  743.  
  744. return scrolled
  745.  
  746. def add_category_row(self, category, icon_name):
  747. """Add a category row with hover events"""
  748. row = Gtk.ListBoxRow()
  749. event_box = Gtk.EventBox()
  750. event_box.set_above_child(True)
  751.  
  752. box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) # Reducido de 10
  753. box.set_property("margin-left", 8) # Reducido de 10
  754. box.set_property("margin-right", 8) # Reducido de 10
  755. box.set_property("margin-top", 3) # Reducido de 8
  756. box.set_property("margin-bottom", 3) # Reducido de 8
  757.  
  758. icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
  759. box.pack_start(icon, False, False, 0)
  760.  
  761. label = Gtk.Label()
  762. font_description = Pango.FontDescription(self.config['font']['family'])
  763. font_description.set_size(self.config['font']['size_categories'])
  764. label.override_font(font_description)
  765. label.set_markup(f"<span foreground='{self.config['colors']['text_normal']}'>{category}</span>")
  766. label.set_halign(Gtk.Align.START)
  767. box.pack_start(label, True, True, 0)
  768.  
  769. event_box.add(box)
  770. row.add(event_box)
  771. row.category_name = category
  772.  
  773. event_box.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK)
  774. event_box.connect("enter-notify-event", lambda w, e: self.on_category_hover_enter(row, e))
  775. event_box.connect("leave-notify-event", lambda w, e: self.on_category_hover_leave(row, e))
  776.  
  777. self.categories_listbox.add(row)
  778. row.show_all()
  779.  
  780. def get_row_category(self, row):
  781. """Get category name from row"""
  782. return getattr(row, "category_name", None)
  783.  
  784. def on_menu_enter(self, widget, event):
  785. """Handle mouse entering the whole menu"""
  786. self.mouse_in_menu = True
  787. if self.restore_timeout:
  788. GLib.source_remove(self.restore_timeout)
  789. self.restore_timeout = None
  790. return False
  791.  
  792. def on_menu_leave(self, widget, event):
  793. """Handle mouse leaving the whole menu"""
  794. self.mouse_in_menu = False
  795. if not self.restore_timeout:
  796. self.restore_timeout = GLib.timeout_add(150, self.restore_to_selected_category)
  797. return False
  798.  
  799. def on_category_hover_enter(self, row, event):
  800. """Handle mouse entering a category row"""
  801. category = self.get_row_category(row)
  802. if not category or category == self.current_category:
  803. return False
  804.  
  805. if self.hover_timeout:
  806. GLib.source_remove(self.hover_timeout)
  807. if self.restore_timeout:
  808. GLib.source_remove(self.restore_timeout)
  809. self.restore_timeout = None
  810.  
  811. self.hover_timeout = GLib.timeout_add(150, self._activate_hover_preview, category)
  812. self.hovered_category = category
  813.  
  814. return False
  815.  
  816. def on_category_hover_leave(self, row, event):
  817. """Handle mouse leaving a category row"""
  818. if self.hover_timeout:
  819. GLib.source_remove(self.hover_timeout)
  820. self.hover_timeout = None
  821.  
  822. self.hovered_category = None
  823. return False
  824.  
  825. def on_category_clicked(self, listbox, row):
  826. """Handle category selection by click or Enter key."""
  827. if not row:
  828. return
  829.  
  830. category = self.get_row_category(row)
  831. if category:
  832. if self.hover_timeout:
  833. GLib.source_remove(self.hover_timeout)
  834. self.hover_timeout = None
  835.  
  836. if self.selected_category_row:
  837. self.selected_category_row.get_style_context().remove_class("selected-category")
  838.  
  839. row.get_style_context().add_class("selected-category")
  840.  
  841. self.selected_category_row = row
  842. self.selected_category = category
  843. self.current_category = category
  844.  
  845. self.show_category_applications(category)
  846.  
  847. def restore_to_selected_category(self):
  848. """Revert to the permanently selected category"""
  849. self.restore_timeout = None
  850. if not self.mouse_in_menu:
  851. self.current_category = self.selected_category
  852. self.show_category_applications(self.selected_category)
  853. return False
  854.  
  855. def _activate_hover_preview(self, category):
  856. """Activate the category preview on hover"""
  857. self.hover_timeout = None
  858. self.current_category = category
  859. self.show_category_applications(category)
  860. return False
  861.  
  862. def _restore_selected_category(self):
  863. """Restore the selected category if no active hover"""
  864. if (not self.hover_timeout and
  865. self.selected_category and
  866. self.selected_category != self.current_category):
  867.  
  868. self.hovered_category = None
  869. self.current_category = self.selected_category
  870. self.show_category_applications(self.selected_category)
  871.  
  872. self.restore_timeout = None
  873. return False
  874.  
  875. def create_applications_area(self):
  876. """Create applications display area"""
  877. scrolled = Gtk.ScrolledWindow()
  878. scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
  879.  
  880. self.apps_flowbox = Gtk.FlowBox()
  881. self.apps_flowbox.set_valign(Gtk.Align.START)
  882. self.apps_flowbox.set_max_children_per_line(30)
  883. self.apps_flowbox.set_selection_mode(Gtk.SelectionMode.SINGLE) # Cambiado a SINGLE
  884. self.apps_flowbox.set_property("margin-left", 10)
  885. self.apps_flowbox.set_property("margin-right", 10)
  886. self.apps_flowbox.set_property("margin-top", 10)
  887. self.apps_flowbox.set_property("margin-bottom", 10)
  888.  
  889. apps_eventbox = Gtk.EventBox()
  890. apps_eventbox.add(self.apps_flowbox)
  891. apps_eventbox.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK)
  892. apps_eventbox.connect("enter-notify-event", self.on_apps_area_enter)
  893.  
  894. # Conecta el manejador de teclas a la ventana del FlowBox
  895. self.apps_flowbox.connect("key-press-event", self.on_apps_key_press)
  896.  
  897. scrolled.add(apps_eventbox)
  898.  
  899. first_category = None
  900. preferred_order = ['Desktop', 'System', 'Setup', 'Utility', 'Filesystem',
  901. 'Graphic', 'Document', 'Business', 'Personal',
  902. 'Network', 'Internet', 'Multimedia', 'Fun', 'Help', 'Leave']
  903.  
  904. for cat in preferred_order:
  905. if cat in self.applications and self.applications[cat]:
  906. first_category = cat
  907. break
  908.  
  909. if first_category:
  910. self.current_category = first_category
  911. GLib.idle_add(self.show_category_applications, first_category)
  912.  
  913. return scrolled
  914.  
  915. def on_apps_area_enter(self, widget, event):
  916. """Handle mouse entering the applications area"""
  917. print("Mouse entered applications area")
  918. return False
  919.  
  920. def create_statusbar(self):
  921. """Create status bar (kept for compatibility but not shown)"""
  922. statusbar = Gtk.Statusbar()
  923. context_id = statusbar.get_context_id("main")
  924. total_apps = sum(len(apps) for apps in self.applications.values())
  925. statusbar.push(context_id, f"Total applications: {total_apps}")
  926. return statusbar
  927.  
  928. def create_app_button(self, app_info):
  929. """Create a button for an application"""
  930. button = Gtk.Button()
  931. button.set_can_focus(True)
  932. button.set_relief(Gtk.ReliefStyle.NONE)
  933. button.connect("clicked", self.on_app_clicked, app_info)
  934.  
  935. # Contenedor vertical para ícono y nombre
  936. box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
  937. box.set_hexpand(False)
  938. box.set_property("margin-left", 5)
  939. box.set_property("margin-right", 5)
  940. box.set_property("margin-top", 5)
  941. box.set_property("margin-bottom", 5)
  942.  
  943. # Ícono de la aplicación
  944. icon = self.load_app_icon(app_info.get('Icon', 'application-x-executable'))
  945. box.pack_start(icon, False, False, 0)
  946.  
  947. # Nombre de la aplicación
  948. name_label = Gtk.Label(label=app_info['Name'])
  949.  
  950. # Estilo de fuente
  951. font_description = Pango.FontDescription(self.config['font']['family'])
  952. font_description.set_size(self.config['font']['size_names'])
  953. name_label.override_font(font_description)
  954.  
  955. # Solución definitiva para el salto de línea
  956. name_label.set_line_wrap(True)
  957. name_label.set_max_width_chars(10) # Fija el ancho máximo en caracteres
  958. name_label.set_lines(2)
  959. name_label.set_ellipsize(Pango.EllipsizeMode.END)
  960. name_label.set_justify(Gtk.Justification.CENTER)
  961. name_label.set_halign(Gtk.Align.CENTER)
  962.  
  963. box.pack_start(name_label, False, False, 0)
  964.  
  965. button.add(box)
  966. button.set_tooltip_text(app_info.get('Comment', app_info['Name']))
  967.  
  968. # Añade la información de la aplicación al botón para un acceso más fácil
  969. button.app_info = app_info
  970.  
  971. return button
  972.  
  973.  
  974. def load_app_icon(self, icon_name):
  975. """Carga el ícono de la aplicación con caché y un sistema robusto de fallbacks."""
  976. if not icon_name:
  977. icon_name = "application-x-executable"
  978.  
  979. cache_key = f"{icon_name}_{self.icon_size}"
  980. if cache_key in self.icon_cache:
  981. return self.icon_cache[cache_key]
  982.  
  983. icon_theme = Gtk.IconTheme.get_default()
  984. clean_name = os.path.splitext(os.path.basename(icon_name))[0]
  985. icon_candidates = [clean_name, clean_name.lower(), icon_name]
  986.  
  987. for candidate in icon_candidates:
  988. try:
  989. if icon_theme.has_icon(candidate):
  990. # Forzar el tamaño para que se redimensione
  991. icon_info = icon_theme.lookup_icon(candidate, self.icon_size, Gtk.IconLookupFlags.FORCE_SIZE)
  992. if icon_info:
  993. pixbuf = icon_info.load_icon()
  994. if pixbuf:
  995. image = Gtk.Image.new_from_pixbuf(pixbuf)
  996. self.icon_cache[cache_key] = image
  997. return image
  998. except Exception:
  999. continue
  1000.  
  1001. icon_path = self.find_icon_path(icon_name)
  1002. if icon_path and os.path.exists(icon_path):
  1003. try:
  1004. if self.icon_size <= 32:
  1005. pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(icon_path, self.icon_size, self.icon_size, True)
  1006. image = Gtk.Image.new_from_pixbuf(pixbuf)
  1007. self.icon_cache[cache_key] = image
  1008. return image
  1009. else:
  1010. resized_icon = self.resize_icon_with_magick(icon_path)
  1011. if resized_icon:
  1012. try:
  1013. pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(resized_icon, self.icon_size, self.icon_size, True)
  1014. image = Gtk.Image.new_from_pixbuf(pixbuf)
  1015. self.icon_cache[cache_key] = image
  1016. return image
  1017. finally:
  1018. try:
  1019. os.unlink(resized_icon)
  1020. except:
  1021. pass
  1022. except Exception as e:
  1023. pass
  1024.  
  1025. try:
  1026. image = Gtk.Image.new_from_icon_name("application-x-executable", Gtk.IconSize.BUTTON)
  1027. self.icon_cache[cache_key] = image
  1028. return image
  1029. except:
  1030. image = Gtk.Image()
  1031. self.icon_cache[cache_key] = image
  1032. return image
  1033.  
  1034. def find_icon_path(self, icon_name):
  1035. """Find icon in the defined icon paths"""
  1036. if os.path.isabs(icon_name):
  1037. return icon_name if os.path.exists(icon_name) else None
  1038.  
  1039. extensions = ['.png', '.svg', '.xpm', '.ico', '.jpg', '.jpeg', '.gif', '']
  1040.  
  1041. for path in self.parser.icon_paths:
  1042. if not os.path.exists(path):
  1043. continue
  1044.  
  1045. for ext in extensions:
  1046. full_path = os.path.join(path, icon_name + ext)
  1047. if os.path.exists(full_path) and self.is_valid_image_file(full_path):
  1048. return full_path
  1049.  
  1050. try:
  1051. for filename in os.listdir(path):
  1052. if filename.startswith(icon_name):
  1053. full_path = os.path.join(path, filename)
  1054. if os.path.isfile(full_path) and self.is_valid_image_file(full_path):
  1055. return full_path
  1056. except (OSError, PermissionError):
  1057. continue
  1058.  
  1059. return None
  1060.  
  1061. def is_valid_image_file(self, file_path):
  1062. """Check if file is a valid image that GdkPixbuf can load"""
  1063. if not os.path.isfile(file_path):
  1064. return False
  1065.  
  1066. _, ext = os.path.splitext(file_path.lower())
  1067. valid_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg', '.xpm', '.ico', '.tiff', '.tif'}
  1068.  
  1069. if ext in valid_extensions:
  1070. return True
  1071.  
  1072. try:
  1073. with open(file_path, 'rb') as f:
  1074. header = f.read(16)
  1075. if header.startswith(b'\x89PNG') or header.startswith(b'\xFF\xD8\xFF') or header.startswith(b'GIF87a') or header.startswith(b'GIF89a') or b'<svg' in header.lower() or header.startswith(b'<?xml') or b'XPM' in header or header.startswith(b'BM'):
  1076. return True
  1077. except (OSError, IOError):
  1078. pass
  1079.  
  1080. return False
  1081.  
  1082. def resize_icon_with_magick(self, icon_path):
  1083. """Resize icon using ImageMagick if available"""
  1084. try:
  1085. result = subprocess.run(['which', 'magick'], capture_output=True, timeout=5)
  1086. magick_cmd = 'magick' if result.returncode == 0 else 'convert'
  1087. if magick_cmd == 'convert':
  1088. result = subprocess.run(['which', 'convert'], capture_output=True, timeout=5)
  1089. if result.returncode != 0:
  1090. return None
  1091.  
  1092. temp_dir = '/tmp'
  1093. temp_file = os.path.join(temp_dir, f"resized_icon_{os.getpid()}_{hash(icon_path)}_{self.icon_size}.png")
  1094. if os.path.exists(temp_file):
  1095. return temp_file
  1096.  
  1097. _, ext = os.path.splitext(icon_path.lower())
  1098.  
  1099. if ext == '.svg':
  1100. cmd = [magick_cmd, '-background', 'none', icon_path, '-resize', f'{self.icon_size}x{self.icon_size}', '-flatten', temp_file]
  1101. else:
  1102. cmd = [magick_cmd, icon_path, '-resize', f'{self.icon_size}x{self.icon_size}', temp_file]
  1103.  
  1104. result = subprocess.run(cmd, capture_output=True, timeout=15, env={**os.environ, 'MAGICK_CONFIGURE_PATH': ''})
  1105.  
  1106. if os.path.exists(temp_file) and os.path.getsize(temp_file) > 0:
  1107. return temp_file
  1108. elif result.returncode != 0:
  1109. stderr = result.stderr.decode()
  1110. if not any(warn in stderr for warn in ['linearGradient', 'radialGradient', 'warning/color.c']):
  1111. print(f"ImageMagick failed for {icon_path}: {stderr}")
  1112.  
  1113. except subprocess.TimeoutExpired:
  1114. print(f"ImageMagick timeout for {icon_path}")
  1115. except Exception as e:
  1116. print(f"ImageMagick resize failed for {icon_path}: {e}")
  1117.  
  1118. return None
  1119.  
  1120. def show_all_applications(self):
  1121. """Show all applications with lazy loading"""
  1122. if not self.apps_flowbox:
  1123. return
  1124.  
  1125. self.current_category = "All"
  1126.  
  1127. for child in self.apps_flowbox.get_children():
  1128. child.destroy()
  1129.  
  1130. GLib.idle_add(self.load_applications_batch, list(self.applications.items()), 0)
  1131.  
  1132. def show_category_applications(self, category):
  1133. """Show applications from specific category with lazy loading"""
  1134. if not self.apps_flowbox:
  1135. return
  1136.  
  1137. self.current_category = category
  1138.  
  1139. for child in self.apps_flowbox.get_children():
  1140. child.destroy()
  1141.  
  1142. if category in self.applications:
  1143. apps_data = [(category, self.applications[category])]
  1144. GLib.idle_add(self.load_applications_batch, apps_data, 0)
  1145.  
  1146. self.apps_flowbox.show_all()
  1147.  
  1148. def load_applications_batch(self, apps_data, start_index, batch_size=10):
  1149. """Load applications in batches to avoid UI freezing"""
  1150. count = 0
  1151.  
  1152. for category, apps in apps_data:
  1153. for i, app in enumerate(apps[start_index:], start_index):
  1154. if count >= batch_size:
  1155. GLib.idle_add(self.load_applications_batch, [(category, apps)], i)
  1156. return False
  1157.  
  1158. button = self.create_app_button(app)
  1159. self.apps_flowbox.add(button)
  1160. count += 1
  1161.  
  1162. self.apps_flowbox.show_all()
  1163. return False
  1164.  
  1165. def on_search_changed(self, search_entry):
  1166. """Handle search text change"""
  1167. if not self.apps_flowbox:
  1168. return
  1169.  
  1170. search_text = search_entry.get_text().lower()
  1171.  
  1172. for child in self.apps_flowbox.get_children():
  1173. child.destroy()
  1174.  
  1175. if not search_text:
  1176. if hasattr(self, 'current_category') and self.current_category:
  1177. self.show_category_applications(self.current_category)
  1178. else:
  1179. preferred_order = ['Desktop', 'System', 'Setup', 'Utility', 'Filesystem',
  1180. 'Graphic', 'Document', 'Business', 'Personal',
  1181. 'Network', 'Internet', 'Multimedia', 'Fun', 'Help', 'Leave']
  1182. for cat in preferred_order:
  1183. if cat in self.applications and self.applications[cat]:
  1184. self.show_category_applications(cat)
  1185. break
  1186. return
  1187.  
  1188. for category, apps in self.applications.items():
  1189. for app in apps:
  1190. if (search_text in app['Name'].lower() or
  1191. search_text in app.get('Comment', '').lower()):
  1192. button = self.create_app_button(app)
  1193. self.apps_flowbox.add(button)
  1194.  
  1195. self.apps_flowbox.show_all()
  1196.  
  1197. def on_apps_key_press(self, widget, event):
  1198. """Handles key presses (arrows, Enter) on the apps flowbox."""
  1199. keyval = event.keyval
  1200.  
  1201. if keyval in [Gdk.KEY_Down, Gdk.KEY_Up, Gdk.KEY_Right, Gdk.KEY_Left]:
  1202. self.navigate_apps(keyval)
  1203. return True
  1204. elif keyval == Gdk.KEY_Return:
  1205. self.launch_selected_app()
  1206. return True
  1207. return False
  1208.  
  1209. def navigate_apps(self, keyval):
  1210. """Navigate through applications with arrow keys."""
  1211. children = self.apps_flowbox.get_children()
  1212. if not children:
  1213. return
  1214.  
  1215. selected_children = self.apps_flowbox.get_selected_children()
  1216. if not selected_children:
  1217. current_index = -1
  1218. else:
  1219. current_index = children.index(selected_children[0])
  1220.  
  1221. new_index = -1
  1222.  
  1223. # Determine the number of columns on the first row.
  1224. # This is a more robust way to get the column count.
  1225. cols = 1
  1226. if len(children) > 1:
  1227. y_pos_first_child = children[0].get_allocation().y
  1228. for i in range(1, len(children)):
  1229. if children[i].get_allocation().y == y_pos_first_child:
  1230. cols += 1
  1231. else:
  1232. break
  1233.  
  1234. if keyval == Gdk.KEY_Down:
  1235. if current_index >= 0:
  1236. new_index = current_index + cols
  1237. elif keyval == Gdk.KEY_Up:
  1238. if current_index >= 0:
  1239. new_index = current_index - cols
  1240. elif keyval == Gdk.KEY_Right:
  1241. if current_index >= 0:
  1242. new_index = current_index + 1
  1243. else:
  1244. new_index = 0
  1245. elif keyval == Gdk.KEY_Left:
  1246. if current_index > 0:
  1247. new_index = current_index - 1
  1248. else: # Go to the end if at the start
  1249. new_index = len(children) - 1
  1250.  
  1251. if 0 <= new_index < len(children):
  1252. self.apps_flowbox.unselect_all()
  1253. self.apps_flowbox.select_child(children[new_index])
  1254. children[new_index].grab_focus()
  1255.  
  1256. def launch_selected_app(self):
  1257. """Lanza la aplicación seleccionada con el teclado."""
  1258. selected = self.apps_flowbox.get_selected_children()
  1259. if selected:
  1260. child = selected[0]
  1261. button = child.get_child() # el Gtk.Button
  1262. if button and hasattr(button, 'app_info'):
  1263. self.on_app_clicked(button, button.app_info)
  1264.  
  1265.  
  1266. def on_browser_search_clicked(self, button):
  1267. """Launches a browser search with the text from the search box"""
  1268. search_query = self.search_entry.get_text().strip()
  1269.  
  1270. if not search_query:
  1271. print("Search box is empty. Doing nothing.")
  1272. return
  1273.  
  1274. # Encode the search query to be URL-safe
  1275. encoded_query = urllib.parse.quote_plus(search_query)
  1276. search_url = f"https://www.google.com/search?q={encoded_query}"
  1277.  
  1278. try:
  1279. print(f"Launching browser search for: '{search_query}'")
  1280. # Use xdg-open to launch the default browser
  1281. subprocess.Popen(["xdg-open", search_url],
  1282. stdout=subprocess.DEVNULL,
  1283. stderr=subprocess.DEVNULL)
  1284. # Close the menu after launching the browser
  1285. Gtk.main_quit()
  1286. except FileNotFoundError:
  1287. print("Error: 'xdg-open' not found. Please make sure you have a default browser configured.")
  1288. except Exception as e:
  1289. print(f"Failed to launch browser: {e}")
  1290.  
  1291. def on_app_clicked(self, button, app_info):
  1292. """Handle application launch"""
  1293. try:
  1294. GLib.timeout_add(50, lambda: Gtk.main_quit())
  1295.  
  1296. command = app_info['Exec']
  1297. try:
  1298. cmd_parts = shlex.split(command)
  1299. except ValueError:
  1300. cmd_parts = command.split()
  1301.  
  1302. cleaned_parts = [part for part in cmd_parts if not any(part.startswith(code) for code in ['%f', '%F', '%u', '%U', '%i', '%c'])]
  1303.  
  1304. if not cleaned_parts:
  1305. print(f"No executable command found for {app_info['Name']}")
  1306. return
  1307.  
  1308. if app_info.get('Terminal', False):
  1309. subprocess.Popen(['lxterminal', '-e'] + cleaned_parts,
  1310. stdout=subprocess.DEVNULL,
  1311. stderr=subprocess.DEVNULL)
  1312. else:
  1313. subprocess.Popen(cleaned_parts,
  1314. stdout=subprocess.DEVNULL,
  1315. stderr=subprocess.DEVNULL)
  1316.  
  1317. print(f"Launching: {app_info['Name']} ({' '.join(cleaned_parts)})")
  1318.  
  1319. except Exception as e:
  1320. print(f"Error launching {app_info.get('Name', 'Unknown')}: {e}")
  1321.  
  1322. def on_profile_clicked(self, button):
  1323. """Open ProfileManager when profile thumbnail is clicked"""
  1324. try:
  1325. GLib.timeout_add(100, lambda: Gtk.main_quit())
  1326. profile_manager_path = self.config['paths']['profile_manager']
  1327. if os.path.exists(profile_manager_path):
  1328. subprocess.Popen([profile_manager_path],
  1329. stdout=subprocess.DEVNULL,
  1330. stderr=subprocess.DEVNULL)
  1331. else:
  1332. subprocess.Popen(["python3", profile_manager_path],
  1333. stdout=subprocess.DEVNULL,
  1334. stderr=subprocess.DEVNULL)
  1335. print(f"Launching Profile Manager: {profile_manager_path}")
  1336. except Exception as e:
  1337. print(f"Error opening Profile Manager: {e}")
  1338.  
  1339. # Función que faltaba
  1340. def on_config_clicked(self, button):
  1341. """Lanza el script de configuración."""
  1342. try:
  1343. # Cerrar la ventana del menú inmediatamente
  1344. GLib.timeout_add(100, lambda: Gtk.main_quit())
  1345.  
  1346. # Lanzar el script de configuración
  1347. config_script = "/usr/local/bin/pymenu-config.py"
  1348. subprocess.Popen(["python3", config_script],
  1349. stdout=subprocess.DEVNULL,
  1350. stderr=subprocess.DEVNULL)
  1351. print(f"Lanzando el configurador: {config_script}")
  1352. except Exception as e:
  1353. print(f"Error al lanzar el configurador: {e}")
  1354.  
  1355. def on_shutdown_clicked(self, button):
  1356. """Run shutdown command"""
  1357. try:
  1358. GLib.timeout_add(100, lambda: Gtk.main_quit())
  1359. shutdown_cmd_path = self.config['paths']['shutdown_cmd']
  1360. if os.path.exists(shutdown_cmd_path):
  1361. subprocess.Popen([shutdown_cmd_path],
  1362. stdout=subprocess.DEVNULL,
  1363. stderr=subprocess.DEVNULL)
  1364. else:
  1365. subprocess.Popen(["python3", shutdown_cmd_path],
  1366. stdout=subprocess.DEVNULL,
  1367. stderr=subprocess.DEVNULL)
  1368. print(f"Launching shutdown command: {shutdown_cmd_path}")
  1369. except Exception as e:
  1370. print(f"Failed to run shutdown command: {e}")
  1371.  
  1372. def main():
  1373. icon_size = None
  1374. jwm_file = None
  1375. x = None
  1376. y = None
  1377.  
  1378. if len(sys.argv) >= 3:
  1379. try:
  1380. x = int(sys.argv[1])
  1381. y = int(sys.argv[2])
  1382. except ValueError:
  1383. jwm_file = sys.argv[1]
  1384. try:
  1385. icon_size = int(sys.argv[2])
  1386. if icon_size not in [16, 24, 32, 40, 48]:
  1387. icon_size = None
  1388. except ValueError:
  1389. icon_size = None
  1390. elif len(sys.argv) == 2:
  1391. try:
  1392. x = int(sys.argv[1])
  1393. x = None
  1394. except ValueError:
  1395. jwm_file = sys.argv[1]
  1396.  
  1397. app = ArcMenuLauncher(icon_size, jwm_file, x, y)
  1398. app.connect("destroy", Gtk.main_quit)
  1399. app.show_all()
  1400.  
  1401. Gtk.main()
  1402.  
  1403. if __name__ == "__main__":
  1404. main()
  1405.  
Advertisement
Add Comment
Please, Sign In to add comment