diff --git a/src/browser/__pycache__/chrome.cpython-313.pyc b/src/browser/__pycache__/chrome.cpython-313.pyc index 0db28ed..b3af27a 100644 Binary files a/src/browser/__pycache__/chrome.cpython-313.pyc and b/src/browser/__pycache__/chrome.cpython-313.pyc differ diff --git a/src/browser/__pycache__/tab.cpython-313.pyc b/src/browser/__pycache__/tab.cpython-313.pyc index 5469c9c..b67ea87 100644 Binary files a/src/browser/__pycache__/tab.cpython-313.pyc and b/src/browser/__pycache__/tab.cpython-313.pyc differ diff --git a/src/browser/browser.py b/src/browser/browser.py index 2727f3a..8653642 100644 --- a/src/browser/browser.py +++ b/src/browser/browser.py @@ -33,6 +33,8 @@ class Browser: def new_tab(self, url: str): tab = Tab(self) + # Normalize URL to ensure https:// protocol + url = self._normalize_url(url) tab.load(URL(url)) self.tabs.append(tab) self.active_tab = tab @@ -76,6 +78,8 @@ class Browser: def navigate_to(self, url_str: str): if not url_str: return + # Add https:// if no protocol provided + url_str = self._normalize_url(url_str) if not self.active_tab: self.new_tab(url_str) return @@ -84,6 +88,18 @@ class Browser: self.chrome.update_address_bar() self._log(f"Navigate to: {url_str}", logging.INFO) + def _normalize_url(self, url_str: str) -> str: + """Add https:// protocol if not present.""" + url_str = url_str.strip() + # If URL already has a protocol, return as-is + if "://" in url_str: + return url_str + # Special about: URLs + if url_str.startswith("about:"): + return url_str + # Otherwise, assume https:// + return f"https://{url_str}" + def go_back(self): if self.active_tab and self.active_tab.go_back(): self.chrome.paint() diff --git a/src/browser/chrome.py b/src/browser/chrome.py index 25fb511..6074818 100644 --- a/src/browser/chrome.py +++ b/src/browser/chrome.py @@ -133,40 +133,13 @@ class Chrome: # Clear existing tabs self._clear_children(self.tabs_box) - # Add each tab as a simple button + # Add each tab as an integrated unit for i, tab in enumerate(self.browser.tabs): is_active = tab is self.browser.active_tab - # Simple container for tab label + close button - tab_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) - tab_box.set_homogeneous(False) - - # Tab label button - tab_label = f"{i+1}: {tab.title}" - tab_btn = Gtk.Button(label=tab_label) - tab_btn.set_hexpand(True) - tab_btn.set_relief(Gtk.ReliefStyle.NORMAL) - - if is_active: - tab_btn.add_css_class("suggested-action") - else: - tab_btn.add_css_class("flat") - - # Store tab reference on the button for handler - tab_btn.tab = tab - tab_btn.connect("clicked", self._on_tab_clicked) - tab_box.append(tab_btn) - - # Close button - close_btn = Gtk.Button(label="✕") - close_btn.set_size_request(36, -1) - close_btn.set_relief(Gtk.ReliefStyle.FLAT) - close_btn.add_css_class("flat") - close_btn.tab = tab - close_btn.connect("clicked", self._on_close_clicked) - tab_box.append(close_btn) - - self.tabs_box.append(tab_box) + # Create integrated tab widget + tab_widget = self._create_integrated_tab(tab, i, is_active) + self.tabs_box.append(tab_widget) # New tab button new_tab_btn = Gtk.Button(label="+") @@ -176,6 +149,80 @@ class Chrome: new_tab_btn.connect("clicked", self._on_new_tab_clicked) self.tabs_box.append(new_tab_btn) + def _create_integrated_tab(self, tab, index: int, is_active: bool) -> Gtk.Widget: + """Create an integrated tab widget with close button as one unit.""" + # Outer container - this is the visual tab + tab_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + tab_container.add_css_class("integrated-tab") + if is_active: + tab_container.add_css_class("active-tab") + tab_container.set_homogeneous(False) + + # Left side: tab label (expandable) + tab_label = f"{index+1}: {tab.title}" + tab_btn = Gtk.Button(label=tab_label) + tab_btn.set_hexpand(True) + tab_btn.add_css_class("tab-label") + tab_btn.add_css_class("flat") + tab_btn.tab = tab + tab_btn.connect("clicked", self._on_tab_clicked) + tab_container.append(tab_btn) + + # Right side: close button (fixed width, no expand) + close_btn = Gtk.Button(label="✕") + close_btn.set_size_request(34, -1) + close_btn.add_css_class("tab-close") + close_btn.add_css_class("flat") + close_btn.tab = tab + close_btn.connect("clicked", self._on_close_clicked) + tab_container.append(close_btn) + + # Apply CSS styling to make it look like one unit + css = Gtk.CssProvider() + css.load_from_data(b""" + .integrated-tab { + background-color: @theme_bg_color; + border: 1px solid @borders; + border-radius: 4px 4px 0 0; + margin-right: 2px; + padding: 0px; + } + + .integrated-tab.active-tab { + background-color: @theme_base_color; + } + + .tab-label { + padding: 6px 8px; + font-weight: 500; + border: none; + border-radius: 0; + } + + .tab-label:hover { + background-color: mix(@theme_bg_color, @theme_fg_color, 0.95); + } + + .tab-close { + padding: 2px 4px; + font-size: 0.9em; + border: none; + border-left: 1px solid @borders; + border-radius: 0; + min-width: 32px; + } + + .tab-close:hover { + background-color: @error_color; + color: white; + } + """) + + context = tab_container.get_style_context() + context.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + return tab_container + def _on_tab_clicked(self, btn: Gtk.Button): """Handle tab button click - set as active.""" if hasattr(btn, 'tab'): diff --git a/src/browser/tab.py b/src/browser/tab.py index 481c47e..36899e1 100644 --- a/src/browser/tab.py +++ b/src/browser/tab.py @@ -21,7 +21,8 @@ class Frame: logger = logging.getLogger("bowser.frame") # Handle special about: URLs - if url.origin == "about:startpage": + url_str = str(url) + if url_str.startswith("about:startpage"): html = render_startpage() self.document = parse_html(html) self.tab.current_url = url diff --git a/tests/test_url_normalization.py b/tests/test_url_normalization.py new file mode 100644 index 0000000..1fa7c08 --- /dev/null +++ b/tests/test_url_normalization.py @@ -0,0 +1,70 @@ +"""Tests for URL normalization.""" + +import pytest +from src.browser.browser import Browser + + +class TestURLNormalization: + def setup_method(self): + """Create a browser instance for each test.""" + self.browser = Browser() + + def test_normalize_url_with_https(self): + """Test that URLs with https:// protocol are unchanged.""" + url = "https://example.com" + normalized = self.browser._normalize_url(url) + assert normalized == "https://example.com" + + def test_normalize_url_with_http(self): + """Test that URLs with http:// protocol are unchanged.""" + url = "http://example.com" + normalized = self.browser._normalize_url(url) + assert normalized == "http://example.com" + + def test_normalize_url_without_protocol(self): + """Test that URLs without protocol get https:// added.""" + url = "example.com" + normalized = self.browser._normalize_url(url) + assert normalized == "https://example.com" + + def test_normalize_url_with_path(self): + """Test that URLs with path but no protocol get https:// added.""" + url = "example.com/path/to/page" + normalized = self.browser._normalize_url(url) + assert normalized == "https://example.com/path/to/page" + + def test_normalize_url_with_about(self): + """Test that about: URLs are not modified.""" + url = "about:startpage" + normalized = self.browser._normalize_url(url) + assert normalized == "about:startpage" + + def test_normalize_url_strips_whitespace(self): + """Test that leading/trailing whitespace is stripped.""" + url = " example.com " + normalized = self.browser._normalize_url(url) + assert normalized == "https://example.com" + + def test_normalize_url_with_query_string(self): + """Test that URLs with query strings work correctly.""" + url = "example.com/search?q=test" + normalized = self.browser._normalize_url(url) + assert normalized == "https://example.com/search?q=test" + + def test_normalize_url_with_subdomain(self): + """Test that subdomains work correctly.""" + url = "www.example.com" + normalized = self.browser._normalize_url(url) + assert normalized == "https://www.example.com" + + def test_normalize_url_with_port(self): + """Test that ports are preserved.""" + url = "example.com:8080" + normalized = self.browser._normalize_url(url) + assert normalized == "https://example.com:8080" + + def test_normalize_url_localhost(self): + """Test that localhost URLs work correctly.""" + url = "localhost:3000" + normalized = self.browser._normalize_url(url) + assert normalized == "https://localhost:3000"