diff --git a/lazy_player/__init__.py b/lazy_player/__init__.py
index 609685f..1dc9c99 100644
--- a/lazy_player/__init__.py
+++ b/lazy_player/__init__.py
@@ -2,540 +2,20 @@ from __future__ import annotations
 
 import os
 import sys
-from datetime import datetime
-from pathlib import Path
-from typing import Any, cast
 
 import gi
 
-from .file_model import FileItem, FileListModel, FileType
-from .thumbnailer import Thumbnailer
-from .video_player import VideoPlayer
-
 gi.require_version("Gdk", "4.0")
-gi.require_version("Gtk", "4.0")
+gi.require_version("GLib", "2.0")
+gi.require_version("GObject", "2.0")
 gi.require_version("Gst", "1.0")
+gi.require_version("Gtk", "4.0")
 gi.require_version("Pango", "1.0")
-from gi.repository import Gdk, GLib, Gst, Gtk, Pango  # NOQA: E402
 
+from gi.repository import Gst  # NOQA: E402
 
-class MainWindow(Gtk.ApplicationWindow):
-    stack: Gtk.Stack
-    list_view: Gtk.ListView
-    list_model: FileListModel
-    selection_model: Gtk.SingleSelection
-    video_widget: Gtk.Picture
-    tick_callback_id: int
-    overlay_label: Gtk.Label
-    overlay_hide_time: float
-    last_position_save: float
-
-    directory_history: list[Path]
-    selection_history: dict[str, int]
-
-    def __init__(self, *args: Any, thumbnailer: Thumbnailer, **kwargs: Any):
-        super().__init__(*args, **kwargs)
-        self.thumbnailer = thumbnailer
-
-        # Directory history stack
-        self.directory_history = []
-        self.selection_history = {}
-
-        # For overlay text timeout
-        self.overlay_hide_time = 0.0
-        self.last_position_save = 0.0
-        self.overlay_label = Gtk.Label()
-        self.overlay_label.set_name("overlay-text")
-        self.overlay_label.set_valign(Gtk.Align.CENTER)
-        self.overlay_label.set_halign(Gtk.Align.CENTER)
-        self.overlay_label.set_visible(False)
-        self.overlay_label.set_wrap(True)
-        self.overlay_label.set_wrap_mode(Pango.WrapMode.WORD_CHAR)
-
-        self.tick_callback_id = self.add_tick_callback(self._on_tick, None)
-
-        # Make window fullscreen and borderless
-        self.set_decorated(False)
-        self.fullscreen()
-
-        # Setup key event controller
-        key_controller = Gtk.EventControllerKey()
-        key_controller.connect("key-pressed", self._on_key_pressed)
-        self.add_controller(key_controller)
-
-        # Disable and hide mouse cursor
-        self.set_can_target(False)
-        display = Gdk.Display.get_default()
-        if display:
-            cursor = Gdk.Cursor.new_from_name("none")
-            if cursor:
-                self.set_cursor(cursor)
-
-        # Main horizontal box to split the screen
-        main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
-
-        # Create stack to hold our widgets
-        self.stack = Gtk.Stack()
-
-        # Create video widget and overlay
-        self.video_widget = Gtk.Picture()
-        self.video_widget.set_can_shrink(True)
-        self.video_widget.set_keep_aspect_ratio(True)
-        self.video_widget.set_vexpand(True)
-        self.video_widget.set_hexpand(True)
-
-        video_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
-        video_box.set_name("black-overlay")
-        video_box.set_vexpand(True)
-        video_box.set_hexpand(True)
-
-        # Create an overlay container
-        overlay = Gtk.Overlay()
-        overlay.set_child(self.video_widget)
-        overlay.add_overlay(self.overlay_label)
-
-        video_box.append(overlay)
-
-        # Setup video player
-        self.video_player = VideoPlayer(self.video_widget)
-
-        # Add both main menu and overlay to stack
-        self.stack.add_named(main_box, "menu")
-        self.stack.add_named(video_box, "player")
-        self.stack.set_visible_child_name("menu")
-
-        # Create a grid to handle the 1:2 ratio
-        grid = Gtk.Grid()
-        grid.set_column_homogeneous(True)
-        grid.set_hexpand(True)
-
-        # Left third (1/3 width)
-        left_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
-        left_box.set_valign(Gtk.Align.START)
-        left_box.set_halign(Gtk.Align.FILL)
-
-        # Create digital clock
-        self.clock_label = Gtk.Label()
-        self.clock_label.set_name("digital-clock")
-        self.clock_label.set_halign(Gtk.Align.CENTER)
-        self.clock_label.set_valign(Gtk.Align.CENTER)
-        self.clock_label.set_text(datetime.now().strftime("%H:%M"))
-        left_box.append(self.clock_label)
-
-        # Create image widget for thumbnail
-        self.thumbnail_image = Gtk.Picture()
-        self.thumbnail_image.set_name("thumbnail-image")
-        self.thumbnail_image.set_size_request(384, 216)  # 16:9 aspect ratio
-        self.thumbnail_image.set_can_shrink(True)
-        self.thumbnail_image.set_keep_aspect_ratio(True)
-        self.thumbnail_image.set_resource(None)
-        left_box.append(self.thumbnail_image)
-
-        # Right two-thirds (2/3 width)
-        right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
-        right_box.set_hexpand(True)
-
-        # Attach boxes to grid with specific column spans
-        grid.attach(left_box, 0, 0, 1, 1)
-        grid.attach(right_box, 1, 0, 2, 1)
-
-        main_box.append(grid)
-
-        # Create list model and view
-        self.list_model = FileListModel()
-        self.list_view = Gtk.ListView()
-        self.selection_model = Gtk.SingleSelection.new(self.list_model)
-        self.selection_model.connect("selection-changed", self._on_selection_changed)
-        self.list_view.set_model(self.selection_model)
-        self.list_view.set_vexpand(True)
-
-        def on_activate(widget: Gtk.ListView, index: int):
-            selected_item = self.selection_model.get_item(index)
-            if selected_item:
-                file_item = cast(FileItem, selected_item)
-
-                if file_item.file_type == FileType.DIRECTORY:
-                    self._navigate_to(file_item.full_path)
-                    return
-
-                position = file_item.saved_position
-                duration = file_item.saved_duration
-
-                if (position / duration) >= 0.99:
-                    position = 0
-
-                # Start playing the video
-                self.video_player.play(
-                    file_item.full_path,
-                    position,
-                    file_item.saved_subtitle_track,
-                    file_item.saved_audio_track,
-                )
-                self.last_position_save = self.now
-
-                self.stack.set_visible_child_name("player")
-                self.show_overlay_text(f"Playing: {file_item.name}")
-
-        self.list_view.connect("activate", on_activate)
-
-        # Factory for list items
-        factory = Gtk.SignalListItemFactory()
-        factory.connect("setup", self._setup_list_item)
-        factory.connect("bind", self._bind_list_item)
-        self.list_view.set_factory(factory)
-
-        # Populate the list store
-        self._populate_file_list()
-
-        # Add list view to a scrolled window
-        scrolled = Gtk.ScrolledWindow()
-        scrolled.set_child(self.list_view)
-        right_box.append(scrolled)
-
-        self.set_child(self.stack)
-
-    @property
-    def now(self) -> float:
-        frame_clock = self.get_frame_clock()
-        if frame_clock is None:
-            return 0
-
-        return frame_clock.get_frame_time() / 1_000_000
-
-    @property
-    def selection(self) -> FileItem | None:
-        selected_item = self.selection_model.get_selected_item()
-        return cast(FileItem, selected_item) if selected_item else None
-
-    def _setup_list_item(self, factory: Gtk.SignalListItemFactory, list_item: Gtk.ListItem):
-        # Create horizontal box to hold icon and label
-        box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
-        box.set_spacing(8)
-
-        # Create icon image
-        icon = Gtk.Image()
-        icon.set_css_classes(["file-icon"])
-        box.append(icon)
-
-        # Create label
-        label = Gtk.Label()
-        label.set_halign(Gtk.Align.START)
-        box.append(label)
-
-        list_item.set_child(box)
-
-    def _bind_list_item(self, factory: Gtk.SignalListItemFactory, list_item: Gtk.ListItem):
-        box = cast(Gtk.Box, list_item.get_child())
-        icon = cast(Gtk.Image, box.get_first_child())
-        label = cast(Gtk.Label, box.get_last_child())
-        item = cast(FileItem, list_item.get_item())
-
-        def update_icon(*args: object) -> None:
-            if item.file_type == FileType.DIRECTORY:
-                icon.set_from_icon_name("folder-symbolic")
-                icon.set_css_classes(["file-icon"])
-            else:
-                position = item.saved_position
-                duration = item.saved_duration
-
-                if position == 0:
-                    icon.set_from_icon_name("media-playback-start-symbolic")
-                    icon.set_css_classes(["file-icon", "unwatched"])
-                elif (position / duration) >= 0.99:
-                    icon.set_from_icon_name("object-select-symbolic")
-                    icon.set_css_classes(["file-icon", "completed"])
-                else:
-                    icon.set_from_icon_name("media-playback-pause-symbolic")
-                    icon.set_css_classes(["file-icon", "in-progress"])
-
-        label.set_text(item.name)
-        item.connect("notify::saved-position", update_icon)
-        item.connect("notify::saved-duration", update_icon)
-        update_icon()
-
-    def _refresh(self):
-        self._populate_file_list()
-
-        pos = self.selection_history.get(str(os.getcwd()), 0)
-        self.selection_model.set_selected(pos)
-        self.list_view.scroll_to(pos, Gtk.ListScrollFlags.SELECT | Gtk.ListScrollFlags.FOCUS, None)
-
-    def _navigate_to(self, path: Path):
-        self.directory_history.append(Path(os.getcwd()))
-        os.chdir(path)
-        self._populate_file_list()
-
-        pos = self.selection_history.get(str(path), 0)
-        self.selection_model.set_selected(pos)
-        self.list_view.scroll_to(pos, Gtk.ListScrollFlags.SELECT | Gtk.ListScrollFlags.FOCUS, None)
-
-    def _navigate_back(self):
-        if not self.directory_history:
-            return
-
-        prev_dir = self.directory_history.pop()
-        os.chdir(prev_dir)
-        self._populate_file_list()
-
-        pos = self.selection_history.get(str(prev_dir), 0)
-        self.selection_model.set_selected(pos)
-        self.list_view.scroll_to(pos, Gtk.ListScrollFlags.SELECT | Gtk.ListScrollFlags.FOCUS, None)
-
-    def _on_selection_changed(
-        self,
-        selection_model: Gtk.SingleSelection,
-        position: int,
-        n_items: int,
-    ):
-        position = selection_model.get_selected()
-
-        if position == Gtk.INVALID_LIST_POSITION:
-            return
-
-        self.selection_history[os.getcwd()] = position
-
-        file_item = self.selection
-        if file_item is not None:
-            # Update thumbnail if available
-            if file_item.thumbnail:
-                gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail))
-                texture = Gdk.Texture.new_from_bytes(gbytes)
-                self.thumbnail_image.set_paintable(texture)
-            else:
-                self.thumbnailer.generate_thumbnail(file_item)
-                self.thumbnail_image.set_paintable(None)
-
-    def _toggle_play_pause(self) -> None:
-        """Toggle between play and pause states"""
-        self.video_player.toggle_play_pause()
-
-    def _on_player_key_pressed(
-        self,
-        keyval: int,
-        keycode: int,
-        state: Gdk.ModifierType,
-    ) -> bool:
-        if keyval == Gdk.keyval_from_name("space"):
-            self._toggle_play_pause()
-            return True
-
-        elif keyval == Gdk.keyval_from_name("Escape"):
-            self._save_position()
-            self.video_player.stop()
-            self.stack.set_visible_child_name("menu")
-            self.list_view.grab_focus()
-            return True
-
-        elif keyval == Gdk.keyval_from_name("Left"):
-            self.video_player.seek_relative(-10)
-            return True
-
-        elif keyval == Gdk.keyval_from_name("Right"):
-            self.video_player.seek_relative(10)
-            return True
-
-        elif keyval == Gdk.keyval_from_name("Down"):
-            self.video_player.seek_relative(-60)
-            return True
-
-        elif keyval == Gdk.keyval_from_name("Up"):
-            self.video_player.seek_relative(60)
-            return True
-
-        elif keyval == Gdk.keyval_from_name("Home"):
-            self.video_player.seek_start()
-            return True
-
-        elif keyval == Gdk.keyval_from_name("End"):
-            self.video_player.seek_end()
-            return True
-
-        elif keyval == Gdk.keyval_from_name("j"):
-            has_subs, index, lang = self.video_player.cycle_subtitles()
-
-            if has_subs:
-                if index:
-                    self.show_overlay_text(f"Subtitles #{index} ({lang})")
-                else:
-                    self.show_overlay_text("Subtitles turned off")
-
-                file_item = self.selection
-                if file_item is not None:
-                    file_item.saved_subtitle_track = index - 1
-            else:
-                self.show_overlay_text("No subtitles available")
-
-            return True
-
-        elif keyval == Gdk.keyval_from_name("a"):
-            has_audio, index, lang = self.video_player.cycle_audio()
-
-            if has_audio:
-                self.show_overlay_text(f"Audio #{index} ({lang})")
-                file_item = self.selection
-                if file_item is not None:
-                    file_item.saved_audio_track = index - 1
-            else:
-                self.show_overlay_text("No audio tracks available")
-
-            return True
-
-        return False
-
-    def _toggle_watched_status(self) -> None:
-        """Toggle watched status for the selected file"""
-
-        file_item = self.selection
-
-        if file_item is None or file_item.file_type == FileType.DIRECTORY:
-            return
-
-        position = file_item.saved_position
-        duration = file_item.saved_duration
-
-        # If position exists and is >= 99% through, clear it
-        if position > 0 and (position / duration) >= 0.99:
-            file_item.saved_position = 0
-        else:
-            # Otherwise mark as complete
-            file_item.saved_position = duration
-
-        # Force the list to update the changed item
-        self.list_model.items_changed(self.selection_model.get_selected(), 1, 1)
-
-    def _on_menu_key_pressed(
-        self,
-        keyval: int,
-        keycode: int,
-        state: Gdk.ModifierType,
-    ) -> bool:
-        if keyval == Gdk.keyval_from_name("q"):
-            self.close()
-            return True
-
-        elif keyval == Gdk.keyval_from_name("r"):
-            self._refresh()
-            return True
-
-        elif keyval == Gdk.keyval_from_name("w"):
-            self._toggle_watched_status()
-            return True
-
-        elif keyval == Gdk.keyval_from_name("BackSpace"):
-            self._navigate_back()
-            return True
-
-        return False
-
-    def _on_key_pressed(
-        self,
-        controller: Gtk.EventControllerKey,
-        keyval: int,
-        keycode: int,
-        state: Gdk.ModifierType,
-    ) -> bool:
-        # Handle keys differently based on which view is active
-        if self.stack.get_visible_child_name() == "player":
-            return self._on_player_key_pressed(keyval, keycode, state)
-        else:
-            return self._on_menu_key_pressed(keyval, keycode, state)
-
-    def _save_position(self) -> None:
-        """Save current playback position as xattr"""
-
-        if not self.video_player.is_playing:
-            return
-
-        file_item = self.selection
-
-        if file_item is None or file_item.file_type == FileType.DIRECTORY:
-            return
-
-        position = self.video_player.get_position()
-        if position is not None:
-            file_item.saved_position = position
-
-        duration = self.video_player.get_duration()
-        if duration is not None:
-            file_item.saved_duration = duration
-
-    def show_overlay_text(self, text: str, timeout_seconds: float = 1.0) -> None:
-        """Show text in a centered overlay that disappears after timeout"""
-        self.overlay_label.set_text(text)
-        self.overlay_label.set_visible(True)
-
-        # Set absolute time when overlay should hide
-        self.overlay_hide_time = self.now + timeout_seconds
-
-    def _on_tick(self, widget: Gtk.Widget, frame_clock: Gdk.FrameClock, data: Any) -> bool:
-        current_time = frame_clock.get_frame_time() / 1_000_000
-
-        self.clock_label.set_text(datetime.now().strftime("%H:%M"))
-
-        if current_time >= self.overlay_hide_time:
-            self.overlay_label.set_visible(False)
-
-        # Save position every 60 seconds
-        frame_time = frame_clock.get_frame_time() / 1_000_000
-        if frame_time - self.last_position_save >= 60.0:
-            self._save_position()
-            self.last_position_save = frame_time
-
-        # Update thumbnail if available
-        file_item = self.selection
-
-        if file_item is not None:
-            if file_item.thumbnail and not self.thumbnail_image.get_paintable():
-                gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail))
-                texture = Gdk.Texture.new_from_bytes(gbytes)
-                self.thumbnail_image.set_paintable(texture)
-
-        return True
-
-    def _populate_file_list(self) -> None:
-        items: list[FileItem] = []
-
-        for entry in os.scandir():
-            if entry.name.startswith("."):
-                continue
-
-            path = Path(entry.name)
-
-            if path.is_dir():
-                items.append(FileItem(path.name, FileType.DIRECTORY, path.resolve()))
-            elif path.suffix in (".mkv", ".mp4", ".avi"):
-                file_item = FileItem(path.name, FileType.VIDEO, path.resolve())
-                items.append(file_item)
-
-        # Sort directories first, then files, both alphabetically
-        items.sort(key=lambda x: (x.file_type != FileType.DIRECTORY, x.name.lower()))
-
-        self.list_model.set_items(items)
-
-
-class App(Gtk.Application):
-    def __init__(self, thumbnailer: Thumbnailer):
-        super().__init__()
-        self.thumbnailer = thumbnailer
-
-        # Load CSS
-        css_provider = Gtk.CssProvider()
-        css_file = Path(__file__).parent / "style.css"
-        css_provider.load_from_path(str(css_file))
-
-        display = Gdk.Display.get_default()
-        if display is None:
-            raise RuntimeError("No display available")
-
-        Gtk.StyleContext.add_provider_for_display(
-            display,
-            css_provider,
-            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
-        )
-
-    def do_activate(self):
-        win = MainWindow(application=self, thumbnailer=self.thumbnailer)
-        win.present()
+from .application import Application  # NOQA: E402
+from .thumbnailer import Thumbnailer  # NOQA: E402
 
 
 def main():
@@ -546,7 +26,7 @@ def main():
     Gst.init(None)
 
     thumbnailer = Thumbnailer()
-    app = App(thumbnailer)
+    app = Application(thumbnailer=thumbnailer)
 
     try:
         thumbnailer.start()
diff --git a/lazy_player/application.py b/lazy_player/application.py
new file mode 100644
index 0000000..2cbaca7
--- /dev/null
+++ b/lazy_player/application.py
@@ -0,0 +1,35 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from gi.repository import Gdk, Gtk
+
+from .main_window import MainWindow
+from .thumbnailer import Thumbnailer
+
+__all__ = ["Application"]
+
+
+class Application(Gtk.Application):
+    def __init__(self, thumbnailer: Thumbnailer):
+        super().__init__()
+        self.thumbnailer = thumbnailer
+
+        # Load CSS
+        css_provider = Gtk.CssProvider()
+        css_file = Path(__file__).parent / "style.css"
+        css_provider.load_from_path(str(css_file))
+
+        display = Gdk.Display.get_default()
+        if display is None:
+            raise RuntimeError("No display available")
+
+        Gtk.StyleContext.add_provider_for_display(
+            display,
+            css_provider,
+            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
+        )
+
+    def do_activate(self):
+        win = MainWindow(application=self, thumbnailer=self.thumbnailer)
+        win.present()
diff --git a/lazy_player/file_model.py b/lazy_player/file_model.py
index ae49f2c..698ed9e 100644
--- a/lazy_player/file_model.py
+++ b/lazy_player/file_model.py
@@ -6,10 +6,7 @@ from enum import Enum, auto
 from pathlib import Path
 from typing import Optional, overload
 
-import gi
-
-gi.require_version("GObject", "2.0")
-from gi.repository import Gio, GObject  # NOQA: E402
+from gi.repository import Gio, GObject
 
 
 class FileType(Enum):
diff --git a/lazy_player/main_window.py b/lazy_player/main_window.py
new file mode 100644
index 0000000..8ffbd57
--- /dev/null
+++ b/lazy_player/main_window.py
@@ -0,0 +1,506 @@
+from __future__ import annotations
+
+import os
+from datetime import datetime
+from pathlib import Path
+from typing import Any, cast
+
+from gi.repository import Gdk, GLib, Gtk, Pango
+
+from .file_model import FileItem, FileListModel, FileType
+from .thumbnailer import Thumbnailer
+from .video_player import VideoPlayer
+
+
+class MainWindow(Gtk.ApplicationWindow):
+    stack: Gtk.Stack
+    list_view: Gtk.ListView
+    list_model: FileListModel
+    selection_model: Gtk.SingleSelection
+    video_widget: Gtk.Picture
+    tick_callback_id: int
+    overlay_label: Gtk.Label
+    overlay_hide_time: float
+    last_position_save: float
+
+    directory_history: list[Path]
+    selection_history: dict[str, int]
+
+    def __init__(self, *args: Any, thumbnailer: Thumbnailer, **kwargs: Any):
+        super().__init__(*args, **kwargs)
+        self.thumbnailer = thumbnailer
+
+        # Directory history stack
+        self.directory_history = []
+        self.selection_history = {}
+
+        # For overlay text timeout
+        self.overlay_hide_time = 0.0
+        self.last_position_save = 0.0
+        self.overlay_label = Gtk.Label()
+        self.overlay_label.set_name("overlay-text")
+        self.overlay_label.set_valign(Gtk.Align.CENTER)
+        self.overlay_label.set_halign(Gtk.Align.CENTER)
+        self.overlay_label.set_visible(False)
+        self.overlay_label.set_wrap(True)
+        self.overlay_label.set_wrap_mode(Pango.WrapMode.WORD_CHAR)
+
+        self.tick_callback_id = self.add_tick_callback(self._on_tick, None)
+
+        # Make window fullscreen and borderless
+        self.set_decorated(False)
+        self.fullscreen()
+
+        # Setup key event controller
+        key_controller = Gtk.EventControllerKey()
+        key_controller.connect("key-pressed", self._on_key_pressed)
+        self.add_controller(key_controller)
+
+        # Disable and hide mouse cursor
+        self.set_can_target(False)
+        display = Gdk.Display.get_default()
+        if display:
+            cursor = Gdk.Cursor.new_from_name("none")
+            if cursor:
+                self.set_cursor(cursor)
+
+        # Main horizontal box to split the screen
+        main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+
+        # Create stack to hold our widgets
+        self.stack = Gtk.Stack()
+
+        # Create video widget and overlay
+        self.video_widget = Gtk.Picture()
+        self.video_widget.set_can_shrink(True)
+        self.video_widget.set_keep_aspect_ratio(True)
+        self.video_widget.set_vexpand(True)
+        self.video_widget.set_hexpand(True)
+
+        video_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+        video_box.set_name("black-overlay")
+        video_box.set_vexpand(True)
+        video_box.set_hexpand(True)
+
+        # Create an overlay container
+        overlay = Gtk.Overlay()
+        overlay.set_child(self.video_widget)
+        overlay.add_overlay(self.overlay_label)
+
+        video_box.append(overlay)
+
+        # Setup video player
+        self.video_player = VideoPlayer(self.video_widget)
+
+        # Add both main menu and overlay to stack
+        self.stack.add_named(main_box, "menu")
+        self.stack.add_named(video_box, "player")
+        self.stack.set_visible_child_name("menu")
+
+        # Create a grid to handle the 1:2 ratio
+        grid = Gtk.Grid()
+        grid.set_column_homogeneous(True)
+        grid.set_hexpand(True)
+
+        # Left third (1/3 width)
+        left_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+        left_box.set_valign(Gtk.Align.START)
+        left_box.set_halign(Gtk.Align.FILL)
+
+        # Create digital clock
+        self.clock_label = Gtk.Label()
+        self.clock_label.set_name("digital-clock")
+        self.clock_label.set_halign(Gtk.Align.CENTER)
+        self.clock_label.set_valign(Gtk.Align.CENTER)
+        self.clock_label.set_text(datetime.now().strftime("%H:%M"))
+        left_box.append(self.clock_label)
+
+        # Create image widget for thumbnail
+        self.thumbnail_image = Gtk.Picture()
+        self.thumbnail_image.set_name("thumbnail-image")
+        self.thumbnail_image.set_size_request(384, 216)  # 16:9 aspect ratio
+        self.thumbnail_image.set_can_shrink(True)
+        self.thumbnail_image.set_keep_aspect_ratio(True)
+        self.thumbnail_image.set_resource(None)
+        left_box.append(self.thumbnail_image)
+
+        # Right two-thirds (2/3 width)
+        right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+        right_box.set_hexpand(True)
+
+        # Attach boxes to grid with specific column spans
+        grid.attach(left_box, 0, 0, 1, 1)
+        grid.attach(right_box, 1, 0, 2, 1)
+
+        main_box.append(grid)
+
+        # Create list model and view
+        self.list_model = FileListModel()
+        self.list_view = Gtk.ListView()
+        self.selection_model = Gtk.SingleSelection.new(self.list_model)
+        self.selection_model.connect("selection-changed", self._on_selection_changed)
+        self.list_view.set_model(self.selection_model)
+        self.list_view.set_vexpand(True)
+
+        def on_activate(widget: Gtk.ListView, index: int):
+            selected_item = self.selection_model.get_item(index)
+            if selected_item:
+                file_item = cast(FileItem, selected_item)
+
+                if file_item.file_type == FileType.DIRECTORY:
+                    self._navigate_to(file_item.full_path)
+                    return
+
+                position = file_item.saved_position
+                duration = file_item.saved_duration
+
+                if (position / duration) >= 0.99:
+                    position = 0
+
+                # Start playing the video
+                self.video_player.play(
+                    file_item.full_path,
+                    position,
+                    file_item.saved_subtitle_track,
+                    file_item.saved_audio_track,
+                )
+                self.last_position_save = self.now
+
+                self.stack.set_visible_child_name("player")
+                self.show_overlay_text(f"Playing: {file_item.name}")
+
+        self.list_view.connect("activate", on_activate)
+
+        # Factory for list items
+        factory = Gtk.SignalListItemFactory()
+        factory.connect("setup", self._setup_list_item)
+        factory.connect("bind", self._bind_list_item)
+        self.list_view.set_factory(factory)
+
+        # Populate the list store
+        self._populate_file_list()
+
+        # Add list view to a scrolled window
+        scrolled = Gtk.ScrolledWindow()
+        scrolled.set_child(self.list_view)
+        right_box.append(scrolled)
+
+        self.set_child(self.stack)
+
+    @property
+    def now(self) -> float:
+        frame_clock = self.get_frame_clock()
+        if frame_clock is None:
+            return 0
+
+        return frame_clock.get_frame_time() / 1_000_000
+
+    @property
+    def selection(self) -> FileItem | None:
+        selected_item = self.selection_model.get_selected_item()
+        return cast(FileItem, selected_item) if selected_item else None
+
+    def _setup_list_item(self, factory: Gtk.SignalListItemFactory, list_item: Gtk.ListItem):
+        # Create horizontal box to hold icon and label
+        box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+        box.set_spacing(8)
+
+        # Create icon image
+        icon = Gtk.Image()
+        icon.set_css_classes(["file-icon"])
+        box.append(icon)
+
+        # Create label
+        label = Gtk.Label()
+        label.set_halign(Gtk.Align.START)
+        box.append(label)
+
+        list_item.set_child(box)
+
+    def _bind_list_item(self, factory: Gtk.SignalListItemFactory, list_item: Gtk.ListItem):
+        box = cast(Gtk.Box, list_item.get_child())
+        icon = cast(Gtk.Image, box.get_first_child())
+        label = cast(Gtk.Label, box.get_last_child())
+        item = cast(FileItem, list_item.get_item())
+
+        def update_icon(*args: object) -> None:
+            if item.file_type == FileType.DIRECTORY:
+                icon.set_from_icon_name("folder-symbolic")
+                icon.set_css_classes(["file-icon"])
+            else:
+                position = item.saved_position
+                duration = item.saved_duration
+
+                if position == 0:
+                    icon.set_from_icon_name("media-playback-start-symbolic")
+                    icon.set_css_classes(["file-icon", "unwatched"])
+                elif (position / duration) >= 0.99:
+                    icon.set_from_icon_name("object-select-symbolic")
+                    icon.set_css_classes(["file-icon", "completed"])
+                else:
+                    icon.set_from_icon_name("media-playback-pause-symbolic")
+                    icon.set_css_classes(["file-icon", "in-progress"])
+
+        label.set_text(item.name)
+        item.connect("notify::saved-position", update_icon)
+        item.connect("notify::saved-duration", update_icon)
+        update_icon()
+
+    def _refresh(self):
+        self._populate_file_list()
+
+        pos = self.selection_history.get(str(os.getcwd()), 0)
+        self.selection_model.set_selected(pos)
+        self.list_view.scroll_to(pos, Gtk.ListScrollFlags.SELECT | Gtk.ListScrollFlags.FOCUS, None)
+
+    def _navigate_to(self, path: Path):
+        self.directory_history.append(Path(os.getcwd()))
+        os.chdir(path)
+        self._populate_file_list()
+
+        pos = self.selection_history.get(str(path), 0)
+        self.selection_model.set_selected(pos)
+        self.list_view.scroll_to(pos, Gtk.ListScrollFlags.SELECT | Gtk.ListScrollFlags.FOCUS, None)
+
+    def _navigate_back(self):
+        if not self.directory_history:
+            return
+
+        prev_dir = self.directory_history.pop()
+        os.chdir(prev_dir)
+        self._populate_file_list()
+
+        pos = self.selection_history.get(str(prev_dir), 0)
+        self.selection_model.set_selected(pos)
+        self.list_view.scroll_to(pos, Gtk.ListScrollFlags.SELECT | Gtk.ListScrollFlags.FOCUS, None)
+
+    def _on_selection_changed(
+        self,
+        selection_model: Gtk.SingleSelection,
+        position: int,
+        n_items: int,
+    ):
+        position = selection_model.get_selected()
+
+        if position == Gtk.INVALID_LIST_POSITION:
+            return
+
+        self.selection_history[os.getcwd()] = position
+
+        file_item = self.selection
+        if file_item is not None:
+            # Update thumbnail if available
+            if file_item.thumbnail:
+                gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail))
+                texture = Gdk.Texture.new_from_bytes(gbytes)
+                self.thumbnail_image.set_paintable(texture)
+            else:
+                self.thumbnailer.generate_thumbnail(file_item)
+                self.thumbnail_image.set_paintable(None)
+
+    def _toggle_play_pause(self) -> None:
+        """Toggle between play and pause states"""
+        self.video_player.toggle_play_pause()
+
+    def _on_player_key_pressed(
+        self,
+        keyval: int,
+        keycode: int,
+        state: Gdk.ModifierType,
+    ) -> bool:
+        if keyval == Gdk.keyval_from_name("space"):
+            self._toggle_play_pause()
+            return True
+
+        elif keyval == Gdk.keyval_from_name("Escape"):
+            self._save_position()
+            self.video_player.stop()
+            self.stack.set_visible_child_name("menu")
+            self.list_view.grab_focus()
+            return True
+
+        elif keyval == Gdk.keyval_from_name("Left"):
+            self.video_player.seek_relative(-10)
+            return True
+
+        elif keyval == Gdk.keyval_from_name("Right"):
+            self.video_player.seek_relative(10)
+            return True
+
+        elif keyval == Gdk.keyval_from_name("Down"):
+            self.video_player.seek_relative(-60)
+            return True
+
+        elif keyval == Gdk.keyval_from_name("Up"):
+            self.video_player.seek_relative(60)
+            return True
+
+        elif keyval == Gdk.keyval_from_name("Home"):
+            self.video_player.seek_start()
+            return True
+
+        elif keyval == Gdk.keyval_from_name("End"):
+            self.video_player.seek_end()
+            return True
+
+        elif keyval == Gdk.keyval_from_name("j"):
+            has_subs, index, lang = self.video_player.cycle_subtitles()
+
+            if has_subs:
+                if index:
+                    self.show_overlay_text(f"Subtitles #{index} ({lang})")
+                else:
+                    self.show_overlay_text("Subtitles turned off")
+
+                file_item = self.selection
+                if file_item is not None:
+                    file_item.saved_subtitle_track = index - 1
+            else:
+                self.show_overlay_text("No subtitles available")
+
+            return True
+
+        elif keyval == Gdk.keyval_from_name("a"):
+            has_audio, index, lang = self.video_player.cycle_audio()
+
+            if has_audio:
+                self.show_overlay_text(f"Audio #{index} ({lang})")
+                file_item = self.selection
+                if file_item is not None:
+                    file_item.saved_audio_track = index - 1
+            else:
+                self.show_overlay_text("No audio tracks available")
+
+            return True
+
+        return False
+
+    def _toggle_watched_status(self) -> None:
+        """Toggle watched status for the selected file"""
+
+        file_item = self.selection
+
+        if file_item is None or file_item.file_type == FileType.DIRECTORY:
+            return
+
+        position = file_item.saved_position
+        duration = file_item.saved_duration
+
+        # If position exists and is >= 99% through, clear it
+        if position > 0 and (position / duration) >= 0.99:
+            file_item.saved_position = 0
+        else:
+            # Otherwise mark as complete
+            file_item.saved_position = duration
+
+        # Force the list to update the changed item
+        self.list_model.items_changed(self.selection_model.get_selected(), 1, 1)
+
+    def _on_menu_key_pressed(
+        self,
+        keyval: int,
+        keycode: int,
+        state: Gdk.ModifierType,
+    ) -> bool:
+        if keyval == Gdk.keyval_from_name("q"):
+            self.close()
+            return True
+
+        elif keyval == Gdk.keyval_from_name("r"):
+            self._refresh()
+            return True
+
+        elif keyval == Gdk.keyval_from_name("w"):
+            self._toggle_watched_status()
+            return True
+
+        elif keyval == Gdk.keyval_from_name("BackSpace"):
+            self._navigate_back()
+            return True
+
+        return False
+
+    def _on_key_pressed(
+        self,
+        controller: Gtk.EventControllerKey,
+        keyval: int,
+        keycode: int,
+        state: Gdk.ModifierType,
+    ) -> bool:
+        # Handle keys differently based on which view is active
+        if self.stack.get_visible_child_name() == "player":
+            return self._on_player_key_pressed(keyval, keycode, state)
+        else:
+            return self._on_menu_key_pressed(keyval, keycode, state)
+
+    def _save_position(self) -> None:
+        """Save current playback position as xattr"""
+
+        if not self.video_player.is_playing:
+            return
+
+        file_item = self.selection
+
+        if file_item is None or file_item.file_type == FileType.DIRECTORY:
+            return
+
+        position = self.video_player.get_position()
+        if position is not None:
+            file_item.saved_position = position
+
+        duration = self.video_player.get_duration()
+        if duration is not None:
+            file_item.saved_duration = duration
+
+    def show_overlay_text(self, text: str, timeout_seconds: float = 1.0) -> None:
+        """Show text in a centered overlay that disappears after timeout"""
+        self.overlay_label.set_text(text)
+        self.overlay_label.set_visible(True)
+
+        # Set absolute time when overlay should hide
+        self.overlay_hide_time = self.now + timeout_seconds
+
+    def _on_tick(self, widget: Gtk.Widget, frame_clock: Gdk.FrameClock, data: Any) -> bool:
+        current_time = frame_clock.get_frame_time() / 1_000_000
+
+        self.clock_label.set_text(datetime.now().strftime("%H:%M"))
+
+        if current_time >= self.overlay_hide_time:
+            self.overlay_label.set_visible(False)
+
+        # Save position every 60 seconds
+        frame_time = frame_clock.get_frame_time() / 1_000_000
+        if frame_time - self.last_position_save >= 60.0:
+            self._save_position()
+            self.last_position_save = frame_time
+
+        # Update thumbnail if available
+        file_item = self.selection
+
+        if file_item is not None:
+            if file_item.thumbnail and not self.thumbnail_image.get_paintable():
+                gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail))
+                texture = Gdk.Texture.new_from_bytes(gbytes)
+                self.thumbnail_image.set_paintable(texture)
+
+        return True
+
+    def _populate_file_list(self) -> None:
+        items: list[FileItem] = []
+
+        for entry in os.scandir():
+            if entry.name.startswith("."):
+                continue
+
+            path = Path(entry.name)
+
+            if path.is_dir():
+                items.append(FileItem(path.name, FileType.DIRECTORY, path.resolve()))
+            elif path.suffix in (".mkv", ".mp4", ".avi"):
+                file_item = FileItem(path.name, FileType.VIDEO, path.resolve())
+                items.append(file_item)
+
+        # Sort directories first, then files, both alphabetically
+        items.sort(key=lambda x: (x.file_type != FileType.DIRECTORY, x.name.lower()))
+
+        self.list_model.set_items(items)
diff --git a/lazy_player/thumbnailer.py b/lazy_player/thumbnailer.py
index 82179d1..2bf52e2 100644
--- a/lazy_player/thumbnailer.py
+++ b/lazy_player/thumbnailer.py
@@ -3,13 +3,10 @@ from __future__ import annotations
 import threading
 from queue import Empty, Queue
 
-import gi
+from gi.repository import Gst
 
 from .file_model import FileItem
 
-gi.require_version("Gst", "1.0")
-from gi.repository import Gst  # NOQA: E402
-
 
 class Thumbnailer(threading.Thread):
     queue: Queue[FileItem | None]
diff --git a/lazy_player/video_player.py b/lazy_player/video_player.py
index 4789e71..50f5277 100644
--- a/lazy_player/video_player.py
+++ b/lazy_player/video_player.py
@@ -2,12 +2,7 @@ from __future__ import annotations
 
 from pathlib import Path
 
-import gi
-
-gi.require_version("Gtk", "4.0")
-gi.require_version("Gst", "1.0")
-gi.require_version("GObject", "2.0")
-from gi.repository import GObject, Gst, Gtk  # NOQA: E402  # NOQA: E402
+from gi.repository import GObject, Gst, Gtk
 
 
 class VideoPlayer(GObject.Object):