Repository: dooit-org/dooit

Issue:
[BUG] Crash on start up
**Describe the bug** 🐛
Dooit 3.0.3 crashes on start up.

**To Reproduce** 🐣
Steps to reproduce the behavior:
1. Type dooit in terminal

**Expected behavior**  🤔 
Not crashing

**Screenshots** 🧐
![241118_11h14m37s_screenshot](https://github.com/user-attachments/assets/1e0ad55e-9bf4-4977-b3df-d108824c02d5)


**Desktop (please complete the following information):**  🤖
 - OS: ArchLinux
 - Kernel 6.11.8-arch1-2

**Additional context** 📝
Package installed from AuR using yay.

Discussion:
@kraanzu: That is weird. Did you install from the AUR?

Oh, looking at the errors. It seems like the tui Library was updated and now used some variable name which was used by dooit earlier. Maybe I'll pin the version on AUR if you're installing from there@nguyentran0212: Yup. I just run pacman - Syu and then update the dooit package from AUR. @kraanzu: Alright. Thanks for confirming, I'll update this in an hour or so@kraanzu: Hi @nguyentran0212, 

should be fixed now. Can you re-sync the packages and check again? Thanks :)@nguyentran0212: @kraanzu thanks for the quick response! I can confirm that version 3.0.4 works without any problem.@github-actions: Did we solve your problem?
Glad we could help! 

Consider sponsoring my work through [github sponsors](https://github.com/sponsors/kraanzu) :smile:
&ndash; @kraanzu

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c213e3..f0be2b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## 3.0.4
+
+This is a minor release with no new changes but fixing some stuff that was caused by recent textual update
+- Fix tint on focused lists
+- Startup crash because of variable override (#207)
+
 ## 3.0.3
 
 ### Fixed
diff --git a/dooit/__main__.py b/dooit/__main__.py
index 17d3bf5..1988e5b 100644
--- a/dooit/__main__.py
+++ b/dooit/__main__.py
@@ -3,7 +3,7 @@ from pathlib import Path
 from platformdirs import user_data_dir, user_config_dir
 
 OLD_CONFIG = Path(user_data_dir("dooit")) / "todo.yaml"
-VERSION = "3.0.3"
+VERSION = "3.0.4"
 
 
 def run_dooit():
diff --git a/dooit/ui/api/api_components/vars.py b/dooit/ui/api/api_components/vars.py
index d27fcd1..f2a499b 100644
--- a/dooit/ui/api/api_components/vars.py
+++ b/dooit/ui/api/api_components/vars.py
@@ -10,13 +10,13 @@ from ._base import ApiComponent
 
 
 if TYPE_CHECKING:  # pragma: no cover
-    from dooit.ui.tui import Dooit
+    from dooit.ui.api.dooit_api import DooitAPI
 
 
 class VarManager(ApiComponent):
-    def __init__(self, app: "Dooit") -> None:
+    def __init__(self, api: "DooitAPI") -> None:
         super().__init__()
-        self.app = app
+        self.api = api
         self._show_confirm = True
 
     @property
@@ -29,19 +29,19 @@ class VarManager(ApiComponent):
 
     @property
     def mode(self) -> str:
-        return self.app.current_mode
+        return self.api.app.dooit_mode
 
     @property
     def theme(self) -> DooitThemeBase:
-        return self.app.current_theme
+        return self.api.css.theme
 
     @property
     def workspaces_tree(self) -> WorkspacesTree:
-        return self.app.query_one(WorkspacesTree)
+        return self.api.app.query_one(WorkspacesTree)
 
     @property
     def current_workspace(self) -> Optional[Workspace]:
-        tree = self.app.workspace_tree
+        tree = self.api.vars.workspaces_tree
         if tree.highlighted is None:
             return None
 
@@ -49,7 +49,7 @@ class VarManager(ApiComponent):
 
     @property
     def todos_tree(self) -> Optional[TodosTree]:
-        todo_switcher = self.app.query_one(
+        todo_switcher = self.api.app.query_one(
             "#todo_switcher", expect_type=ContentSwitcher
         )
         if todo_switcher.visible_content and isinstance(
diff --git a/dooit/ui/api/dooit_api.py b/dooit/ui/api/dooit_api.py
index 9a1fdc0..a63856c 100644
--- a/dooit/ui/api/dooit_api.py
+++ b/dooit/ui/api/dooit_api.py
@@ -25,11 +25,11 @@ class DooitAPI:
         self.app = app
         self.plugin_manager = PluginManager(self)
         self.css = CssManager()
-        self.keys = KeyManager(self.app.get_mode)
+        self.keys = KeyManager(self.app.get_dooit_mode)
         self.layouts = LayoutManager(self.app)
         self.formatter = Formatter(self)
         self.bar = BarManager(self)
-        self.vars = VarManager(self.app)
+        self.vars = VarManager(self)
         self.dashboard = DashboardManager(self.app)
 
         self.css.refresh_css()
diff --git a/dooit/ui/api/loader.py b/dooit/ui/api/loader.py
index 18c18ea..1f48b66 100644
--- a/dooit/ui/api/loader.py
+++ b/dooit/ui/api/loader.py
@@ -2,42 +2,49 @@ import importlib.util
 import sys
 from typing import TYPE_CHECKING
 from pathlib import Path
+from contextlib import contextmanager
 
 if TYPE_CHECKING:  # pragma: no cover
     from .plug import PluginManager
 
 
-def register(api: "PluginManager", path: Path) -> None:
-    spec = importlib.util.spec_from_file_location("", path)
+@contextmanager
+def temporary_sys_path(path: Path):
+    """Context manager to temporarily add a directory to sys.path."""
 
-    if spec and spec.loader:
-        foo = importlib.util.module_from_spec(spec)
-        spec.loader.exec_module(foo)
-        for obj in vars(foo).values():
-            api.register(obj)
+    # Temporarily add the parent directory to the path
+    # to allow imports from other files in the same directory
 
+    parent_path = str(path.parent.absolute())
+    if parent_path not in sys.path:
+        sys.path.insert(0, parent_path)
+    try:
+        yield
+    finally:
+        if parent_path in sys.path:
+            sys.path.remove(parent_path)
 
-def load_file(api: "PluginManager", path: Path) -> bool:
-    if not path.exists():
-        return False
 
-    if path.suffix == ".py":
-        register(api, path)
+def register(api: "PluginManager", path: Path) -> None:
+    module_name = f"dynamic_{path.stem}"
+    spec = importlib.util.spec_from_file_location(module_name, path)
 
-    return True
+    if spec and spec.loader:
+        parent_path = str(path.parent.absolute())
+        sys.path.append(parent_path)
 
+        with temporary_sys_path(path):
+            module = importlib.util.module_from_spec(spec)
+            spec.loader.exec_module(module)
+            for obj in vars(module).values():
+                api.register(obj)
 
-def load_dir(api: "PluginManager", path: Path) -> bool:
-    # allows users to import from the directory
-    sys.path.append(str(path.resolve()))
 
+def load_file(api: "PluginManager", path: Path) -> bool:
     if not path.exists():
         return False
 
-    for file in path.iterdir():
-        if file.is_dir():
-            return load_dir(api, file)
-
-        load_file(api, file)
+    if path.suffix == ".py":
+        register(api, path)
 
     return True
diff --git a/dooit/ui/api/plug.py b/dooit/ui/api/plug.py
index aa46094..380be21 100644
--- a/dooit/ui/api/plug.py
+++ b/dooit/ui/api/plug.py
@@ -1,5 +1,6 @@
-from functools import partial
+import os
 import sys
+from functools import partial
 from collections import defaultdict
 from pathlib import Path
 from typing import TYPE_CHECKING, Callable, List, Type
@@ -8,7 +9,7 @@ from textual.css.query import NoMatches
 
 from dooit.ui.api.event_handlers import DOOIT_EVENT_ATTR, DOOIT_TIMER_ATTR
 from dooit.ui.api.events import DooitEvent
-from .loader import load_dir, load_file
+from .loader import load_file
 
 if TYPE_CHECKING:  # pragma: no cover
     from dooit.ui.api.dooit_api import DooitAPI
@@ -24,6 +25,10 @@ CONFIG_FOLDER = Path(user_config_dir(MAIN_FOLDER))
 DEFAULT_CONFIG = BASE_PATH / "utils" / "default_config.py"
 
 
+def is_running_under_pytest() -> bool:
+    return "PYTEST_CURRENT_TEST" in os.environ
+
+
 class PluginManager:
     def __init__(self, api: "DooitAPI") -> None:
         self.events: defaultdict[Type[DooitEvent], List[Callable]] = defaultdict(list)
@@ -33,7 +38,10 @@ class PluginManager:
 
     def scan(self):
         load_file(self, DEFAULT_CONFIG)
-        load_dir(self, CONFIG_FOLDER)
+        if is_running_under_pytest():
+            return
+
+        load_file(self, CONFIG_FOLDER / "config.py")
 
     def _update_dooit_value(self, obj, *params):
         res = obj(self.api, *params)
diff --git a/dooit/ui/tui.py b/dooit/ui/tui.py
index e1c0093..7cbc117 100644
--- a/dooit/ui/tui.py
+++ b/dooit/ui/tui.py
@@ -1,10 +1,7 @@
-import webbrowser
 from typing import Optional
 from textual import on
 from textual.app import App
 from textual.binding import Binding
-
-from dooit.api.theme import DooitThemeBase
 from dooit.ui.api.events import ModeChanged, DooitEvent, ModeType, Startup, _QuitApp
 from dooit.ui.api.events.events import ShutDown
 from dooit.ui.widgets import BarSwitcher
@@ -37,7 +34,7 @@ class Dooit(App):
 
     def __init__(self, connection_string: Optional[str] = None):
         super().__init__(watch_css=True)
-        self._mode: ModeType = "NORMAL"
+        self.dooit_mode: ModeType = "NORMAL"
         manager.connect(connection_string)
 
     async def base_setup(self):
@@ -48,7 +45,7 @@ class Dooit(App):
         self.push_screen("main")
 
     async def setup_poller(self):
-        self.set_interval(1, self.poll)
+        self.set_interval(1, self.poll_dooit_db)
 
     async def on_mount(self):
         await self.base_setup()
@@ -70,14 +67,10 @@ class Dooit(App):
     def bar_switcher(self) -> BarSwitcher:
         return self.query_one(BarSwitcher)
 
-    def get_mode(self) -> ModeType:
-        return self._mode
-
-    @property
-    def current_theme(self) -> DooitThemeBase:
-        return self.api.css.theme
+    def get_dooit_mode(self) -> ModeType:
+        return self.dooit_mode
 
-    async def poll(self):  # pragma: no cover
+    async def poll_dooit_db(self):  # pragma: no cover
         def refresh_all_trees():
             trees = self.query(ModelTree)
             for tree in trees:
@@ -93,12 +86,12 @@ class Dooit(App):
             self.bar.refresh()
 
     @on(ShutDown)
-    def shutdown(self, event: ShutDown):
+    def shutdown(self, _: ShutDown):
         self.api.css.cleanup()
 
     @on(ModeChanged)
     def change_status(self, event: ModeChanged):
-        self._mode = event.mode
+        self.dooit_mode = event.mode
         if event.mode == "NORMAL":
             self.workspace_tree.refresh_options()
             todos_tree = self.api.vars.todos_tree
@@ -110,7 +103,7 @@ class Dooit(App):
         await self.action_quit()
 
     async def action_open_url(self, url: str) -> None:  # pragma: no cover
-        webbrowser.open(url, new=2)
+        self.open_url(url)
 
 
 if __name__ == "__main__":  # pragma: no cover
diff --git a/dooit/utils/css_manager.py b/dooit/utils/css_manager.py
index 9abfafb..4b3c5a9 100644
--- a/dooit/utils/css_manager.py
+++ b/dooit/utils/css_manager.py
@@ -26,7 +26,7 @@ class CssManager:
         theme: DooitThemeBase = DooitThemeBase(),
         cache_path: Path = dooit_cache_path,
     ):
-        self.theme = theme
+        self.theme: DooitThemeBase = theme
         self.cache_path = cache_path
         self.stylesheets: Path = cache_path / "stylesheets"
         self.css_file: Path = cache_path / "dooit.tcss"
@@ -59,14 +59,14 @@ class CssManager:
         self.write(css)
 
     def add_theme(self, theme: Type[DooitThemeBase]):
-        self.themes[theme._name] = theme
+        self.themes[theme._name] = theme()
         self.refresh_css()
 
     def set_theme(self, theme: Union[str, Type[DooitThemeBase]]):
         if isinstance(theme, str):
             self.theme = self.themes.get(theme, DooitThemeBase)
         else:
-            self.theme = theme
+            self.theme = theme()
 
         self.refresh_css()
 
diff --git a/dooit/utils/default_config.py b/dooit/utils/default_config.py
index 13337ed..d537e90 100644
--- a/dooit/utils/default_config.py
+++ b/dooit/utils/default_config.py
@@ -13,7 +13,7 @@ from rich.text import Text
 
 @subscribe(ModeChanged)
 def get_mode(api: DooitAPI, event: ModeChanged):
-    theme = api.app.current_theme
+    theme = api.vars.theme
     mode = event.mode
 
     MODES = {
@@ -32,7 +32,7 @@ def get_mode(api: DooitAPI, event: ModeChanged):
 
 @timer(1)
 def get_clock(api: DooitAPI):
-    theme = api.app.current_theme
+    theme = api.vars.theme
     time = datetime.now().strftime("%H:%M:%S")
     return Text(
         f" {time} ",
@@ -45,7 +45,7 @@ def get_clock(api: DooitAPI):
 
 @subscribe(Startup)
 def get_user(api: DooitAPI, _: Startup):
-    theme = api.app.current_theme
+    theme = api.vars.theme
     try:
         username = os.getlogin()
     except OSError:
@@ -65,17 +65,19 @@ def get_user(api: DooitAPI, _: Startup):
 # Todo formatters
 
 
-def todo_status_formatter(status: str, todo: Todo, api: DooitAPI):
+def todo_status_formatter(status: str, _: Todo, api: DooitAPI):
     text = "o"
-    color = api.app.current_theme.yellow
+    theme = api.vars.theme
+
+    color = theme.yellow
 
     if status == "completed":
         text = "x"
-        color = api.app.current_theme.green
+        color = theme.green
 
     if status == "overdue":
         text = "!"
-        color = api.app.current_theme.red
+        color = theme.red
 
     return Text(text, style=Style(color=color, bold=True))
 
@@ -96,7 +98,7 @@ def todo_urgency_formatter(urgency, _, api: DooitAPI):
     if urgency == 0:
         return ""
 
-    theme = api.app.current_theme
+    theme = api.vars.theme
     colors = {
         1: theme.green,
         2: theme.yellow,
diff --git a/pyproject.toml b/pyproject.toml
index 8e8ce08..cabb19f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "dooit"
-version = "3.0.3"
+version = "3.0.4"
 description = "A TUI Todo Manager"
 authors = ["kraanzu <kraanzu@gmail.com>"]
 maintainers = ["kraanzu <kraanzu@gmail.com>"]
@@ -15,7 +15,7 @@ python = "^3.9"
 pyperclip = "^1.9.0"
 pyyaml = "^6.0.2"
 tzlocal = "^5.2"
-textual = "^0.85.2"
+textual = "^0.86.1"
 python-dateutil = "^2.9.0.post0"
 sqlalchemy = "^2.0.36"
 platformdirs = "^4.3.6"


Test patch:
diff --git a/tests/test_core/test_css_manager.py b/tests/test_core/test_css_manager.py
index 7b74a4d..723eefb 100644
--- a/tests/test_core/test_css_manager.py
+++ b/tests/test_core/test_css_manager.py
@@ -17,7 +17,7 @@ def test_css_manager():
 
     # via classname
     manager.set_theme(TestTheme)
-    assert manager.theme is TestTheme
+    assert manager.theme.__class__ is TestTheme
 
     # reset
     manager.set_theme(DooitThemeBase)
@@ -26,7 +26,7 @@ def test_css_manager():
     # via name
     manager.add_theme(TestTheme)
     manager.set_theme("test_theme")
-    assert manager.theme is TestTheme
+    assert manager.theme.__class__ is TestTheme
 
 
 # ----------------------------------------
diff --git a/tests/test_ui/test_loader.py b/tests/test_ui/test_loader.py
index 564bf07..68ab27a 100644
--- a/tests/test_ui/test_loader.py
+++ b/tests/test_ui/test_loader.py
@@ -13,14 +13,3 @@ async def test_loader():
 
         incorrect_path = temp_folder / "incorrect_path"
         assert not loader.load_file(app.api.plugin_manager, incorrect_path)
-
-        incorrect_folder = temp_folder / "incorrect_folder"
-        assert not loader.load_dir(app.api.plugin_manager, incorrect_path)
-
-        subfolder = incorrect_folder / "subfolder"
-        test_file = subfolder / "test_file.py"
-
-        subfolder.mkdir(parents=True, exist_ok=True)
-        test_file.touch()
-
-        assert loader.load_dir(app.api.plugin_manager, incorrect_folder)
diff --git a/tests/test_ui/test_startup.py b/tests/test_ui/test_startup.py
index 4f8b927..20dfd04 100644
--- a/tests/test_ui/test_startup.py
+++ b/tests/test_ui/test_startup.py
@@ -13,4 +13,4 @@ async def test_startup():
         app.bar_switcher
         app.workspace_tree
 
-        assert app.get_mode() == "NORMAL"
+        assert app.get_dooit_mode() == "NORMAL"