From 9eb93a60333c990ccd5a7dc816314b2049da51a3 Mon Sep 17 00:00:00 2001 From: KapaDev Date: Sun, 27 Jul 2025 14:33:55 +0300 Subject: [PATCH 1/5] ai --- config/data.py | 42 +++ config/settings_constants.py | 2 + config/settings_gui.py | 136 ++++++++ config/settings_utils.py | 1 + main.css | 4 +- modules/ai.py | 621 +++++++++++++++++++++++++++++++++++ modules/ai_services.py | 242 ++++++++++++++ modules/notch.py | 371 ++++++++++++--------- styles/ai.css | 221 +++++++++++++ 9 files changed, 1477 insertions(+), 163 deletions(-) create mode 100644 modules/ai.py create mode 100644 modules/ai_services.py create mode 100644 styles/ai.css diff --git a/config/data.py b/config/data.py index ffc5545b..cda0390e 100644 --- a/config/data.py +++ b/config/data.py @@ -16,6 +16,20 @@ NOTIF_POS_KEY = "notif_pos" NOTIF_POS_DEFAULT = "Top" +# AI API Keys +AI_OPENAI_KEY = "ai_openai_key" +AI_GEMINI_KEY = "ai_gemini_key" +AI_CLAUDE_KEY = "ai_claude_key" +AI_GROK_KEY = "ai_grok_key" +AI_DEEPSEEK_KEY = "ai_deepseek_key" + +# AI Model Selections +AI_OPENAI_MODEL = "ai_openai_model" +AI_GEMINI_MODEL = "ai_gemini_model" +AI_CLAUDE_MODEL = "ai_claude_model" +AI_GROK_MODEL = "ai_grok_model" +AI_DEEPSEEK_MODEL = "ai_deepseek_model" + CACHE_DIR = str(GLib.get_user_cache_dir()) + f"/{APP_NAME}" USERNAME = os.getlogin() @@ -95,6 +109,20 @@ def load_config(): BAR_METRICS_DISKS = config.get('bar_metrics_disks', ["/"]) METRICS_VISIBLE = config.get('metrics_visible', {'cpu': True, 'ram': True, 'disk': True, 'gpu': True}) METRICS_SMALL_VISIBLE = config.get('metrics_small_visible', {'cpu': True, 'ram': True, 'disk': True, 'gpu': True}) + + # AI API Keys + AI_OPENAI_API_KEY = config.get(AI_OPENAI_KEY, "") + AI_GEMINI_API_KEY = config.get(AI_GEMINI_KEY, "") + AI_CLAUDE_API_KEY = config.get(AI_CLAUDE_KEY, "") + AI_GROK_API_KEY = config.get(AI_GROK_KEY, "") + AI_DEEPSEEK_API_KEY = config.get(AI_DEEPSEEK_KEY, "") + + # AI Model Selections (with backward compatibility for old format) + AI_OPENAI_MODEL = config.get(AI_OPENAI_MODEL, config.get("gpt-3.5-turbo", "gpt-3.5-turbo")) + AI_GEMINI_MODEL = config.get(AI_GEMINI_MODEL, config.get("gemini-1.5-pro", "gemini-1.5-pro")) + AI_CLAUDE_MODEL = config.get(AI_CLAUDE_MODEL, config.get("claude-3-sonnet-20240229", "claude-3-sonnet-20240229")) + AI_GROK_MODEL = config.get(AI_GROK_MODEL, config.get("grok-beta", "grok-beta")) + AI_DEEPSEEK_MODEL = config.get(AI_DEEPSEEK_MODEL, config.get("deepseek-chat", "deepseek-chat")) else: WALLPAPERS_DIR = WALLPAPERS_DIR_DEFAULT BAR_POSITION = "Top" @@ -112,6 +140,20 @@ def load_config(): PANEL_POSITION = PANEL_POSITION_DEFAULT NOTIF_POS = NOTIF_POS_DEFAULT + + # AI API Keys (empty by default) + AI_OPENAI_API_KEY = "" + AI_GEMINI_API_KEY = "" + AI_CLAUDE_API_KEY = "" + AI_GROK_API_KEY = "" + AI_DEEPSEEK_API_KEY = "" + + # AI Model Selections (default models) + AI_OPENAI_MODEL = "gpt-3.5-turbo" + AI_GEMINI_MODEL = "gemini-1.5-pro" + AI_CLAUDE_MODEL = "claude-3-sonnet-20240229" + AI_GROK_MODEL = "grok-beta" + AI_DEEPSEEK_MODEL = "deepseek-chat" BAR_COMPONENTS_VISIBILITY = { diff --git a/config/settings_constants.py b/config/settings_constants.py index ed301e22..d22213d7 100644 --- a/config/settings_constants.py +++ b/config/settings_constants.py @@ -88,4 +88,6 @@ 'disk': True, 'gpu': True, }, + 'prefix_ai': "SUPER SHIFT", + 'suffix_ai': "A", } diff --git a/config/settings_gui.py b/config/settings_gui.py index 0dd1fb35..fd64d855 100644 --- a/config/settings_gui.py +++ b/config/settings_gui.py @@ -56,11 +56,13 @@ def __init__(self, show_lock_checkbox: bool, show_idle_checkbox: bool, **kwargs) self.key_bindings_tab_content = self.create_key_bindings_tab() self.appearance_tab_content = self.create_appearance_tab() self.system_tab_content = self.create_system_tab() + self.ai_tab_content = self.create_ai_tab() self.about_tab_content = self.create_about_tab() self.tab_stack.add_titled(self.key_bindings_tab_content, "key_bindings", "Key Bindings") self.tab_stack.add_titled(self.appearance_tab_content, "appearance", "Appearance") self.tab_stack.add_titled(self.system_tab_content, "system", "System") + self.tab_stack.add_titled(self.ai_tab_content, "ai", "AI") self.tab_stack.add_titled(self.about_tab_content, "about", "About") tab_switcher = Gtk.StackSwitcher() @@ -129,6 +131,7 @@ def create_key_bindings_tab(self): ("Toggle Caffeine", 'prefix_caffeine', 'suffix_caffeine'), ("Reload CSS", 'prefix_css', 'suffix_css'), ("Restart with inspector", 'prefix_restart_inspector', 'suffix_restart_inspector'), + ("AI", 'prefix_ai', 'suffix_ai'), ] for i, (label_text, prefix_key, suffix_key) in enumerate(bindings): @@ -568,6 +571,131 @@ def _add_disk_entry_widget(self, path): self.disk_entries.show_all() + def create_ai_tab(self): + scrolled_window = ScrolledWindow( + h_scrollbar_policy="never", + v_scrollbar_policy="automatic", + h_expand=True, + v_expand=True, + propagate_width=False, + propagate_height=False + ) + + main_vbox = Box(orientation="v", spacing=15, style="margin: 20px;") + scrolled_window.add(main_vbox) + + # Title + title_label = Label(markup="AI API Keys", h_align="start", style="font-size: 1.2em; margin-bottom: 10px;") + main_vbox.add(title_label) + + # Description + desc_label = Label( + label="Enter your API keys for the AI models you want to use. Leave empty if you don't have a key for a specific model.", + h_align="start", + style="margin-bottom: 20px; color: #888;" + ) + main_vbox.add(desc_label) + + # API Keys Grid + api_grid = Gtk.Grid() + api_grid.set_column_spacing(15) + api_grid.set_row_spacing(10) + api_grid.set_margin_start(5) + api_grid.set_margin_end(5) + + # Headers + model_label = Label(markup="AI Service", h_align="start") + key_label = Label(markup="API Key", h_align="start") + model_select_label = Label(markup="Model", h_align="start") + api_grid.attach(model_label, 0, 0, 1, 1) + api_grid.attach(key_label, 1, 0, 1, 1) + api_grid.attach(model_select_label, 2, 0, 1, 1) + + # Import data module to get current API keys and models + from .data import ( + AI_OPENAI_API_KEY, AI_GEMINI_API_KEY, AI_CLAUDE_API_KEY, + AI_GROK_API_KEY, AI_DEEPSEEK_API_KEY, + AI_OPENAI_KEY, AI_GEMINI_KEY, AI_CLAUDE_KEY, + AI_GROK_KEY, AI_DEEPSEEK_KEY, + AI_OPENAI_MODEL, AI_GEMINI_MODEL, AI_CLAUDE_MODEL, + AI_GROK_MODEL, AI_DEEPSEEK_MODEL + ) + + # AI Models and their API key entries + self.ai_entries = [] + self.ai_model_combos = {} + + # Get the actual model values from the imported constants + # These are the actual model names like "gpt-3.5-turbo", "gemini-1.5-pro", etc. + from .data import ( + AI_OPENAI_MODEL as OPENAI_MODEL_VALUE, + AI_GEMINI_MODEL as GEMINI_MODEL_VALUE, + AI_CLAUDE_MODEL as CLAUDE_MODEL_VALUE, + AI_GROK_MODEL as GROK_MODEL_VALUE, + AI_DEEPSEEK_MODEL as DEEPSEEK_MODEL_VALUE + ) + + # Create a mapping of model keys to their current values + model_values = { + AI_OPENAI_MODEL: OPENAI_MODEL_VALUE, + AI_GEMINI_MODEL: GEMINI_MODEL_VALUE, + AI_CLAUDE_MODEL: CLAUDE_MODEL_VALUE, + AI_GROK_MODEL: GROK_MODEL_VALUE, + AI_DEEPSEEK_MODEL: DEEPSEEK_MODEL_VALUE, + } + + # Define available models for each service + ai_services = [ + ("Chat GPT (OpenAI)", AI_OPENAI_KEY, AI_OPENAI_API_KEY, AI_OPENAI_MODEL, [ + "gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-4", "gpt-4-turbo", "gpt-4o", "gpt-4o-mini" + ]), + ("Gemini (Google)", AI_GEMINI_KEY, AI_GEMINI_API_KEY, AI_GEMINI_MODEL, [ + "gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-2.0-flash-exp", + "gemini-1.5-pro", "gemini-1.5-flash", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest", + "gemini-1.0-pro", "gemini-1.0-pro-vision" + ]), + ("Claude (Anthropic)", AI_CLAUDE_KEY, AI_CLAUDE_API_KEY, AI_CLAUDE_MODEL, [ + "claude-3-sonnet-20240229", "claude-3-opus-20240229", "claude-3-haiku-20240307", + "claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022" + ]), + ("Grok (xAI)", AI_GROK_KEY, AI_GROK_API_KEY, AI_GROK_MODEL, [ + "grok-beta", "grok-2" + ]), + ("Deepseek", AI_DEEPSEEK_KEY, AI_DEEPSEEK_API_KEY, AI_DEEPSEEK_MODEL, [ + "deepseek-chat", "deepseek-coder", "deepseek-llm-7b-chat" + ]), + ] + + for i, (service_name, key_name, current_key, model_key, available_models) in enumerate(ai_services): + row = i + 1 + model_label = Label(label=service_name, h_align="start") + key_entry = Entry(text=current_key, tooltip_text=f"Enter your {service_name} API key") + key_entry.set_visibility(False) # Hide the API key for security + + # Create model selection combo box + model_combo = Gtk.ComboBoxText() + for model in available_models: + model_combo.append_text(model) + + # Set current model + current_model = model_values[model_key] + model_combo.set_active(available_models.index(current_model) if current_model in available_models else 0) + + api_grid.attach(model_label, 0, row, 1, 1) + api_grid.attach(key_entry, 1, row, 1, 1) + api_grid.attach(model_combo, 2, row, 1, 1) + + self.ai_entries.append((key_name, key_entry)) + self.ai_model_combos[model_key] = model_combo + + main_vbox.add(api_grid) + + # Add some spacing + main_vbox.add(Box(v_expand=True)) + + return scrolled_window + + def create_about_tab(self): vbox = Box(orientation="v", spacing=18, style="margin: 30px;") vbox.add(Label(markup=f"{APP_NAME_CAP}", h_align="start", style="font-size: 1.5em; margin-bottom: 8px;")) @@ -660,6 +788,14 @@ def on_accept(self, widget): child.get_children()[0].get_text() for child in self.disk_entries.get_children() if isinstance(child, Gtk.Box) and child.get_children() and isinstance(child.get_children()[0], Entry) ] + # Save AI API keys and model selections + for key_name, key_entry in self.ai_entries: + current_bind_vars_snapshot[key_name] = key_entry.get_text() + + # Save AI model selections + for model_key, model_combo in self.ai_model_combos.items(): + selected_model = model_combo.get_active_text() + current_bind_vars_snapshot[model_key] = selected_model selected_icon_path = self.selected_face_icon replace_lock = self.lock_switch and self.lock_switch.get_active() diff --git a/config/settings_utils.py b/config/settings_utils.py index 1727e97b..4a125a91 100644 --- a/config/settings_utils.py +++ b/config/settings_utils.py @@ -233,6 +233,7 @@ def generate_hyprconf() -> str: bind = {bind_vars.get('prefix_caffeine', 'SUPER SHIFT')}, {bind_vars.get('suffix_caffeine', 'M')}, exec, $fabricSend 'notch.dashboard.widgets.buttons.caffeine_button.toggle_inhibit(external=True)' # Toggle Caffeine bind = {bind_vars.get('prefix_css', 'SUPER SHIFT')}, {bind_vars.get('suffix_css', 'B')}, exec, $fabricSend 'app.set_css()' # Reload CSS bind = {bind_vars.get('prefix_restart_inspector', 'SUPER CTRL ALT')}, {bind_vars.get('suffix_restart_inspector', 'B')}, exec, killall {APP_NAME}; uwsm-app $(GTK_DEBUG=interactive python {home}/.config/{APP_NAME_CAP}/main.py) # Restart with inspector +bind = {bind_vars.get('prefix_ai', 'SUPER SHIFT')}, {bind_vars.get('suffix_ai', 'A')}, exec, $fabricSend 'notch.open_notch("ai")' # AI Panel # Wallpapers directory: {bind_vars.get('wallpapers_dir', '~/.config/Ax-Shell/assets/wallpapers_example')} diff --git a/main.css b/main.css index e95aecba..287dbdba 100644 --- a/main.css +++ b/main.css @@ -21,6 +21,7 @@ @import url("./styles/tools.css"); @import url("./styles/wallpapers.css"); @import url("./styles/workspaces.css"); +@import url("./styles/ai.css"); * { all: unset; @@ -113,5 +114,6 @@ } #later-button:active { - background-color: var(--surface-bright); + background-color: var(--primary); } + diff --git a/modules/ai.py b/modules/ai.py new file mode 100644 index 00000000..e8359663 --- /dev/null +++ b/modules/ai.py @@ -0,0 +1,621 @@ +from fabric.widgets.box import Box +from fabric.widgets.label import Label +from fabric.widgets.button import Button +from fabric.widgets.scrolledwindow import ScrolledWindow +from fabric.widgets.revealer import Revealer +from fabric.widgets.entry import Entry +from widgets.wayland import WaylandWindow as Window +import gi +import asyncio + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk, GLib +from fabric.widgets.stack import Stack + +# Import AI services +from .ai_services import ai_manager + +class AI(Window): + def __init__(self, **kwargs): + super().__init__( + name="ai-window", + title="AI Panel", + size=(400, 600), + layer="top", + anchor="top left bottom", + margin="0px 0px 0px 0px", + keyboard_mode="exclusive", + exclusivity="normal", + visible=False, + all_visible=False, + **kwargs, + ) + self.set_size_request(400, 500) + + # Create revealer for slide animation (recommended for WaylandWindow) + self.revealer = Revealer( + name="ai-revealer", + transition_type="slide_right", + transition_duration=250, + ) + + # Main container + self.main_box = Box( + orientation="v", + spacing=16, + style="border: 4px solid #000; border-radius: 16px; margin: 0px 16px 16px 0px; padding: 24px; min-width: 320px; min-height: 480px; background: #000000;", + ) + + # Title label (handwritten style, large) + self.title_label = Label( + label="panel", + h_align="start", + style="font-family: 'Comic Sans MS', 'Comic Sans', cursive; font-size: 2em; font-weight: bold; margin-bottom: 12px;" + ) + self.main_box.add(self.title_label) + + # # Divider (horizontal line) + # self.divider = Box( + # style="min-height: 2px; background-color: #fff; margin: 16px 0 24px 0;", + # h_expand=True + # ) + # self.main_box.add(self.divider) + + # Chat area (scrollable) + self.chat_scroll = ScrolledWindow( + name="ai-chat-scroll", + vexpand=True, + hexpand=True, + min_content_height=200, + ) + + # Chat container for messages + self.chat_container = Box( + name="ai-chat-container", + orientation="v", + spacing=8, + margin_start=8, + margin_end=8, + margin_top=8, + margin_bottom=8, + ) + + self.chat_scroll.add(self.chat_container) + self.main_box.add(self.chat_scroll) + + # Spacer to push dropdown to bottom + self.spacer = Box(v_expand=True) + self.main_box.add(self.spacer) + + # Text field and gear button container (horizontal) + self.input_container = Box( + orientation="h", + spacing=8, + h_align="fill", + h_expand=True, + v_align="fill", + style="margin: 8px 0 8px -18px;" # Reduced left margin from 8px to 4px + ) + + # AI Model selection (gear button only) - now to the left of text field + self.model_button = Button( + name="ai-model-button", + child=Label(name="ai-model-icon", markup="⚙"), # Gear icon + tooltip_text="AI Model Settings", + halign="start" + ) + self.model_button.connect("clicked", self._on_model_button_clicked) + + # Create a popover for the model options + self.model_popover = Gtk.Popover() + self.model_popover.set_relative_to(self.model_button) + self.model_popover.set_position(Gtk.PositionType.BOTTOM) + + # Create a vertical box for model options + self.model_options_box = Box( + orientation="v", + spacing=4, + margin_start=8, + margin_end=8, + margin_top=8, + margin_bottom=8, + name="ai-model-options-box" + ) + + # Create buttons for each model + ai_models = ["Chat GPT", "Gemini", "Claude", "Grok", "Deepseek"] + self.model_buttons = {} # Store references to buttons + + for model in ai_models: + # Default styling for unselected models + model_button = Button( + label=model, + halign="fill", + name="ai-model-option-button" + ) + model_button.get_style_context().add_class("ai-model-button-unselected") + model_button.connect("clicked", self._on_model_option_clicked, model) + self.model_options_box.add(model_button) + self.model_buttons[model] = model_button + + # Set initial selected model styling + self.selected_model = "Chat GPT" + self._update_model_button_styles() + + self.model_popover.add(self.model_options_box) + # self.model_button.set_popover(self.model_popover) # Not available in GTK3 + + self.input_container.add(self.model_button) + + # Text field (input area) - now multi-line with wrapping + self.text_view = Gtk.TextView() + self.text_view.set_name("ai-text-view") + self.text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + self.text_view.set_hexpand(True) + self.text_view.set_halign(Gtk.Align.FILL) + self.text_view.set_vexpand(False) + self.text_view.set_margin_start(8) + self.text_view.set_margin_end(8) + self.text_view.set_margin_top(8) + self.text_view.set_margin_bottom(8) + + # Enable text wrapping to go down a line + self.text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + + # Set GTK properties for proper expansion + self.text_view.set_hexpand(True) + self.text_view.set_halign(Gtk.Align.FILL) + + # Enable keyboard shortcuts + self.text_view.set_accepts_tab(False) # Disable tab to prevent focus issues + + # Connect key press event to handle Enter key + self.text_view.connect("key-press-event", self._on_text_key_press) + # Also connect to the buffer to catch all text changes + self.text_view.get_buffer().connect("insert-text", self._on_text_insert) + + # Create a scrolled window for the text view with max height + self.text_scroll = Gtk.ScrolledWindow() + self.text_scroll.set_name("ai-text-scroll") + self.text_scroll.set_hexpand(True) + self.text_scroll.set_halign(Gtk.Align.FILL) + self.text_scroll.set_vexpand(False) + self.text_scroll.set_size_request(-1, 120) # Max height of 120px (about 6-8 lines) + self.text_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.text_scroll.add(self.text_view) + + # Style the text entry container to match app launcher search field + self.entry_container = Box( + h_align="fill", + h_expand=True, + v_align="fill", + name="ai-entry-container" + ) + self.entry_container.add(self.text_scroll) + self.input_container.add(self.entry_container) + + self.main_box.add(self.input_container) + + # Add the main box to the revealer, and revealer to the window + self.revealer.add(self.main_box) + self.add(self.revealer) + + def show_at_position(self, x, y): + self.move(x, y) + self.set_visible(True) + self.show_all() + + # Reveal the content with smooth slide animation + self.revealer.set_reveal_child(True) + self.grab_focus() + + # Ensure key events are captured + self.add_events(Gdk.EventMask.KEY_PRESS_MASK) + + def hide_ai_panel(self): + print("hide_ai_panel() called") + # Hide the content with smooth slide animation + self.revealer.set_reveal_child(False) + + # Wait for animation to complete before hiding the window + GLib.timeout_add(250, self._hide_after_animation) # 250ms to match animation duration + + def _hide_after_animation(self): + """Hide the window after the revealer animation completes""" + self.set_visible(False) + self.hide() + return False + + + + # Initialize variable to store the text + self.current_message = "" + self.selected_model = "Chat GPT" # Default model + + # Close window on Escape key press + self.add_events(Gdk.EventMask.KEY_PRESS_MASK) + self.connect("key-press-event", self._on_key_press) + + # Also connect to the main window for key events + self.connect("key-press-event", self._on_key_press) + + def _on_model_button_clicked(self, button): + """Handle gear button click - manually show popover""" + self.model_popover.show_all() + self.model_popover.popup() + + def _on_model_option_clicked(self, button, model_name): + """Handle model selection from popover""" + self.selected_model = model_name + print(f"Selected AI model: {model_name}") + self._update_model_button_styles() # Update button styles after selection + # Close the popover after selection + self.model_popover.hide() + + def on_model_changed(self, combo): + """Handle model selection change""" + active_iter = combo.get_active_iter() + if active_iter is not None: + selected_model = self.model_store.get_value(active_iter, 0) + self.selected_model = selected_model + print(f"Selected AI model: {selected_model}") + # Hide the dropdown after selection + self.model_dropdown.set_visible(False) + + def show_ai_panel(self): + """Show the AI panel with revealer animation""" + self.show_at_position(0, 0) + + def _on_text_key_press(self, widget, event): + """Handle key press events in the text entry""" + # Check for Shift+Enter to insert new line + if event.keyval == Gdk.KEY_Return and event.state & Gdk.ModifierType.SHIFT_MASK: + # Set flag to allow newline + self._shift_pressed = True + # Insert a newline at cursor position + buffer = self.text_view.get_buffer() + buffer.insert_at_cursor("\n") + # Clear the flag after a short delay + GLib.timeout_add(100, self._clear_shift_flag) + return True + + # Regular Enter to send message + elif event.keyval == Gdk.KEY_Return: + # Prevent default behavior and send message + self._send_current_message() + return True + + # Handle other keyboard shortcuts + elif event.state & Gdk.ModifierType.CONTROL_MASK: + if event.keyval == Gdk.KEY_a: # Ctrl+A - Select All + self.text_view.emit("select-all") + return True + elif event.keyval == Gdk.KEY_c: # Ctrl+C - Copy + self.text_view.emit("copy-clipboard") + return True + elif event.keyval == Gdk.KEY_v: # Ctrl+V - Paste + self.text_view.emit("paste-clipboard") + return True + elif event.keyval == Gdk.KEY_x: # Ctrl+X - Cut + self.text_view.emit("cut-clipboard") + return True + elif event.keyval == Gdk.KEY_z: # Ctrl+Z - Undo + self.text_view.emit("undo") + return True + elif event.keyval == Gdk.KEY_y: # Ctrl+Y - Redo + self.text_view.emit("redo") + return True + + return False + + def _clear_shift_flag(self): + """Clear the shift pressed flag""" + if hasattr(self, '_shift_pressed'): + delattr(self, '_shift_pressed') + return False + + def _on_text_insert(self, buffer, location, text, length): + """Handle text insertion to prevent unwanted newlines""" + # If this is a newline and we're not in Shift+Enter mode, prevent it + if text == '\n' and not hasattr(self, '_shift_pressed'): + # Remove the newline + buffer.delete(location, buffer.get_iter_at_offset(location.get_offset() + 1)) + # Trigger message sending + self._send_current_message() + return True + return False + + def _send_current_message(self): + """Send the current message to the AI""" + # Prevent multiple sends + if hasattr(self, '_sending_message') and self._sending_message: + return + + self._sending_message = True + + # Save the text to variable + self.current_message = self.text_view.get_buffer().get_text( + self.text_view.get_buffer().get_start_iter(), + self.text_view.get_buffer().get_end_iter(), + include_hidden_chars=False + ) + print(f"Message saved: {self.current_message}") + + # Add message to chat only if there's content + if self.current_message.strip(): + self.add_user_message(self.current_message) + + # Clear the text field + self.text_view.get_buffer().set_text("") + + # Here you can add logic to send the message to the selected AI model + print(f"Sending message to {self.selected_model}: {self.current_message}") + + # Reset sending flag after a short delay + GLib.timeout_add(100, self._reset_sending_flag) + + def _reset_sending_flag(self): + """Reset the sending message flag""" + self._sending_message = False + return False + + def add_user_message(self, message): + """Add a user message to the chat area (right side)""" + # Create message container + message_container = Gtk.Box() + message_container.set_orientation(Gtk.Orientation.HORIZONTAL) + message_container.set_halign(Gtk.Align.END) # Right align for user messages + message_container.set_margin_top(4) + message_container.set_margin_bottom(4) + + # Create message bubble + message_bubble = Gtk.Box() + message_bubble.set_name("user-message-bubble") + message_bubble.get_style_context().add_class("user-message-bubble") + message_bubble.set_size_request(300, -1) # Max width: 300px, height: expand + message_bubble.set_vexpand(False) + message_bubble.set_hexpand(False) + + # Create message text view for better text wrapping + message_text = Gtk.TextView() + message_text.set_name("user-message-text") + message_text.get_style_context().add_class("user-message-text") + message_text.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + message_text.set_editable(False) + message_text.set_cursor_visible(False) + message_text.set_size_request(220, 60) # Fixed width and height + message_text.set_hexpand(False) + message_text.set_halign(Gtk.Align.START) + message_text.set_vexpand(False) + message_text.set_margin_start(6) + message_text.set_margin_end(6) + message_text.set_margin_top(3) + message_text.set_margin_bottom(3) + + # Set the text content + text_buffer = message_text.get_buffer() + text_buffer.set_text(message) + + message_bubble.add(message_text) + message_container.add(message_bubble) + + # Add to chat container + self.chat_container.add(message_container) + + # Scroll to bottom + self.chat_scroll.get_vadjustment().set_value( + self.chat_scroll.get_vadjustment().get_upper() + ) + + # Show the new message + self.chat_container.show_all() + + # Get AI response + self.get_ai_response(message) + + def add_ai_message(self, message): + """Add an AI message to the chat area (left side)""" + # Create message container + message_container = Gtk.Box() + message_container.set_orientation(Gtk.Orientation.HORIZONTAL) + message_container.set_halign(Gtk.Align.START) # Left align for AI messages + message_container.set_margin_top(4) + message_container.set_margin_bottom(4) + + # Create message bubble + message_bubble = Gtk.Box() + message_bubble.set_name("ai-message-bubble") + message_bubble.get_style_context().add_class("ai-message-bubble") + message_bubble.set_size_request(300, -1) # Max width: 300px, height: expand + message_bubble.set_vexpand(False) + message_bubble.set_hexpand(False) + + # Create message text view for better text wrapping + message_text = Gtk.TextView() + message_text.set_name("ai-message-text") + message_text.get_style_context().add_class("ai-message-text") + message_text.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + message_text.set_editable(False) + message_text.set_cursor_visible(False) + message_text.set_size_request(220, 60) # Fixed width and height + message_text.set_hexpand(False) + message_text.set_halign(Gtk.Align.START) + message_text.set_vexpand(False) + message_text.set_margin_start(6) + message_text.set_margin_end(6) + message_text.set_margin_top(3) + message_text.set_margin_bottom(3) + + # Set the text content + text_buffer = message_text.get_buffer() + text_buffer.set_text(message) + + message_bubble.add(message_text) + message_container.add(message_bubble) + + # Add to chat container + self.chat_container.add(message_container) + + # Scroll to bottom + self.chat_scroll.get_vadjustment().set_value( + self.chat_scroll.get_vadjustment().get_upper() + ) + + # Show the new message + self.chat_container.show_all() + + def get_ai_response(self, user_message): + """Get response from the selected AI model""" + # Show typing indicator + self.show_typing_indicator() + + def _run_async_response(): + """Run the async response in a new event loop""" + async def _get_response(): + try: + response = await ai_manager.get_response(self.selected_model, user_message) + # Hide typing indicator and show response + GLib.idle_add(self.hide_typing_indicator) + GLib.idle_add(self.add_ai_message, response) + except Exception as e: + error_msg = f"Error getting AI response: {str(e)}" + GLib.idle_add(self.hide_typing_indicator) + GLib.idle_add(self.add_ai_message, error_msg) + + # Create new event loop for this thread + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + loop.run_until_complete(_get_response()) + finally: + loop.close() + + # Run the async function in a new thread + import threading + thread = threading.Thread(target=_run_async_response) + thread.daemon = True + thread.start() + + def show_typing_indicator(self): + """Show typing indicator with animated dots""" + print("Showing typing indicator") + + # Create typing indicator container + typing_container = Gtk.Box() + typing_container.set_orientation(Gtk.Orientation.HORIZONTAL) + typing_container.set_halign(Gtk.Align.START) # Left align for AI messages + typing_container.set_margin_top(4) + typing_container.set_margin_bottom(4) + + # Create typing bubble + typing_bubble = Gtk.Box() + typing_bubble.set_name("typing-bubble") + typing_bubble.get_style_context().add_class("typing-bubble") + typing_bubble.set_size_request(80, 40) # Small size for typing indicator + typing_bubble.set_vexpand(False) + typing_bubble.set_hexpand(False) + + # Create dots container + dots_container = Gtk.Box() + dots_container.set_orientation(Gtk.Orientation.HORIZONTAL) + dots_container.set_spacing(4) + dots_container.set_margin_start(12) + dots_container.set_margin_end(12) + dots_container.set_margin_top(8) + dots_container.set_margin_bottom(8) + + # Create three animated dots + self.typing_dots = [] + for i in range(3): + dot = Gtk.Label() + dot.set_name(f"typing-dot-{i}") + dot.get_style_context().add_class("typing-dot") + dot.get_style_context().add_class("typing-dot-inactive") + dot.set_text("●") + dots_container.add(dot) + self.typing_dots.append(dot) + print(f"Created dot {i}") + + typing_bubble.add(dots_container) + typing_container.add(typing_bubble) + + # Add to chat container + self.chat_container.add(typing_container) + + # Scroll to bottom + self.chat_scroll.get_vadjustment().set_value( + self.chat_scroll.get_vadjustment().get_upper() + ) + + # Show the typing indicator + self.chat_container.show_all() + + # Store reference to typing container for removal + self.current_typing_container = typing_container + + # Initialize animation state + self._dot_index = 0 + + print(f"Starting animation with {len(self.typing_dots)} dots") + # Start animation + GLib.timeout_add(500, self._animate_typing_dots) + + def hide_typing_indicator(self): + """Hide the typing indicator""" + if hasattr(self, 'current_typing_container') and self.current_typing_container: + self.chat_container.remove(self.current_typing_container) + self.current_typing_container = None + self.chat_container.show_all() + + def _animate_typing_dots(self): + """Animate the typing dots""" + if not hasattr(self, 'typing_dots') or not self.typing_dots: + print("No typing dots found") + return False + + print(f"Animating dots, index: {self._dot_index}") + + # Simple animation: cycle through dots + for i, dot in enumerate(self.typing_dots): + if i == (self._dot_index % 3): + dot.get_style_context().add_class("typing-dot-active") + dot.get_style_context().remove_class("typing-dot-inactive") + print(f"Dot {i} is now active") + else: + dot.get_style_context().add_class("typing-dot-inactive") + dot.get_style_context().remove_class("typing-dot-active") + + # Update dot index + self._dot_index += 1 + + # Continue animation if typing indicator is still visible + if hasattr(self, 'current_typing_container') and self.current_typing_container: + GLib.timeout_add(500, self._animate_typing_dots) + + return False # Important: return False to stop the timeout + + def _on_key_press(self, widget, event): + # Close window when Escape key is pressed + if event.keyval == Gdk.KEY_Escape: + print("Escape key pressed - closing AI panel") + self.hide_ai_panel() + return True + return False + + def do_key_press_event(self, event): + """Override key press event for the window""" + if event.keyval == Gdk.KEY_Escape: + print("Escape key pressed (do_key_press_event) - closing AI panel") + self.hide_ai_panel() + return True + return super().do_key_press_event(event) + + def _update_model_button_styles(self): + """Update the styles of all model buttons to reflect the selected model.""" + for model_name, button in self.model_buttons.items(): + if model_name == self.selected_model: + button.get_style_context().add_class("ai-model-button-selected") + button.get_style_context().remove_class("ai-model-button-unselected") + else: + button.get_style_context().add_class("ai-model-button-unselected") + button.get_style_context().remove_class("ai-model-button-selected") \ No newline at end of file diff --git a/modules/ai_services.py b/modules/ai_services.py new file mode 100644 index 00000000..dfc98f20 --- /dev/null +++ b/modules/ai_services.py @@ -0,0 +1,242 @@ +import asyncio +import json +import os +import sys +from typing import Optional + +# Import the data module to get API keys and models +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'config')) +from config.data import ( + AI_OPENAI_API_KEY, AI_GEMINI_API_KEY, AI_CLAUDE_API_KEY, + AI_GROK_API_KEY, AI_DEEPSEEK_API_KEY, + AI_OPENAI_MODEL, AI_GEMINI_MODEL, AI_CLAUDE_MODEL, + AI_GROK_MODEL, AI_DEEPSEEK_MODEL +) + +class AIService: + """Base class for AI services""" + + def __init__(self, api_key: str, model_name: str): + self.api_key = api_key + self.model_name = model_name + + async def generate_response(self, message: str) -> str: + """Generate a response from the AI model""" + if not self.api_key: + return f"API key not found for {self.model_name}. Please make sure you pasted it in the Ax-Shell settings." + + try: + # Run the API call in a thread to avoid blocking + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, self._make_api_call_sync, message) + except Exception as e: + return f"Error communicating with {self.model_name}: {str(e)}" + + def _make_api_call_sync(self, message: str) -> str: + """Synchronous version of the API call""" + return self._make_api_call(message) + + async def _make_api_call(self, message: str) -> str: + """Make the actual API call - to be implemented by subclasses""" + raise NotImplementedError + +class OpenAIService(AIService): + """OpenAI/ChatGPT service""" + + def __init__(self): + super().__init__(AI_OPENAI_API_KEY, "Chat GPT") + self.model = AI_OPENAI_MODEL + + async def _make_api_call(self, message: str) -> str: + try: + import openai + openai.api_key = self.api_key + + response = await asyncio.to_thread( + openai.ChatCompletion.create, + model=self.model, + messages=[ + {"role": "user", "content": message} + ], + max_tokens=1000, + temperature=0.7 + ) + + return response.choices[0].message.content + except ImportError: + return "OpenAI library not installed. Please install it with: pip install openai" + except Exception as e: + return f"OpenAI API error: {str(e)}" + +class GeminiService(AIService): + """Google Gemini service""" + + def __init__(self): + super().__init__(AI_GEMINI_API_KEY, "Gemini") + self.model = AI_GEMINI_MODEL + + def _make_api_call_sync(self, message: str) -> str: + """Synchronous API call for Gemini""" + try: + import google.generativeai as genai + + # Validate API key + if not self.api_key or self.api_key.strip() == "": + return "Gemini API key is empty. Please add your API key in the Ax-Shell settings." + + # Configure the API + genai.configure(api_key=self.api_key) + + # Test the API key with a simple call + try: + model = genai.GenerativeModel(self.model) + + # Make the API call with error handling + response = model.generate_content(message) + + # Check if response is valid and has content + if response and hasattr(response, 'text') and response.text: + return response.text + elif response and hasattr(response, 'parts') and response.parts: + # Handle response with parts + return response.parts[0].text + elif response and hasattr(response, 'candidates') and response.candidates: + # Try accessing through candidates + candidate = response.candidates[0] + if hasattr(candidate, 'content') and candidate.content: + if hasattr(candidate.content, 'parts') and candidate.content.parts: + return candidate.content.parts[0].text + else: + return "Gemini API returned an empty response. Please try again." + + except Exception as api_error: + # Handle specific API errors + error_str = str(api_error) + if "API_KEY_INVALID" in error_str or "INVALID_ARGUMENT" in error_str: + return "Invalid Gemini API key. Please check your API key in the Ax-Shell settings." + elif "QUOTA_EXCEEDED" in error_str: + return "Gemini API quota exceeded. Please check your usage limits." + elif "PERMISSION_DENIED" in error_str: + return "Permission denied. Please check your Gemini API key permissions." + elif "NoneType" in error_str and "from_call" in error_str: + return """Gemini API is currently experiencing issues. + +This appears to be a known issue with the Google Generative AI library. + +Please try: +1. Using a different AI model (ChatGPT, Claude, or Deepseek) +2. Updating the library: pip install --upgrade google-generativeai +3. Checking your internet connection + +For now, I recommend using ChatGPT or Claude instead.""" + else: + return f"Gemini API error: {error_str}" + + except ImportError: + return "Google Generative AI library not installed. Please install it with: pip install google-generativeai" + except Exception as e: + return f"Gemini configuration error: {str(e)}" + + async def _make_api_call(self, message: str) -> str: + """Async wrapper for the synchronous API call""" + return self._make_api_call_sync(message) + +class ClaudeService(AIService): + """Anthropic Claude service""" + + def __init__(self): + super().__init__(AI_CLAUDE_API_KEY, "Claude") + self.model = AI_CLAUDE_MODEL + + async def _make_api_call(self, message: str) -> str: + try: + import anthropic + client = anthropic.Anthropic(api_key=self.api_key) + + response = await asyncio.to_thread( + client.messages.create, + model=self.model, + max_tokens=1000, + messages=[ + {"role": "user", "content": message} + ] + ) + + return response.content[0].text + except ImportError: + return "Anthropic library not installed. Please install it with: pip install anthropic" + except Exception as e: + return f"Claude API error: {str(e)}" + +class GrokService(AIService): + """xAI Grok service""" + + def __init__(self): + super().__init__(AI_GROK_API_KEY, "Grok") + self.model = AI_GROK_MODEL + + async def _make_api_call(self, message: str) -> str: + # Grok API is not publicly available yet, so we'll return a placeholder + return f"Grok API is not publicly available yet. Selected model: {self.model}. Please check back later for updates." + +class DeepseekService(AIService): + """Deepseek service""" + + def __init__(self): + super().__init__(AI_DEEPSEEK_API_KEY, "Deepseek") + self.model = AI_DEEPSEEK_MODEL + + async def _make_api_call(self, message: str) -> str: + try: + import openai + openai.api_key = self.api_key + openai.api_base = "https://api.deepseek.com/v1" + + response = await asyncio.to_thread( + openai.ChatCompletion.create, + model=self.model, + messages=[ + {"role": "user", "content": message} + ], + max_tokens=1000, + temperature=0.7 + ) + + return response.choices[0].message.content + except ImportError: + return "OpenAI library not installed. Please install it with: pip install openai" + except Exception as e: + return f"Deepseek API error: {str(e)}" + +class AIManager: + """Manager class for all AI services""" + + def __init__(self): + self.services = { + "Chat GPT": OpenAIService(), + "Gemini": GeminiService(), + "Claude": ClaudeService(), + "Grok": GrokService(), + "Deepseek": DeepseekService(), + } + + async def get_response(self, model_name: str, message: str) -> str: + """Get a response from the specified AI model""" + if model_name not in self.services: + return f"Unknown AI model: {model_name}" + + service = self.services[model_name] + return await service.generate_response(message) + + def get_available_models(self) -> list: + """Get list of available models""" + return list(self.services.keys()) + + def has_api_key(self, model_name: str) -> bool: + """Check if a model has an API key configured""" + if model_name not in self.services: + return False + return bool(self.services[model_name].api_key) + +# Global AI manager instance +ai_manager = AIManager() \ No newline at end of file diff --git a/modules/notch.py b/modules/notch.py index 79462dda..4ae76cc6 100644 --- a/modules/notch.py +++ b/modules/notch.py @@ -22,11 +22,11 @@ from utils.icon_resolver import IconResolver from utils.occlusion import check_occlusion from widgets.wayland import WaylandWindow as Window +from modules.ai import AI class Notch(Window): def __init__(self, **kwargs): - is_panel_vertical = False if data.PANEL_THEME == "Panel": is_panel_vertical = data.VERTICAL @@ -35,12 +35,10 @@ def __init__(self, **kwargs): revealer_transition_type = "slide-down" if data.PANEL_THEME == "Notch": - anchor_val = "top" revealer_transition_type = "slide-down" elif data.PANEL_THEME == "Panel": if is_panel_vertical: - if data.BAR_POSITION == "Left": match data.PANEL_POSITION: case "Start": @@ -70,7 +68,6 @@ def __init__(self, **kwargs): anchor_val = "right" revealer_transition_type = "slide-left" else: - if data.BAR_POSITION == "Top": match data.PANEL_POSITION: case "Start": @@ -121,12 +118,12 @@ def __init__(self, **kwargs): current_margin_str = dense_edge_margin_top_str case _: current_margin_str = default_top_anchor_margin_str - + super().__init__( name="notch", layer="top", - anchor=anchor_val, - margin=current_margin_str, + anchor=anchor_val, + margin=current_margin_str, keyboard_mode="none", exclusivity="none" if data.PANEL_THEME == "Notch" else "normal", visible=True, @@ -141,7 +138,7 @@ def __init__(self, **kwargs): self.is_hovered = False self._prevent_occlusion = False self._occlusion_timer_id = None - + self.icon_resolver = IconResolver() self._all_apps = get_desktop_applications() self.app_identifiers = self._build_app_identifiers_map() @@ -156,13 +153,13 @@ def __init__(self, **kwargs): self.btdevices.set_visible(False) self.nwconnections.set_visible(False) - self.launcher = AppLauncher(notch=self) self.overview = Overview() self.emoji = EmojiPicker(notch=self) self.power = PowerMenu(notch=self) self.tmux = TmuxManager(notch=self) self.cliphist = ClipHistory(notch=self) + self.ai = AI() self.window_label = Label( name="notch-window-label", @@ -171,9 +168,7 @@ def __init__(self, **kwargs): ) self.window_icon = Image( - name="notch-window-icon", - icon_name="application-x-executable", - icon_size=20 + name="notch-window-icon", icon_name="application-x-executable", icon_size=20 ) self.active_window = ActiveWindow( @@ -184,17 +179,20 @@ def __init__(self, **kwargs): f"{{'Desktop' if not win_title or win_title == 'unknown' else win_title}}", ), ) - + self.active_window_box = CenterBox( name="active-window-box", h_expand=True, h_align="fill", start_children=self.window_icon, center_children=self.active_window, - end_children=None + end_children=None, ) - self.active_window_box.connect("button-press-event", lambda widget, event: (self.open_notch("dashboard"), False)[1]) + self.active_window_box.connect( + "button-press-event", + lambda widget, event: (self.open_notch("dashboard"), False)[1], + ) self.active_window.connect("notify::label", self.update_window_icon) @@ -205,13 +203,22 @@ def __init__(self, **kwargs): self.active_window.get_children()[0].set_halign(Gtk.Align.FILL) self.active_window.get_children()[0].set_ellipsize(Pango.EllipsizeMode.END) - self.active_window.connect("notify::label", lambda *_: self.restore_label_properties()) + self.active_window.connect( + "notify::label", lambda *_: self.restore_label_properties() + ) self.player_small = PlayerSmall() - self.user_label = Label(name="compact-user", label=f"{data.USERNAME}@{data.HOSTNAME}") + self.user_label = Label( + name="compact-user", label=f"{data.USERNAME}@{data.HOSTNAME}" + ) - self.player_small.mpris_manager.connect("player-appeared", lambda *_: self.compact_stack.set_visible_child(self.player_small)) - self.player_small.mpris_manager.connect("player-vanished", self.on_player_vanished) + self.player_small.mpris_manager.connect( + "player-appeared", + lambda *_: self.compact_stack.set_visible_child(self.player_small), + ) + self.player_small.mpris_manager.connect( + "player-vanished", self.on_player_vanished + ) self.compact_stack = Stack( name="notch-compact-stack", @@ -223,7 +230,7 @@ def __init__(self, **kwargs): self.user_label, self.active_window_box, self.player_small, - ] + ], ) self.compact_stack.set_visible_child(self.active_window_box) @@ -231,12 +238,15 @@ def __init__(self, **kwargs): self.compact.set_visible(True) self.compact.add(self.compact_stack) self.compact.add_events( - Gdk.EventMask.SCROLL_MASK | - Gdk.EventMask.BUTTON_PRESS_MASK | - Gdk.EventMask.SMOOTH_SCROLL_MASK + Gdk.EventMask.SCROLL_MASK + | Gdk.EventMask.BUTTON_PRESS_MASK + | Gdk.EventMask.SMOOTH_SCROLL_MASK ) self.compact.connect("scroll-event", self._on_compact_scroll) - self.compact.connect("button-press-event", lambda widget, event: (self.open_notch("dashboard"), False)[1]) + self.compact.connect( + "button-press-event", + lambda widget, event: (self.open_notch("dashboard"), False)[1], + ) self.compact.connect("enter-notify-event", self.on_button_enter) self.compact.connect("leave-notify-event", self.on_button_leave) @@ -245,7 +255,10 @@ def __init__(self, **kwargs): name="notch-content", v_expand=True, h_expand=True, - style_classes = ["invert"] if (not data.VERTICAL and data.BAR_THEME in ["Dense", "Edge"]) and data.BAR_POSITION not in ["Bottom"] else [], + style_classes=["invert"] + if (not data.VERTICAL and data.BAR_THEME in ["Dense", "Edge"]) + and data.BAR_POSITION not in ["Bottom"] + else [], transition_type="crossfade", transition_duration=250, children=[ @@ -258,7 +271,7 @@ def __init__(self, **kwargs): self.tools, self.tmux, self.cliphist, - ] + ], ) if data.PANEL_THEME == "Panel": @@ -267,7 +280,9 @@ def __init__(self, **kwargs): self.stack.add_style_class(data.BAR_POSITION.lower()) self.stack.add_style_class(data.PANEL_POSITION.lower()) - if is_panel_vertical or (data.PANEL_POSITION in ["Start", "End"] and data.PANEL_THEME == "Panel"): + if is_panel_vertical or ( + data.PANEL_POSITION in ["Start", "End"] and data.PANEL_THEME == "Panel" + ): self.compact.set_size_request(260, 40) self.launcher.set_size_request(320, 635) self.tmux.set_size_request(320, 635) @@ -291,7 +306,7 @@ def __init__(self, **kwargs): children=[ MyCorner("top-right"), Box(), - ] + ], ) self.corner_right = Box( @@ -301,7 +316,7 @@ def __init__(self, **kwargs): children=[ MyCorner("top-left"), Box(), - ] + ], ) self.notch_box = CenterBox( @@ -315,7 +330,7 @@ def __init__(self, **kwargs): ) self.notch_box.add_style_class(data.PANEL_THEME.lower()) - + self.notch_revealer = Revealer( name="notch-revealer", transition_type=revealer_transition_type, @@ -327,8 +342,12 @@ def __init__(self, **kwargs): self.notch_hover_area_event_box = Gtk.EventBox() self.notch_hover_area_event_box.add(self.notch_revealer) if data.PANEL_THEME == "Notch": - self.notch_hover_area_event_box.connect("enter-notify-event", self.on_notch_hover_area_enter) - self.notch_hover_area_event_box.connect("leave-notify-event", self.on_notch_hover_area_leave) + self.notch_hover_area_event_box.connect( + "enter-notify-event", self.on_notch_hover_area_enter + ) + self.notch_hover_area_event_box.connect( + "leave-notify-event", self.on_notch_hover_area_leave + ) self.notch_hover_area_event_box.set_size_request(-1, 1) self.notch_complete = Box( @@ -336,7 +355,7 @@ def __init__(self, **kwargs): orientation="v" if is_panel_vertical else "h", children=[ self.notch_hover_area_event_box, - ] + ], ) self._is_notch_open = False @@ -380,7 +399,7 @@ def __init__(self, **kwargs): self.notch_children = [ self.notch_complete, ] - + self.notch_wrap = Box( name="notch-wrap", children=self.notch_children, @@ -391,20 +410,22 @@ def __init__(self, **kwargs): self.add_keybinding("Escape", lambda *_: self.close_notch()) self.add_keybinding("Ctrl Tab", lambda *_: self.dashboard.go_to_next_child()) - self.add_keybinding("Ctrl Shift ISO_Left_Tab", lambda *_: self.dashboard.go_to_previous_child()) - + self.add_keybinding( + "Ctrl Shift ISO_Left_Tab", lambda *_: self.dashboard.go_to_previous_child() + ) + self.update_window_icon() - - self.active_window.connect("button-press-event", lambda widget, event: (self.open_notch("dashboard"), False)[1]) + + self.active_window.connect( + "button-press-event", + lambda widget, event: (self.open_notch("dashboard"), False)[1], + ) if data.PANEL_THEME != "Notch": for corner in [self.corner_left, self.corner_right]: corner.set_visible(False) - self._current_window_class = self._get_current_window_class() - - if data.PANEL_THEME == "Notch" and data.BAR_POSITION != "Top": GLib.timeout_add(250, self._check_occlusion) @@ -412,7 +433,6 @@ def __init__(self, **kwargs): self.notch_revealer.set_reveal_child(True) else: self.notch_revealer.set_reveal_child(False) - self.connect("key-press-event", self.on_key_press) @@ -424,10 +444,9 @@ def on_button_enter(self, widget, event): return True def on_button_leave(self, widget, event): - if event.detail == Gdk.NotifyType.INFERIOR: return False - + self.is_hovered = False window = widget.get_window() if window: @@ -446,7 +465,7 @@ def on_notch_hover_area_leave(self, widget, event): if event.detail == Gdk.NotifyType.INFERIOR: return False - + self.is_hovered = False return False @@ -469,39 +488,42 @@ def open_notch(self, widget_name: str): self.notch_box.add_style_class("open") self.stack.add_style_class("open") current_stack_child = self.stack.get_visible_child() - is_dashboard_currently_visible = (current_stack_child == self.dashboard) + is_dashboard_currently_visible = current_stack_child == self.dashboard if widget_name == "network_applet": if is_dashboard_currently_visible: - - if self.dashboard.stack.get_visible_child() == self.dashboard.widgets and \ - self.applet_stack.get_visible_child() == self.nwconnections: + if ( + self.dashboard.stack.get_visible_child() == self.dashboard.widgets + and self.applet_stack.get_visible_child() == self.nwconnections + ): self.close_notch() return - self.set_keyboard_mode("exclusive") + self.set_keyboard_mode("exclusive") self.dashboard.go_to_section("widgets") self.applet_stack.set_visible_child(self.nwconnections) return elif widget_name == "bluetooth": if is_dashboard_currently_visible: - - if self.dashboard.stack.get_visible_child() == self.dashboard.widgets and \ - self.applet_stack.get_visible_child() == self.btdevices: + if ( + self.dashboard.stack.get_visible_child() == self.dashboard.widgets + and self.applet_stack.get_visible_child() == self.btdevices + ): self.close_notch() return - self.set_keyboard_mode("exclusive") + self.set_keyboard_mode("exclusive") self.dashboard.go_to_section("widgets") self.applet_stack.set_visible_child(self.btdevices) return elif widget_name == "dashboard": if is_dashboard_currently_visible: - - if self.dashboard.stack.get_visible_child() == self.dashboard.widgets and \ - self.applet_stack.get_visible_child() == self.nhistory: + if ( + self.dashboard.stack.get_visible_child() == self.dashboard.widgets + and self.applet_stack.get_visible_child() == self.nhistory + ): self.close_notch() return @@ -518,7 +540,10 @@ def open_notch(self, widget_name: str): if widget_name in dashboard_sections_map: section_widget_instance = dashboard_sections_map[widget_name] - if is_dashboard_currently_visible and self.dashboard.stack.get_visible_child() == section_widget_instance: + if ( + is_dashboard_currently_visible + and self.dashboard.stack.get_visible_child() == section_widget_instance + ): self.close_notch() return @@ -529,15 +554,40 @@ def open_notch(self, widget_name: str): hide_bar_revealers = False widget_configs = { - "tmux": {"instance": self.tmux, "action": self.tmux.open_manager}, - "cliphist": {"instance": self.cliphist, "action": lambda: GLib.idle_add(self.cliphist.open)}, - "launcher": {"instance": self.launcher, "action": self.launcher.open_launcher, "focus": lambda: (self.launcher.search_entry.set_text(""), self.launcher.search_entry.grab_focus())}, - "emoji": {"instance": self.emoji, "action": self.emoji.open_picker, "focus": lambda: (self.emoji.search_entry.set_text(""), self.emoji.search_entry.grab_focus())}, - "overview": {"instance": self.overview, "hide_revealers": True}, - "power": {"instance": self.power}, - "tools": {"instance": self.tools}, + "tmux": {"instance": self.tmux, "action": self.tmux.open_manager}, + "cliphist": { + "instance": self.cliphist, + "action": lambda: GLib.idle_add(self.cliphist.open), + }, + "launcher": { + "instance": self.launcher, + "action": self.launcher.open_launcher, + "focus": lambda: ( + self.launcher.search_entry.set_text(""), + self.launcher.search_entry.grab_focus(), + ), + }, + "emoji": { + "instance": self.emoji, + "action": self.emoji.open_picker, + "focus": lambda: ( + self.emoji.search_entry.set_text(""), + self.emoji.search_entry.grab_focus(), + ), + }, + "overview": {"instance": self.overview, "hide_revealers": True}, + "power": {"instance": self.power}, + "tools": {"instance": self.tools}, } + if widget_name == "ai": + # Handle AI window separately since it's a standalone window + if self.ai.get_visible(): + self.ai.hide_ai_panel() + else: + self.ai.show_ai_panel() + return + if widget_name in widget_configs: config = widget_configs[widget_name] target_widget_on_stack = config["instance"] @@ -549,7 +599,6 @@ def open_notch(self, widget_name: str): self.close_notch() return else: - target_widget_on_stack = self.dashboard hide_bar_revealers = True @@ -574,14 +623,18 @@ def open_notch(self, widget_name: str): self.dashboard.go_to_section("widgets") self.applet_stack.set_visible_child(self.nhistory) - - if data.BAR_POSITION in ["Top", "Bottom"] and data.PANEL_THEME == "Panel" or data.BAR_POSITION in ["Bottom"] and data.PANEL_THEME == "Notch": + if ( + data.BAR_POSITION in ["Top", "Bottom"] + and data.PANEL_THEME == "Panel" + or data.BAR_POSITION in ["Bottom"] + and data.PANEL_THEME == "Notch" + ): self.bar.revealer_right.set_reveal_child(True) self.bar.revealer_left.set_reveal_child(True) else: self.bar.revealer_right.set_reveal_child(not hide_bar_revealers) self.bar.revealer_left.set_reveal_child(not hide_bar_revealers) - + self._is_notch_open = True def toggle_hidden(self): @@ -637,28 +690,23 @@ def _build_app_identifiers_map(self): """Build a mapping of app identifiers (class names, executables, names) to DesktopApp objects""" identifiers = {} for app in self._all_apps: - if app.name: identifiers[app.name.lower()] = app - if app.display_name: identifiers[app.display_name.lower()] = app - if app.window_class: identifiers[app.window_class.lower()] = app - if app.executable: - exe_basename = app.executable.split('/')[-1].lower() + exe_basename = app.executable.split("/")[-1].lower() identifiers[exe_basename] = app - if app.command_line: - cmd_base = app.command_line.split()[0].split('/')[-1].lower() + cmd_base = app.command_line.split()[0].split("/")[-1].lower() identifiers[cmd_base] = app - + return identifiers def find_app(self, app_id: str): @@ -672,65 +720,69 @@ def update_window_icon(self, *args): label_widget = self.active_window.get_children()[0] if not isinstance(label_widget, Gtk.Label): return - title = label_widget.get_text() - if title == 'Desktop' or not title: - + if title == "Desktop" or not title: self.window_icon.set_visible(False) return - self.window_icon.set_visible(True) - from fabric.hyprland.widgets import get_hyprland_connection + conn = get_hyprland_connection() if conn: try: import json + active_window_json = conn.send_command("j/activewindow").reply.decode() active_window_data = json.loads(active_window_json) - app_id = active_window_data.get("initialClass", "") or active_window_data.get("class", "") - + app_id = active_window_data.get( + "initialClass", "" + ) or active_window_data.get("class", "") icon_size = 20 desktop_app = self.find_app(app_id) - icon_pixbuf = None if desktop_app: icon_pixbuf = desktop_app.get_icon_pixbuf(size=icon_size) - - if not icon_pixbuf: + if not icon_pixbuf: icon_pixbuf = self.icon_resolver.get_icon_pixbuf(app_id, icon_size) - - if not icon_pixbuf and "-" in app_id: + if not icon_pixbuf and "-" in app_id: base_app_id = app_id.split("-")[0] - icon_pixbuf = self.icon_resolver.get_icon_pixbuf(base_app_id, icon_size) - + icon_pixbuf = self.icon_resolver.get_icon_pixbuf( + base_app_id, icon_size + ) + if icon_pixbuf: self.window_icon.set_from_pixbuf(icon_pixbuf) else: - try: - self.window_icon.set_from_icon_name("application-x-executable", 20) + self.window_icon.set_from_icon_name( + "application-x-executable", 20 + ) except: - - self.window_icon.set_from_icon_name("application-x-executable-symbolic", 20) + self.window_icon.set_from_icon_name( + "application-x-executable-symbolic", 20 + ) except Exception as e: print(f"Error updating window icon: {e}") try: self.window_icon.set_from_icon_name("application-x-executable", 20) except: - self.window_icon.set_from_icon_name("application-x-executable-symbolic", 20) + self.window_icon.set_from_icon_name( + "application-x-executable-symbolic", 20 + ) else: try: self.window_icon.set_from_icon_name("application-x-executable", 20) except: - self.window_icon.set_from_icon_name("application-x-executable-symbolic", 20) + self.window_icon.set_from_icon_name( + "application-x-executable-symbolic", 20 + ) def _check_occlusion(self): """ @@ -738,27 +790,29 @@ def _check_occlusion(self): and update the notch_revealer accordingly. """ - - occlusion_edge = "top" occlusion_size = 40 if not (self.is_hovered or self._is_notch_open or self._prevent_occlusion): - is_occluded = check_occlusion((occlusion_edge, occlusion_size)) + is_occluded = check_occlusion((occlusion_edge, occlusion_size)) self.notch_revealer.set_reveal_child(not is_occluded) - + return True def _get_current_window_class(self): """Get the class of the currently active window""" try: from fabric.hyprland.widgets import get_hyprland_connection + conn = get_hyprland_connection() if conn: import json + active_window_json = conn.send_command("j/activewindow").reply.decode() active_window_data = json.loads(active_window_json) - return active_window_data.get("initialClass", "") or active_window_data.get("class", "") + return active_window_data.get( + "initialClass", "" + ) or active_window_data.get("class", "") except Exception as e: print(f"Error getting window class: {e}") return "" @@ -771,33 +825,28 @@ def on_active_window_changed(self, *args): if data.PANEL_THEME != "Notch": return - new_window_class = self._get_current_window_class() - if new_window_class != self._current_window_class: - self._current_window_class = new_window_class - if self._occlusion_timer_id is not None: GLib.source_remove(self._occlusion_timer_id) self._occlusion_timer_id = None - self._prevent_occlusion = True self.notch_revealer.set_reveal_child(True) - - self._occlusion_timer_id = GLib.timeout_add(500, self._restore_occlusion_check) - + self._occlusion_timer_id = GLib.timeout_add( + 500, self._restore_occlusion_check + ) + def _restore_occlusion_check(self): """Re-enable occlusion checking after temporary visibility""" self._prevent_occlusion = False self._occlusion_timer_id = None - return False @@ -805,14 +854,11 @@ def open_launcher_with_text(self, initial_text): """Open the launcher with initial text in the search field.""" self._launcher_transitioning = True - if initial_text: self._typed_chars_buffer = initial_text - if self.stack.get_visible_child() == self.launcher: - current_text = self.launcher.search_entry.get_text() self.launcher.search_entry.set_text(current_text + initial_text) @@ -820,88 +866,90 @@ def open_launcher_with_text(self, initial_text): self.launcher.search_entry.select_region(-1, -1) self.launcher.search_entry.grab_focus() return - self.set_keyboard_mode("exclusive") - - for style in ["launcher", "dashboard", "notification", "overview", "emoji", "power", "tools", "tmux"]: + for style in [ + "launcher", + "dashboard", + "notification", + "overview", + "emoji", + "power", + "tools", + "tmux", + ]: self.stack.remove_style_class(style) - for w in [self.launcher, self.dashboard, self.overview, self.emoji, self.power, self.tools, self.tmux, self.cliphist]: + for w in [ + self.launcher, + self.dashboard, + self.overview, + self.emoji, + self.power, + self.tools, + self.tmux, + self.cliphist, + ]: w.remove_style_class("open") - self.stack.add_style_class("launcher") self.stack.set_visible_child(self.launcher) self.launcher.add_style_class("open") - self.launcher.ensure_initialized() - self.launcher.open_launcher() - if self._launcher_transition_timeout: GLib.source_remove(self._launcher_transition_timeout) - - self._launcher_transition_timeout = GLib.timeout_add(150, self._finalize_launcher_transition) - + + self._launcher_transition_timeout = GLib.timeout_add( + 150, self._finalize_launcher_transition + ) self.bar.revealer_right.set_reveal_child(True) self.bar.revealer_left.set_reveal_child(True) - + self._is_notch_open = True - + def _finalize_launcher_transition(self): """Apply buffered text and finalize launcher transition""" if self._typed_chars_buffer: - entry = self.launcher.search_entry entry.set_text(self._typed_chars_buffer) - entry.grab_focus() - GLib.timeout_add(10, self._ensure_no_text_selection) GLib.timeout_add(50, self._ensure_no_text_selection) GLib.timeout_add(100, self._ensure_no_text_selection) - print(f"Applied buffered text: '{self._typed_chars_buffer}'") - self._typed_chars_buffer = "" - self._launcher_transitioning = False self._launcher_transition_timeout = None - + return False - + def _ensure_no_text_selection(self): """Make absolutely sure no text is selected in the search entry""" entry = self.launcher.search_entry - text_len = len(entry.get_text()) - entry.set_position(text_len) - entry.select_region(text_len, text_len) - if not entry.has_focus(): entry.grab_focus() GLib.idle_add(lambda: entry.select_region(text_len, text_len)) - + return False - def on_key_press(self, widget, event): """Handle key presses at the notch level""" @@ -909,45 +957,44 @@ def on_key_press(self, widget, event): if self._launcher_transitioning: keyval = event.keyval keychar = chr(keyval) if 32 <= keyval <= 126 else None - is_valid_char = ( - (keyval >= Gdk.KEY_a and keyval <= Gdk.KEY_z) or - (keyval >= Gdk.KEY_A and keyval <= Gdk.KEY_Z) or - (keyval >= Gdk.KEY_0 and keyval <= Gdk.KEY_9) or - keyval in (Gdk.KEY_space, Gdk.KEY_underscore, Gdk.KEY_minus, Gdk.KEY_period) + (keyval >= Gdk.KEY_a and keyval <= Gdk.KEY_z) + or (keyval >= Gdk.KEY_A and keyval <= Gdk.KEY_Z) + or (keyval >= Gdk.KEY_0 and keyval <= Gdk.KEY_9) + or keyval + in (Gdk.KEY_space, Gdk.KEY_underscore, Gdk.KEY_minus, Gdk.KEY_period) ) - - if is_valid_char and keychar: + if is_valid_char and keychar: self._typed_chars_buffer += keychar - print(f"Buffered character: {keychar}, buffer now: '{self._typed_chars_buffer}'") + print( + f"Buffered character: {keychar}, buffer now: '{self._typed_chars_buffer}'" + ) return True - - - if (self.stack.get_visible_child() == self.dashboard and - self.dashboard.stack.get_visible_child() == self.dashboard.widgets): - + if ( + self.stack.get_visible_child() == self.dashboard + and self.dashboard.stack.get_visible_child() == self.dashboard.widgets + ): if self.stack.get_visible_child() == self.launcher: return False - keyval = event.keyval keychar = chr(keyval) if 32 <= keyval <= 126 else None - is_valid_char = ( - (keyval >= Gdk.KEY_a and keyval <= Gdk.KEY_z) or - (keyval >= Gdk.KEY_A and keyval <= Gdk.KEY_Z) or - (keyval >= Gdk.KEY_0 and keyval <= Gdk.KEY_9) or - keyval in (Gdk.KEY_space, Gdk.KEY_underscore, Gdk.KEY_minus, Gdk.KEY_period) + (keyval >= Gdk.KEY_a and keyval <= Gdk.KEY_z) + or (keyval >= Gdk.KEY_A and keyval <= Gdk.KEY_Z) + or (keyval >= Gdk.KEY_0 and keyval <= Gdk.KEY_9) + or keyval + in (Gdk.KEY_space, Gdk.KEY_underscore, Gdk.KEY_minus, Gdk.KEY_period) ) - + if is_valid_char and keychar: print(f"Notch received keypress: {keychar}") self.open_launcher_with_text(keychar) return True - + return False diff --git a/styles/ai.css b/styles/ai.css new file mode 100644 index 00000000..e9c046c1 --- /dev/null +++ b/styles/ai.css @@ -0,0 +1,221 @@ + +/* AI Panel Main Styling */ +#ai-panel { + background-color: var(--surface-bright); + border-radius: 0 16px 16px 0; + padding: 16px; + margin: 0px 0px 0px 0px; + min-width: 400px; + min-height: 800px; +} + +/* AI Dropdown Styling */ +.ai-dropdown { + background-color: var(--foreground); +} + +.ai-dropdown popup * { + color: var(--foreground); + background-color: var(--background); +} + +.ai-dropdown popup menuitem { + color: var(--foreground); + background-color: var(--background); +} + +.ai-dropdown popup menuitem:hover { + background-color: var(--red); +} + +/* AI Chat Styling */ +#ai-chat-scroll { + background-color: transparent; + border-radius: 8px; +} + +#ai-chat-container { + background-color: transparent; +} + +#user-message-bubble { + background-color: var(--cyan); + border-radius: 12px; + padding: 8px 12px; + margin-left: 40px; + +} + +/* AI Text Entry Styling */ +#ai-text-view { + background-color: transparent; + color: var(--foreground); + font-size: 1em; + border: none; + outline: none; +} + +#ai-text-view text { + background-color: transparent; + color: var(--foreground); + font-size: 1em; + border: none; + outline: none; +} + + /* AI Gear Button Styling */ +#ai-model-button { + background-color: var(--surface-bright); + color: var(--foreground); + min-width: 20px; + min-height: 40px; + border-radius: 8px; + padding: 8px; +} + +#ai-model-icon { + color: var(--foreground); + font-size: 2em; + font-weight: bold; +} + + /* User Message Styling */ +.user-message-bubble { + background-color: var(--cyan); + border-radius: 12px; + padding: 8px 12px; + margin-left: 40px; +} + +.user-message-text { + color: var(--background); + font-size: 1.1em; + background-color: transparent; + border: none; + outline: none; +} + +.user-message-text text { + background-color: transparent; + border: none; + outline: none; + color: var(--background); + font-size: 1.1em; + font-weight: bold; +} + + /* AI Message Styling */ +.ai-message-bubble { + background-color: var(--surface-bright); + border-radius: 12px; + padding: 8px 12px; + margin-right: 40px; +} + +.ai-message-text { + color: var(--foreground); + font-size: 1.1em; + background-color: transparent; + border: none; + outline: none; +} + +.ai-message-text text { + background-color: transparent; + color: var(--foreground); + font-size: 1.1em; + border: none; + outline: none; +} + + /* Typing Indicator Styling */ +.typing-bubble { + background-color: var(--surface-bright); + border-radius: 12px; + padding: 8px 12px; + margin-right: 40px; +} + +/* AI Model Options Box Styling */ +#ai-model-options-box { + background-color: var(--surface); + border: 1px solid var(--outline); + border-radius: 8px; + padding: 8px; +} + +/* AI Model Option Button Styling */ +#ai-model-option-button { + background-color: var(--surface-bright); + font-size: 0.9em; + color: var(--cyan); + border: none; + outline: none; + box-shadow: none; +} + +.ai-model-button-selected { + border: none; + outline: none; + border-radius: 4px; + padding: 6px 12px; + background-color: var(--cyan); + font-size: 1.1em; + font-weight: bold; + color: #000000; +} + +#ai-model-option-button.ai-model-button-selected { + border: none; + outline: none; + border-radius: 4px; + padding: 6px 12px; + background-color: var(--cyan); + font-size: 0.9em; + color: #000000; +} + +.ai-model-button-selected label { + color: #000000; +} + +#ai-model-option-button.ai-model-button-selected label { + color: #000000; +} + +.ai-model-button-unselected { + border: none; + border-radius: 4px; + padding: 6px 12px; + background-color: var(--surface); + font-size: 0.9em; + color: var(--foreground); +} + +/* AI Entry Container Styling */ +#ai-entry-container { + background-color: var(--surface-bright); + border-radius: 8px; + padding: 12px 16px; +} + +.typing-dot { + color: var(--outline); + font-size: 1.2em; + background-color: transparent; + border: none; + outline: none; + transition: color 0.2s ease-in-out; +} + +.typing-dot-active { + color: var(--foreground); + font-size: 1.2em; + font-weight: bold; +} + +.typing-dot-inactive { + color: var(--outline); + font-size: 1.2em; + opacity: 0.5; +} \ No newline at end of file From 1214b3f147885b3a7a5317c8a539aeaf4625146e Mon Sep 17 00:00:00 2001 From: KapaDev Date: Sun, 27 Jul 2025 14:34:26 +0300 Subject: [PATCH 2/5] tweak --- modules/ai.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/modules/ai.py b/modules/ai.py index e8359663..adf815f6 100644 --- a/modules/ai.py +++ b/modules/ai.py @@ -10,7 +10,6 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk, GLib -from fabric.widgets.stack import Stack # Import AI services from .ai_services import ai_manager @@ -226,19 +225,6 @@ def _hide_after_animation(self): self.hide() return False - - - # Initialize variable to store the text - self.current_message = "" - self.selected_model = "Chat GPT" # Default model - - # Close window on Escape key press - self.add_events(Gdk.EventMask.KEY_PRESS_MASK) - self.connect("key-press-event", self._on_key_press) - - # Also connect to the main window for key events - self.connect("key-press-event", self._on_key_press) - def _on_model_button_clicked(self, button): """Handle gear button click - manually show popover""" self.model_popover.show_all() From 134839f5f327433266023912f0667d440912e3d5 Mon Sep 17 00:00:00 2001 From: KapaDev Date: Sun, 27 Jul 2025 20:58:39 +0300 Subject: [PATCH 3/5] working chat --- modules/ai.py | 403 ++++++++++++++++++++--------------------------- modules/notch.py | 11 +- styles/ai.css | 17 +- 3 files changed, 191 insertions(+), 240 deletions(-) diff --git a/modules/ai.py b/modules/ai.py index adf815f6..4e52f0e5 100644 --- a/modules/ai.py +++ b/modules/ai.py @@ -1,3 +1,4 @@ +from pydoc import text from fabric.widgets.box import Box from fabric.widgets.label import Label from fabric.widgets.button import Button @@ -7,12 +8,16 @@ from widgets.wayland import WaylandWindow as Window import gi import asyncio - gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, GLib +from gi.repository import Gtk, Gdk, GLib, Pango # Import AI services from .ai_services import ai_manager + +# Utility to break long unbreakable words for GTK Label wrapping +import re +def break_long_words(text, n=20): + return re.sub(r'(\S{' + str(n) + r',})', lambda m: '\u200b'.join([m.group(0)[i:i+n] for i in range(0, len(m.group(0)), n)]), text) class AI(Window): def __init__(self, **kwargs): @@ -22,14 +27,14 @@ def __init__(self, **kwargs): size=(400, 600), layer="top", anchor="top left bottom", - margin="0px 0px 0px 0px", - keyboard_mode="exclusive", - exclusivity="normal", + keyboard_mode="on-demand", # Changed from 'none' to 'on-demand' + exclusivity="none", visible=False, all_visible=False, **kwargs, ) self.set_size_request(400, 500) + # self.steal_input() # Removed to allow normal input # Create revealer for slide animation (recommended for WaylandWindow) self.revealer = Revealer( @@ -44,6 +49,8 @@ def __init__(self, **kwargs): spacing=16, style="border: 4px solid #000; border-radius: 16px; margin: 0px 16px 16px 0px; padding: 24px; min-width: 320px; min-height: 480px; background: #000000;", ) + self.main_box.set_hexpand(True) + self.main_box.set_halign(Gtk.Align.FILL) # Title label (handwritten style, large) self.title_label = Label( @@ -64,9 +71,9 @@ def __init__(self, **kwargs): self.chat_scroll = ScrolledWindow( name="ai-chat-scroll", vexpand=True, - hexpand=True, min_content_height=200, ) + self.chat_scroll.set_size_request(384, -1) # Chat container for messages self.chat_container = Box( @@ -78,8 +85,11 @@ def __init__(self, **kwargs): margin_top=8, margin_bottom=8, ) - - self.chat_scroll.add(self.chat_container) + # Wrap in Gtk.Alignment to constrain width + self.chat_alignment = Gtk.Alignment.new(0.5, 0, 0, 0) + self.chat_alignment.set_size_request(384, -1) + self.chat_alignment.add(self.chat_container) + self.chat_scroll.add(self.chat_alignment) self.main_box.add(self.chat_scroll) # Spacer to push dropdown to bottom @@ -91,17 +101,20 @@ def __init__(self, **kwargs): orientation="h", spacing=8, h_align="fill", - h_expand=True, v_align="fill", + hexpand=True, style="margin: 8px 0 8px -18px;" # Reduced left margin from 8px to 4px ) + self.input_container.set_hexpand(True) + self.input_container.set_halign(Gtk.Align.FILL) # AI Model selection (gear button only) - now to the left of text field self.model_button = Button( name="ai-model-button", child=Label(name="ai-model-icon", markup="⚙"), # Gear icon tooltip_text="AI Model Settings", - halign="start" + halign=Gtk.Align.START, + hexpand=False ) self.model_button.connect("clicked", self._on_model_button_clicked) @@ -146,52 +159,34 @@ def __init__(self, **kwargs): self.input_container.add(self.model_button) - # Text field (input area) - now multi-line with wrapping - self.text_view = Gtk.TextView() - self.text_view.set_name("ai-text-view") - self.text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - self.text_view.set_hexpand(True) - self.text_view.set_halign(Gtk.Align.FILL) - self.text_view.set_vexpand(False) - self.text_view.set_margin_start(8) - self.text_view.set_margin_end(8) - self.text_view.set_margin_top(8) - self.text_view.set_margin_bottom(8) - - # Enable text wrapping to go down a line - self.text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - - # Set GTK properties for proper expansion - self.text_view.set_hexpand(True) - self.text_view.set_halign(Gtk.Align.FILL) - - # Enable keyboard shortcuts - self.text_view.set_accepts_tab(False) # Disable tab to prevent focus issues - - # Connect key press event to handle Enter key - self.text_view.connect("key-press-event", self._on_text_key_press) - # Also connect to the buffer to catch all text changes - self.text_view.get_buffer().connect("insert-text", self._on_text_insert) - - # Create a scrolled window for the text view with max height + # Text field (input area) - multiline, scrollable, wrapped + self.text_entry = Gtk.TextView() + self.text_entry.set_name("ai-text-entry") + self.text_entry.set_hexpand(True) + self.text_entry.set_halign(Gtk.Align.FILL) + self.text_entry.set_vexpand(True) + self.text_entry.set_margin_top(8) + self.text_entry.set_margin_bottom(8) + self.text_entry.set_sensitive(True) + self.text_entry.set_editable(True) + self.text_entry.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + + # Make text entry scrollable self.text_scroll = Gtk.ScrolledWindow() - self.text_scroll.set_name("ai-text-scroll") + self.text_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.text_scroll.set_hexpand(True) - self.text_scroll.set_halign(Gtk.Align.FILL) + self.text_scroll.set_vexpand(True) + self.text_scroll.set_min_content_height(40) + self.text_scroll.set_min_content_width(100) + self.text_scroll.get_style_context().add_class('ai-text-scroll') self.text_scroll.set_vexpand(False) - self.text_scroll.set_size_request(-1, 120) # Max height of 120px (about 6-8 lines) - self.text_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - self.text_scroll.add(self.text_view) - - # Style the text entry container to match app launcher search field - self.entry_container = Box( - h_align="fill", - h_expand=True, - v_align="fill", - name="ai-entry-container" - ) - self.entry_container.add(self.text_scroll) - self.input_container.add(self.entry_container) + self.text_scroll.add(self.text_entry) + + + # Add scrolled window to input_container for full width + self.input_container.add(self.text_scroll) + # Remove Entry 'activate' signal (not valid for TextView) + # To send on Enter, handle key-press-event on TextView if needed self.main_box.add(self.input_container) @@ -202,14 +197,20 @@ def __init__(self, **kwargs): def show_at_position(self, x, y): self.move(x, y) self.set_visible(True) + self.present() # Bring window to front + self.grab_focus() # Grab window focus self.show_all() # Reveal the content with smooth slide animation self.revealer.set_reveal_child(True) - self.grab_focus() - - # Ensure key events are captured self.add_events(Gdk.EventMask.KEY_PRESS_MASK) + + # Focus the text entry after window is mapped + GLib.idle_add(self.text_entry.grab_focus) + + # Connect key press and key release events to the text entry + self.text_entry.connect("key-press-event", self._on_text_entry_key_press) + self.text_entry.connect("key-release-event", self._on_text_entry_key_release) def hide_ai_panel(self): print("hide_ai_panel() called") @@ -252,93 +253,17 @@ def show_ai_panel(self): """Show the AI panel with revealer animation""" self.show_at_position(0, 0) - def _on_text_key_press(self, widget, event): - """Handle key press events in the text entry""" - # Check for Shift+Enter to insert new line - if event.keyval == Gdk.KEY_Return and event.state & Gdk.ModifierType.SHIFT_MASK: - # Set flag to allow newline - self._shift_pressed = True - # Insert a newline at cursor position - buffer = self.text_view.get_buffer() - buffer.insert_at_cursor("\n") - # Clear the flag after a short delay - GLib.timeout_add(100, self._clear_shift_flag) - return True - - # Regular Enter to send message - elif event.keyval == Gdk.KEY_Return: - # Prevent default behavior and send message - self._send_current_message() - return True - - # Handle other keyboard shortcuts - elif event.state & Gdk.ModifierType.CONTROL_MASK: - if event.keyval == Gdk.KEY_a: # Ctrl+A - Select All - self.text_view.emit("select-all") - return True - elif event.keyval == Gdk.KEY_c: # Ctrl+C - Copy - self.text_view.emit("copy-clipboard") - return True - elif event.keyval == Gdk.KEY_v: # Ctrl+V - Paste - self.text_view.emit("paste-clipboard") - return True - elif event.keyval == Gdk.KEY_x: # Ctrl+X - Cut - self.text_view.emit("cut-clipboard") - return True - elif event.keyval == Gdk.KEY_z: # Ctrl+Z - Undo - self.text_view.emit("undo") - return True - elif event.keyval == Gdk.KEY_y: # Ctrl+Y - Redo - self.text_view.emit("redo") - return True - - return False - - def _clear_shift_flag(self): - """Clear the shift pressed flag""" - if hasattr(self, '_shift_pressed'): - delattr(self, '_shift_pressed') - return False - - def _on_text_insert(self, buffer, location, text, length): - """Handle text insertion to prevent unwanted newlines""" - # If this is a newline and we're not in Shift+Enter mode, prevent it - if text == '\n' and not hasattr(self, '_shift_pressed'): - # Remove the newline - buffer.delete(location, buffer.get_iter_at_offset(location.get_offset() + 1)) - # Trigger message sending - self._send_current_message() - return True - return False - - def _send_current_message(self): - """Send the current message to the AI""" - # Prevent multiple sends - if hasattr(self, '_sending_message') and self._sending_message: - return - - self._sending_message = True - - # Save the text to variable - self.current_message = self.text_view.get_buffer().get_text( - self.text_view.get_buffer().get_start_iter(), - self.text_view.get_buffer().get_end_iter(), - include_hidden_chars=False - ) + def _on_entry_activate(self, entry): + print("[DEBUG] Entry activate signal fired") + """Send the current message to the AI when Enter is pressed in Entry""" + self.current_message = entry.get_text() print(f"Message saved: {self.current_message}") - - # Add message to chat only if there's content if self.current_message.strip(): self.add_user_message(self.current_message) - - # Clear the text field - self.text_view.get_buffer().set_text("") - - # Here you can add logic to send the message to the selected AI model + entry.set_text("") print(f"Sending message to {self.selected_model}: {self.current_message}") - - # Reset sending flag after a short delay - GLib.timeout_add(100, self._reset_sending_flag) + + # _send_current_message is no longer needed with Gtk.Entry; logic is handled in _on_entry_activate def _reset_sending_flag(self): """Reset the sending message flag""" @@ -347,42 +272,45 @@ def _reset_sending_flag(self): def add_user_message(self, message): """Add a user message to the chat area (right side)""" + message = break_long_words(message) # Create message container - message_container = Gtk.Box() - message_container.set_orientation(Gtk.Orientation.HORIZONTAL) - message_container.set_halign(Gtk.Align.END) # Right align for user messages - message_container.set_margin_top(4) - message_container.set_margin_bottom(4) + message_container = Box( + orientation="h", + h_align="end", + margin_top=8, + margin_bottom=8, + margin_start=48, # Large left margin + margin_end=8, # Small right margin + ) # Create message bubble - message_bubble = Gtk.Box() - message_bubble.set_name("user-message-bubble") - message_bubble.get_style_context().add_class("user-message-bubble") - message_bubble.set_size_request(300, -1) # Max width: 300px, height: expand - message_bubble.set_vexpand(False) + message_bubble = Box( + name="user-message-bubble", + orientation="h", + vexpand=False, + margin_top=2, + margin_bottom=2, + margin_start=0, + margin_end=0, + style="background: #222; border-radius: 16px; padding: 10px;" + ) message_bubble.set_hexpand(False) - - # Create message text view for better text wrapping - message_text = Gtk.TextView() - message_text.set_name("user-message-text") - message_text.get_style_context().add_class("user-message-text") - message_text.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - message_text.set_editable(False) - message_text.set_cursor_visible(False) - message_text.set_size_request(220, 60) # Fixed width and height - message_text.set_hexpand(False) - message_text.set_halign(Gtk.Align.START) - message_text.set_vexpand(False) - message_text.set_margin_start(6) - message_text.set_margin_end(6) - message_text.set_margin_top(3) - message_text.set_margin_bottom(3) - - # Set the text content - text_buffer = message_text.get_buffer() - text_buffer.set_text(message) - - message_bubble.add(message_text) + message_bubble.set_halign(Gtk.Align.END) + + # Create message label for text + message_label = Label( + label=message, + wrap=True, + xalign=1.0, + selectable=True, + style="color: #fff; font-size: 1em; padding: 2px;" + ) + message_label.set_hexpand(True) + message_label.set_halign(Gtk.Align.FILL) + message_label.set_line_wrap(True) + message_label.set_max_width_chars(40) + message_label.set_ellipsize(Pango.EllipsizeMode.NONE) + message_bubble.add(message_label) message_container.add(message_bubble) # Add to chat container @@ -398,57 +326,51 @@ def add_user_message(self, message): # Get AI response self.get_ai_response(message) + def add_ai_message(self, message): """Add an AI message to the chat area (left side)""" + message = break_long_words(message) # Create message container - message_container = Gtk.Box() - message_container.set_orientation(Gtk.Orientation.HORIZONTAL) - message_container.set_halign(Gtk.Align.START) # Left align for AI messages - message_container.set_margin_top(4) - message_container.set_margin_bottom(4) - + message_container = Box( + orientation="h", + h_align="start", + margin_top=8, + margin_bottom=8, + margin_start=8, # Small left margin + margin_end=48, # Large right margin + ) # Create message bubble - message_bubble = Gtk.Box() - message_bubble.set_name("ai-message-bubble") - message_bubble.get_style_context().add_class("ai-message-bubble") - message_bubble.set_size_request(300, -1) # Max width: 300px, height: expand - message_bubble.set_vexpand(False) + message_bubble = Box( + name="ai-message-bubble", + orientation="h", + vexpand=False, + margin_top=2, + margin_bottom=2, + margin_start=0, + margin_end=0, + style="background: #444; border-radius: 16px; padding: 10px;" + ) message_bubble.set_hexpand(False) - - # Create message text view for better text wrapping - message_text = Gtk.TextView() - message_text.set_name("ai-message-text") - message_text.get_style_context().add_class("ai-message-text") - message_text.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - message_text.set_editable(False) - message_text.set_cursor_visible(False) - message_text.set_size_request(220, 60) # Fixed width and height - message_text.set_hexpand(False) - message_text.set_halign(Gtk.Align.START) - message_text.set_vexpand(False) - message_text.set_margin_start(6) - message_text.set_margin_end(6) - message_text.set_margin_top(3) - message_text.set_margin_bottom(3) - - # Set the text content - text_buffer = message_text.get_buffer() - text_buffer.set_text(message) - - message_bubble.add(message_text) + message_bubble.set_halign(Gtk.Align.START) + # Create message label for text + message_label = Label( + label=message, + wrap=True, + xalign=0.0, + selectable=True, + style="color: #fff; font-size: 1em; padding: 2px;" + ) + message_label.set_hexpand(True) + message_label.set_halign(Gtk.Align.FILL) + message_label.set_line_wrap(True) + message_label.set_max_width_chars(40) + message_label.set_ellipsize(Pango.EllipsizeMode.NONE) + message_bubble.add(message_label) message_container.add(message_bubble) - - # Add to chat container self.chat_container.add(message_container) - - # Scroll to bottom - self.chat_scroll.get_vadjustment().set_value( - self.chat_scroll.get_vadjustment().get_upper() - ) - - # Show the new message self.chat_container.show_all() + def get_ai_response(self, user_message): """Get response from the selected AI model""" @@ -487,37 +409,41 @@ def show_typing_indicator(self): print("Showing typing indicator") # Create typing indicator container - typing_container = Gtk.Box() - typing_container.set_orientation(Gtk.Orientation.HORIZONTAL) - typing_container.set_halign(Gtk.Align.START) # Left align for AI messages - typing_container.set_margin_top(4) - typing_container.set_margin_bottom(4) + typing_container = Box( + orientation="h", + h_align="start", + margin_top=4, + margin_bottom=4, + ) # Create typing bubble - typing_bubble = Gtk.Box() - typing_bubble.set_name("typing-bubble") + typing_bubble = Box( + name="typing-bubble", + v_expand=False, + h_expand=False, + ) typing_bubble.get_style_context().add_class("typing-bubble") typing_bubble.set_size_request(80, 40) # Small size for typing indicator - typing_bubble.set_vexpand(False) - typing_bubble.set_hexpand(False) # Create dots container - dots_container = Gtk.Box() - dots_container.set_orientation(Gtk.Orientation.HORIZONTAL) - dots_container.set_spacing(4) - dots_container.set_margin_start(12) - dots_container.set_margin_end(12) - dots_container.set_margin_top(8) - dots_container.set_margin_bottom(8) + dots_container = Box( + orientation="h", + spacing=4, + margin_start=12, + margin_end=12, + margin_top=8, + margin_bottom=8, + ) # Create three animated dots self.typing_dots = [] for i in range(3): - dot = Gtk.Label() - dot.set_name(f"typing-dot-{i}") + dot = Label( + name=f"typing-dot-{i}", + text="●", + ) dot.get_style_context().add_class("typing-dot") dot.get_style_context().add_class("typing-dot-inactive") - dot.set_text("●") dots_container.add(dot) self.typing_dots.append(dot) print(f"Created dot {i}") @@ -594,7 +520,18 @@ def do_key_press_event(self, event): print("Escape key pressed (do_key_press_event) - closing AI panel") self.hide_ai_panel() return True - return super().do_key_press_event(event) + return Gtk.Window.do_key_press_event(self, event) + + def _on_text_entry_key_press(self, widget, event): + if event.keyval == Gdk.KEY_Return and not (event.state & Gdk.ModifierType.SHIFT_MASK): + self._send_current_message() + return True + return False + + def _on_text_entry_key_release(self, widget, event): + """Handle key release events on the text entry.""" + print(f"Text entry key release: {Gdk.keyval_name(event.keyval)} ({event.keyval})") + return False # Return False to allow other handlers to process the event def _update_model_button_styles(self): """Update the styles of all model buttons to reflect the selected model.""" diff --git a/modules/notch.py b/modules/notch.py index 4ae76cc6..b665939f 100644 --- a/modules/notch.py +++ b/modules/notch.py @@ -953,6 +953,12 @@ def _ensure_no_text_selection(self): def on_key_press(self, widget, event): """Handle key presses at the notch level""" + print(f"Notch on_key_press received key: {Gdk.keyval_name(event.keyval)} ({event.keyval})") + + # If the AI panel is visible, allow key events to propagate to it + if self.ai.get_visible(): + print("AI panel is visible, allowing key event to propagate.") + return False # Allow propagation if self._launcher_transitioning: keyval = event.keyval @@ -978,6 +984,7 @@ def on_key_press(self, widget, event): and self.dashboard.stack.get_visible_child() == self.dashboard.widgets ): if self.stack.get_visible_child() == self.launcher: + print("Launcher is visible, not opening again.") return False keyval = event.keyval @@ -992,9 +999,9 @@ def on_key_press(self, widget, event): ) if is_valid_char and keychar: - print(f"Notch received keypress: {keychar}") - + print(f"Notch received keypress: {keychar} and is opening launcher.") self.open_launcher_with_text(keychar) return True + print(f"Notch on_key_press returning False. Current visible child: {self.stack.get_visible_child().get_name()}") return False diff --git a/styles/ai.css b/styles/ai.css index e9c046c1..eadc913e 100644 --- a/styles/ai.css +++ b/styles/ai.css @@ -47,15 +47,22 @@ } /* AI Text Entry Styling */ -#ai-text-view { - background-color: transparent; +#ai-text-entry { + background-color: var(--surface-bright); color: var(--foreground); font-size: 1em; - border: none; - outline: none; + min-width: 20px; + min-height: 40px; + border-radius: 8px; + padding: 8px; +} + +.ai-text-scroll { + min-height: 40px; + padding: 0; } -#ai-text-view text { +#ai-text-entry text { background-color: transparent; color: var(--foreground); font-size: 1em; From cee3dd444ab4f1369061d7cf2cffc6b5299bb8b3 Mon Sep 17 00:00:00 2001 From: KapaDev Date: Sun, 27 Jul 2025 22:11:05 +0300 Subject: [PATCH 4/5] kinda fixed chat styling --- modules/ai.py | 51 ++++++++++++++++++++++++++++++++------------------- styles/ai.css | 13 +++++++++++-- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/modules/ai.py b/modules/ai.py index 4e52f0e5..358acae2 100644 --- a/modules/ai.py +++ b/modules/ai.py @@ -80,14 +80,18 @@ def __init__(self, **kwargs): name="ai-chat-container", orientation="v", spacing=8, - margin_start=8, + margin_start=0, # Set to 0 for flush left margin_end=8, margin_top=8, margin_bottom=8, ) + self.chat_container.set_hexpand(True) + self.chat_container.set_halign(Gtk.Align.FILL) # Wrap in Gtk.Alignment to constrain width - self.chat_alignment = Gtk.Alignment.new(0.5, 0, 0, 0) - self.chat_alignment.set_size_request(384, -1) + self.chat_alignment = Gtk.Alignment.new(0.0, 0, 0, 0) + self.chat_alignment.set_hexpand(True) + self.chat_alignment.set_halign(Gtk.Align.START) + #self.chat_alignment.set_size_request(384, -1) self.chat_alignment.add(self.chat_container) self.chat_scroll.add(self.chat_alignment) self.main_box.add(self.chat_scroll) @@ -116,6 +120,7 @@ def __init__(self, **kwargs): halign=Gtk.Align.START, hexpand=False ) + self.model_button.set_size_request(-1, 40) self.model_button.connect("clicked", self._on_model_button_clicked) # Create a popover for the model options @@ -165,8 +170,8 @@ def __init__(self, **kwargs): self.text_entry.set_hexpand(True) self.text_entry.set_halign(Gtk.Align.FILL) self.text_entry.set_vexpand(True) - self.text_entry.set_margin_top(8) - self.text_entry.set_margin_bottom(8) + self.text_entry.set_margin_top(0) + self.text_entry.set_margin_bottom(0) self.text_entry.set_sensitive(True) self.text_entry.set_editable(True) self.text_entry.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) @@ -263,7 +268,14 @@ def _on_entry_activate(self, entry): entry.set_text("") print(f"Sending message to {self.selected_model}: {self.current_message}") - # _send_current_message is no longer needed with Gtk.Entry; logic is handled in _on_entry_activate + def _send_current_message(self): + buffer = self.text_entry.get_buffer() + start_iter = buffer.get_start_iter() + end_iter = buffer.get_end_iter() + message = buffer.get_text(start_iter, end_iter, True).strip() + if message: + self.add_user_message(message) + buffer.set_text("") # Clear the text field def _reset_sending_flag(self): """Reset the sending message flag""" @@ -279,9 +291,11 @@ def add_user_message(self, message): h_align="end", margin_top=8, margin_bottom=8, - margin_start=48, # Large left margin - margin_end=8, # Small right margin + margin_start=8, + margin_end=8, ) + message_container.set_hexpand(True) + message_container.set_halign(Gtk.Align.END) # <--- THIS IS CRITICAL # Create message bubble message_bubble = Box( @@ -291,22 +305,21 @@ def add_user_message(self, message): margin_top=2, margin_bottom=2, margin_start=0, - margin_end=0, - style="background: #222; border-radius: 16px; padding: 10px;" + margin_end=0 ) - message_bubble.set_hexpand(False) - message_bubble.set_halign(Gtk.Align.END) - + message_bubble.set_hexpand(True) + message_bubble.set_halign(Gtk.Align.FILL) # Create message label for text message_label = Label( label=message, wrap=True, - xalign=1.0, + xalign=0.0, selectable=True, style="color: #fff; font-size: 1em; padding: 2px;" ) + message_label.set_xalign(0.0) message_label.set_hexpand(True) - message_label.set_halign(Gtk.Align.FILL) + message_label.set_halign(Gtk.Align.END) message_label.set_line_wrap(True) message_label.set_max_width_chars(40) message_label.set_ellipsize(Pango.EllipsizeMode.NONE) @@ -337,8 +350,8 @@ def add_ai_message(self, message): h_align="start", margin_top=8, margin_bottom=8, - margin_start=8, # Small left margin - margin_end=48, # Large right margin + margin_start=8, # Same margin as user + margin_end=8, # Same margin as user ) # Create message bubble message_bubble = Box( @@ -347,7 +360,7 @@ def add_ai_message(self, message): vexpand=False, margin_top=2, margin_bottom=2, - margin_start=0, + margin_start=0, # No extra margin on bubble margin_end=0, style="background: #444; border-radius: 16px; padding: 10px;" ) @@ -362,7 +375,7 @@ def add_ai_message(self, message): style="color: #fff; font-size: 1em; padding: 2px;" ) message_label.set_hexpand(True) - message_label.set_halign(Gtk.Align.FILL) + message_label.set_halign(Gtk.Align.END) message_label.set_line_wrap(True) message_label.set_max_width_chars(40) message_label.set_ellipsize(Pango.EllipsizeMode.NONE) diff --git a/styles/ai.css b/styles/ai.css index eadc913e..20bbebb0 100644 --- a/styles/ai.css +++ b/styles/ai.css @@ -111,11 +111,20 @@ font-weight: bold; } - /* AI Message Styling */ + .user-message-bubble { + background-color: var(--cyan); + border-radius: 12px; + padding: 8px 12px 8px 12px; + margin-left: 40px; + margin-right: 40px; +} + +/* AI Message Styling */ .ai-message-bubble { background-color: var(--surface-bright); border-radius: 12px; - padding: 8px 12px; + padding: 8px 12px 8px 12px; + margin-left: 40px; margin-right: 40px; } From 812382c43db8245daa561724cae882c9aea03169 Mon Sep 17 00:00:00 2001 From: KapaDev Date: Sun, 27 Jul 2025 22:21:19 +0300 Subject: [PATCH 5/5] fixed merge errors --- modules/notch.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/modules/notch.py b/modules/notch.py index ba076d7e..9e8f67e8 100644 --- a/modules/notch.py +++ b/modules/notch.py @@ -171,7 +171,6 @@ def __init__(self, **kwargs): self.window_icon = Image( name="notch-window-icon", icon_name="application-x-executable", icon_size=20 - name="notch-window-icon", icon_name="application-x-executable", icon_size=20 ) self.active_window = ActiveWindow( @@ -253,7 +252,6 @@ def __init__(self, **kwargs): self.active_window_box, self.player_small, ], - ], ) self.compact_stack.set_visible_child(self.active_window_box) @@ -264,9 +262,6 @@ def __init__(self, **kwargs): Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK - Gdk.EventMask.SCROLL_MASK - | Gdk.EventMask.BUTTON_PRESS_MASK - | Gdk.EventMask.SMOOTH_SCROLL_MASK ) self.compact.connect("scroll-event", self._on_compact_scroll) self.compact.connect( @@ -306,7 +301,6 @@ def __init__(self, **kwargs): self.tmux, self.cliphist, ], - ], ) if data.PANEL_THEME == "Panel": @@ -315,9 +309,6 @@ def __init__(self, **kwargs): self.stack.add_style_class(data.BAR_POSITION.lower()) self.stack.add_style_class(data.PANEL_POSITION.lower()) - if is_panel_vertical or ( - data.PANEL_POSITION in ["Start", "End"] and data.PANEL_THEME == "Panel" - ): if is_panel_vertical or ( data.PANEL_POSITION in ["Start", "End"] and data.PANEL_THEME == "Panel" ): @@ -345,7 +336,6 @@ def __init__(self, **kwargs): MyCorner("top-right"), Box(), ], - ], ) self.corner_right = Box( @@ -356,7 +346,6 @@ def __init__(self, **kwargs): MyCorner("top-left"), Box(), ], - ], ) self.notch_box = CenterBox(