memoryframes 0.4.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memoryframes-0.4.0 → memoryframes-0.6.0}/PKG-INFO +2 -1
- {memoryframes-0.4.0 → memoryframes-0.6.0}/pyproject.toml +7 -2
- memoryframes-0.6.0/src/MemoryFrames/AppState.py +94 -0
- {memoryframes-0.4.0 → memoryframes-0.6.0}/src/MemoryFrames/PluginInfra/Plugin.py +22 -30
- memoryframes-0.6.0/src/MemoryFrames/PluginInfra/Settings.py +86 -0
- {memoryframes-0.4.0 → memoryframes-0.6.0}/src/MemoryFrames/PluginInfra/TextualUserExperienceInfo.py +2 -2
- memoryframes-0.6.0/src/MemoryFrames/TextualUserExperience.py +150 -0
- memoryframes-0.6.0/src/MemoryFrames/TextualUserExperience.tcss +33 -0
- memoryframes-0.6.0/src/MemoryFrames/__main__.py +107 -0
- {memoryframes-0.4.0 → memoryframes-0.6.0}/README.md +0 -0
- {memoryframes-0.4.0 → memoryframes-0.6.0}/src/MemoryFrames/PluginInfra/Note.py +0 -0
- {memoryframes-0.4.0 → memoryframes-0.6.0}/src/MemoryFrames/PluginInfra/NoteSource.py +0 -0
- {memoryframes-0.4.0 → memoryframes-0.6.0}/src/MemoryFrames/PluginInfra/README.md +0 -0
- {memoryframes-0.4.0 → memoryframes-0.6.0}/src/MemoryFrames/PluginInfra/UserExperienceInfo.py +0 -0
- {memoryframes-0.4.0 → memoryframes-0.6.0}/src/MemoryFrames/PluginInfra/__init__.py +0 -0
- {memoryframes-0.4.0 → memoryframes-0.6.0}/src/MemoryFrames/__init__.py +0 -0
- {memoryframes-0.4.0 → memoryframes-0.6.0}/src/MemoryFrames/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: memoryframes
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Author: David Brownell
|
|
6
6
|
Author-email: David Brownell <github@DavidBrownell.com>
|
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
13
13
|
Requires-Dist: dbrownell-common>=0.16.0
|
|
14
14
|
Requires-Dist: pluggy>=1.6.0
|
|
15
15
|
Requires-Dist: python-frontmatter>=1.1.0
|
|
16
|
+
Requires-Dist: rtyaml>=1.0.0
|
|
16
17
|
Requires-Dist: textual>=6.8.0
|
|
17
18
|
Requires-Dist: typer>=0.20.0
|
|
18
19
|
Requires-Dist: typer-config>=1.4.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "MemoryFrames"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
# ^^^^^
|
|
5
5
|
# Wheel names will be generated according to this value. Do not manually modify this value; instead
|
|
6
6
|
# update it according to committed changes by running this command from the root of the repository:
|
|
@@ -17,6 +17,7 @@ dependencies = [
|
|
|
17
17
|
"dbrownell-common>=0.16.0",
|
|
18
18
|
"pluggy>=1.6.0",
|
|
19
19
|
"python-frontmatter>=1.1.0",
|
|
20
|
+
"rtyaml>=1.0.0",
|
|
20
21
|
"textual>=6.8.0",
|
|
21
22
|
"typer>=0.20.0",
|
|
22
23
|
"typer-config>=1.4.3",
|
|
@@ -32,6 +33,9 @@ classifiers = [
|
|
|
32
33
|
[project.license]
|
|
33
34
|
text = "MIT"
|
|
34
35
|
|
|
36
|
+
[project.scripts]
|
|
37
|
+
memoryframes = "MemoryFrames.__main__:app"
|
|
38
|
+
|
|
35
39
|
[project.urls]
|
|
36
40
|
Homepage = "https://github.com/davidbrownell/MemoryFrames"
|
|
37
41
|
Documentation = "https://github.com/davidbrownell/MemoryFrames"
|
|
@@ -47,6 +51,7 @@ dev = [
|
|
|
47
51
|
"dbrownell-commitemojis>=0.2.0",
|
|
48
52
|
"pre-commit>=4.5.0",
|
|
49
53
|
"py-minisign>=0.13.0",
|
|
54
|
+
"pyfakefs>=5.10.2",
|
|
50
55
|
"pytest>=9.0.2",
|
|
51
56
|
"pytest-cov>=7.0.0",
|
|
52
57
|
"ruff>=0.14.9",
|
|
@@ -54,7 +59,7 @@ dev = [
|
|
|
54
59
|
]
|
|
55
60
|
|
|
56
61
|
[tool.pytest.ini_options]
|
|
57
|
-
addopts = "--verbose -vv --capture=no --cov=MemoryFrames --cov-report html --cov-report term --cov-report xml:coverage.xml --cov-fail-under=
|
|
62
|
+
addopts = "--verbose -vv --capture=no --cov=MemoryFrames --cov-report html --cov-report term --cov-report xml:coverage.xml --cov-fail-under=75.0"
|
|
58
63
|
python_files = [
|
|
59
64
|
"**/*Test.py",
|
|
60
65
|
]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import auto, Enum
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import pluggy
|
|
7
|
+
|
|
8
|
+
from MemoryFrames import APP_NAME
|
|
9
|
+
from MemoryFrames.PluginInfra import Plugin as PluginModule
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from MemoryFrames.PluginInfra.Plugin import Plugin
|
|
13
|
+
from MemoryFrames.PluginInfra.Settings import Settings
|
|
14
|
+
from MemoryFrames.PluginInfra.UserExperienceInfo import UserExperienceInfo
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ----------------------------------------------------------------------
|
|
18
|
+
class AppStateObserver(ABC):
|
|
19
|
+
"""Observer for AppState changes."""
|
|
20
|
+
|
|
21
|
+
# ----------------------------------------------------------------------
|
|
22
|
+
class EventType(Enum):
|
|
23
|
+
"""Value signaling progress during the creation of an AppState instance."""
|
|
24
|
+
|
|
25
|
+
LoadingPlugins = auto()
|
|
26
|
+
|
|
27
|
+
# ----------------------------------------------------------------------
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def OnEvent(self, event_type: EventType) -> None:
|
|
30
|
+
"""Invoke when a processing event begins."""
|
|
31
|
+
raise NotImplementedError() # pragma: no cover
|
|
32
|
+
|
|
33
|
+
# ----------------------------------------------------------------------
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def OnException(self, exception: Exception) -> None:
|
|
36
|
+
"""Invoke when an exception occurs during processing."""
|
|
37
|
+
raise NotImplementedError() # pragma: no cover
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ----------------------------------------------------------------------
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class AppState:
|
|
43
|
+
"""MemoryFrames application state."""
|
|
44
|
+
|
|
45
|
+
# ----------------------------------------------------------------------
|
|
46
|
+
plugins: list[Plugin]
|
|
47
|
+
|
|
48
|
+
# ----------------------------------------------------------------------
|
|
49
|
+
@classmethod
|
|
50
|
+
def Create(
|
|
51
|
+
cls,
|
|
52
|
+
settings: Settings,
|
|
53
|
+
user_experience_info: UserExperienceInfo,
|
|
54
|
+
observer: AppStateObserver,
|
|
55
|
+
) -> AppState | None:
|
|
56
|
+
"""Create an AppState instance."""
|
|
57
|
+
|
|
58
|
+
# Load the plugins
|
|
59
|
+
observer.OnEvent(AppStateObserver.EventType.LoadingPlugins)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
plugins = LoadPlugins(settings, user_experience_info)
|
|
63
|
+
except Exception as ex:
|
|
64
|
+
observer.OnException(ex)
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
# We want the highest priority plugins first, and then sort by name. We apply the negative so
|
|
68
|
+
# that higher priority values appear first while still maintaining ascending order for names.
|
|
69
|
+
plugins.sort(key=lambda plugin: (-(plugin.PLUGIN_PRIORITY or 0), plugin.NAME))
|
|
70
|
+
|
|
71
|
+
return cls(plugins)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ----------------------------------------------------------------------
|
|
75
|
+
def LoadPlugins(
|
|
76
|
+
settings: Settings,
|
|
77
|
+
user_experience_info: UserExperienceInfo,
|
|
78
|
+
) -> list[Plugin]:
|
|
79
|
+
"""Load the plugins.
|
|
80
|
+
|
|
81
|
+
This is implemented as a separate function to make it easier to monkey-patch during testing.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
plugin_manager = pluggy.PluginManager(APP_NAME)
|
|
85
|
+
|
|
86
|
+
plugin_manager.add_hookspecs(PluginModule)
|
|
87
|
+
plugin_manager.load_setuptools_entrypoints(APP_NAME)
|
|
88
|
+
|
|
89
|
+
plugins: list[Plugin] = plugin_manager.hook.GetPlugin(
|
|
90
|
+
settings=settings,
|
|
91
|
+
user_experience_info=user_experience_info,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return plugins
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
1
|
from dataclasses import dataclass
|
|
3
2
|
from functools import cached_property
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
3
|
+
from typing import ClassVar, TYPE_CHECKING
|
|
5
4
|
|
|
6
5
|
import pluggy
|
|
7
6
|
|
|
@@ -13,6 +12,7 @@ if TYPE_CHECKING:
|
|
|
13
12
|
from pathlib import Path
|
|
14
13
|
|
|
15
14
|
from MemoryFrames.PluginInfra.NoteSource import NoteSource, NoteSourceObserver
|
|
15
|
+
from MemoryFrames.PluginInfra.Settings import Settings
|
|
16
16
|
from MemoryFrames.PluginInfra.UserExperienceInfo import UserExperienceInfo
|
|
17
17
|
|
|
18
18
|
|
|
@@ -31,14 +31,31 @@ class ThreadInfo:
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
# ----------------------------------------------------------------------
|
|
34
|
-
class Plugin
|
|
34
|
+
class Plugin:
|
|
35
35
|
"""Base class for all MemoryFrames plugins."""
|
|
36
36
|
|
|
37
|
+
# ----------------------------------------------------------------------
|
|
38
|
+
NAME: ClassVar[str] = ""
|
|
39
|
+
"""The name of the plugin."""
|
|
40
|
+
|
|
41
|
+
AUTHOR: ClassVar[str] = ""
|
|
42
|
+
"""The author of the plugin. This value will be used in the plugin's unique name."""
|
|
43
|
+
|
|
44
|
+
DESCRIPTION: ClassVar[str] = ""
|
|
45
|
+
"""The description of the plugin."""
|
|
46
|
+
|
|
47
|
+
PLUGIN_PRIORITY: ClassVar[int] = 0
|
|
48
|
+
"""The priority of the plugin. Higher values indicate higher priority, which will cause the plugin to appear before other lower priority plugins."""
|
|
49
|
+
|
|
37
50
|
# ----------------------------------------------------------------------
|
|
38
51
|
def __init__(
|
|
39
52
|
self,
|
|
40
53
|
root_data_dir: Path,
|
|
41
54
|
) -> None:
|
|
55
|
+
assert self.__class__.NAME, "Derived classes must set the NAME class variable"
|
|
56
|
+
assert self.__class__.AUTHOR, "Derived classes must set the AUTHOR class variable"
|
|
57
|
+
assert self.__class__.DESCRIPTION, "Derived classes must set the DESCRIPTION class variable"
|
|
58
|
+
|
|
42
59
|
scrubbed_unique_name = self.unique_name
|
|
43
60
|
|
|
44
61
|
for invalid_char in ["<", ">", ":", '"', "/", "\\", "|", "?", "*", "."]:
|
|
@@ -47,34 +64,10 @@ class Plugin(ABC):
|
|
|
47
64
|
self.working_dir = root_data_dir / scrubbed_unique_name
|
|
48
65
|
|
|
49
66
|
# ----------------------------------------------------------------------
|
|
50
|
-
@property
|
|
51
|
-
@abstractmethod
|
|
52
|
-
def name(self) -> str:
|
|
53
|
-
"""The name of the plugin."""
|
|
54
|
-
raise NotImplementedError() # pragma: no cover
|
|
55
|
-
|
|
56
|
-
@property
|
|
57
|
-
@abstractmethod
|
|
58
|
-
def author(self) -> str:
|
|
59
|
-
"""The author of the plugin. This value will be used in the plugin's unique name."""
|
|
60
|
-
raise NotImplementedError() # pragma: no cover
|
|
61
|
-
|
|
62
|
-
@property
|
|
63
|
-
@abstractmethod
|
|
64
|
-
def description(self) -> str:
|
|
65
|
-
"""The description of the plugin."""
|
|
66
|
-
raise NotImplementedError() # pragma: no cover
|
|
67
|
-
|
|
68
|
-
@property
|
|
69
|
-
@abstractmethod
|
|
70
|
-
def plugin_priority(self) -> int:
|
|
71
|
-
"""The priority of the plugin. Higher values indicate higher priority, which will cause the plugin to appear before other lower priority plugins."""
|
|
72
|
-
raise NotImplementedError() # pragma: no cover
|
|
73
|
-
|
|
74
67
|
@cached_property
|
|
75
68
|
def unique_name(self) -> str:
|
|
76
69
|
"""The unique name of the plugin."""
|
|
77
|
-
return f"{self.
|
|
70
|
+
return f"{self.__class__.AUTHOR}_{self.__class__.NAME}"
|
|
78
71
|
|
|
79
72
|
# ----------------------------------------------------------------------
|
|
80
73
|
def GetNoteSource(
|
|
@@ -89,8 +82,7 @@ class Plugin(ABC):
|
|
|
89
82
|
# ----------------------------------------------------------------------
|
|
90
83
|
@pluggy.HookspecMarker(APP_NAME)
|
|
91
84
|
def GetPlugin(
|
|
92
|
-
|
|
93
|
-
all_plugins_settings: dict[str, object],
|
|
85
|
+
settings: Settings,
|
|
94
86
|
user_experience_info: UserExperienceInfo,
|
|
95
87
|
) -> Plugin:
|
|
96
88
|
"""Return a Plugin instance."""
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from typing import cast, TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
import rtyaml
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from MemoryFrames.PluginInfra.Plugin import Plugin
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ----------------------------------------------------------------------
|
|
13
|
+
class Settings:
|
|
14
|
+
"""Settings used by MemoryFrames and its plugins."""
|
|
15
|
+
|
|
16
|
+
# ----------------------------------------------------------------------
|
|
17
|
+
@classmethod
|
|
18
|
+
def DeserializeOrCreate(
|
|
19
|
+
cls,
|
|
20
|
+
content_dir: Path,
|
|
21
|
+
working_dir: Path,
|
|
22
|
+
*,
|
|
23
|
+
verbose: bool,
|
|
24
|
+
debug: bool,
|
|
25
|
+
) -> Settings:
|
|
26
|
+
"""Deserialize settings from disk or create default settings if necessary."""
|
|
27
|
+
|
|
28
|
+
settings_filename = cls._GetSettingsFilename(working_dir)
|
|
29
|
+
|
|
30
|
+
if settings_filename.is_file():
|
|
31
|
+
with settings_filename.open("r", encoding="utf-8") as f:
|
|
32
|
+
settings = rtyaml.load(f)
|
|
33
|
+
else:
|
|
34
|
+
settings = {}
|
|
35
|
+
|
|
36
|
+
return cls(
|
|
37
|
+
content_dir,
|
|
38
|
+
working_dir,
|
|
39
|
+
cast(dict[str, object], settings),
|
|
40
|
+
verbose=verbose,
|
|
41
|
+
debug=debug,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# ----------------------------------------------------------------------
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
content_dir: Path,
|
|
48
|
+
working_dir: Path,
|
|
49
|
+
settings: dict[str, object],
|
|
50
|
+
*,
|
|
51
|
+
verbose: bool,
|
|
52
|
+
debug: bool,
|
|
53
|
+
) -> None:
|
|
54
|
+
if debug is True:
|
|
55
|
+
verbose = True
|
|
56
|
+
|
|
57
|
+
# Commit results
|
|
58
|
+
self.content_dir = content_dir
|
|
59
|
+
self.working_dir = working_dir
|
|
60
|
+
|
|
61
|
+
self.verbose = verbose
|
|
62
|
+
self.debug = debug
|
|
63
|
+
|
|
64
|
+
self._settings = settings
|
|
65
|
+
|
|
66
|
+
# ----------------------------------------------------------------------
|
|
67
|
+
def Serialize(self) -> None:
|
|
68
|
+
"""Serialize settings to disk."""
|
|
69
|
+
|
|
70
|
+
settings_filename = self._GetSettingsFilename(self.working_dir)
|
|
71
|
+
|
|
72
|
+
with settings_filename.open("w", encoding="utf-8") as f:
|
|
73
|
+
rtyaml.dump(self._settings, f)
|
|
74
|
+
|
|
75
|
+
# ----------------------------------------------------------------------
|
|
76
|
+
def GetPluginSettings(self, plugin: type[Plugin]) -> dict[str, object]:
|
|
77
|
+
"""Return the settings associated with the provided Plugin."""
|
|
78
|
+
|
|
79
|
+
return cast(dict[str, object], self._settings.get(f"{plugin.AUTHOR}_{plugin.NAME}", {}))
|
|
80
|
+
|
|
81
|
+
# ----------------------------------------------------------------------
|
|
82
|
+
# ----------------------------------------------------------------------
|
|
83
|
+
# ----------------------------------------------------------------------
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _GetSettingsFilename(working_dir: Path) -> Path:
|
|
86
|
+
return working_dir / "settings.yaml"
|
{memoryframes-0.4.0 → memoryframes-0.6.0}/src/MemoryFrames/PluginInfra/TextualUserExperienceInfo.py
RENAMED
|
@@ -4,7 +4,7 @@ from MemoryFrames.PluginInfra.UserExperienceInfo import UserExperienceInfo
|
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from textual.app import App
|
|
7
|
-
from textual.containers import
|
|
7
|
+
from textual.containers import VerticalScroll
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
# ----------------------------------------------------------------------
|
|
@@ -15,7 +15,7 @@ class TextualUserExperienceInfo(UserExperienceInfo):
|
|
|
15
15
|
def __init__(
|
|
16
16
|
self,
|
|
17
17
|
app: App,
|
|
18
|
-
hierarchy_container:
|
|
18
|
+
hierarchy_container: VerticalScroll,
|
|
19
19
|
) -> None:
|
|
20
20
|
super().__init__()
|
|
21
21
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from dbrownell_Common.Types import override
|
|
7
|
+
from textual.app import App, ComposeResult
|
|
8
|
+
from textual.containers import Horizontal, VerticalScroll
|
|
9
|
+
from textual.screen import ModalScreen
|
|
10
|
+
from textual.widgets import Footer, Header, Label, LoadingIndicator
|
|
11
|
+
|
|
12
|
+
from MemoryFrames import APP_NAME, __version__
|
|
13
|
+
from MemoryFrames.AppState import AppState, AppStateObserver as AppStateObserverBase
|
|
14
|
+
from MemoryFrames.PluginInfra.TextualUserExperienceInfo import TextualUserExperienceInfo
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from MemoryFrames.PluginInfra.Settings import Settings
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ----------------------------------------------------------------------
|
|
21
|
+
def Execute(settings: Settings) -> None:
|
|
22
|
+
"""Execute the Textual user experience."""
|
|
23
|
+
|
|
24
|
+
_App(settings).run()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ----------------------------------------------------------------------
|
|
28
|
+
# ----------------------------------------------------------------------
|
|
29
|
+
# ----------------------------------------------------------------------
|
|
30
|
+
class _App(App):
|
|
31
|
+
CSS_PATH = Path(__file__).with_suffix(".tcss")
|
|
32
|
+
|
|
33
|
+
# ----------------------------------------------------------------------
|
|
34
|
+
def __init__(self, settings: Settings) -> None:
|
|
35
|
+
super().__init__()
|
|
36
|
+
|
|
37
|
+
self._settings = settings
|
|
38
|
+
|
|
39
|
+
self._user_experience = TextualUserExperienceInfo(
|
|
40
|
+
self.app,
|
|
41
|
+
VerticalScroll(id="hierarchies"),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# The app state is initialized in `on_mount`
|
|
45
|
+
self._app_state: AppState | None = None
|
|
46
|
+
|
|
47
|
+
self.title = "Memory Frames"
|
|
48
|
+
assert self.title.replace(" ", "") == APP_NAME, (self.title, APP_NAME)
|
|
49
|
+
|
|
50
|
+
# ----------------------------------------------------------------------
|
|
51
|
+
def compose(self) -> ComposeResult:
|
|
52
|
+
yield Header()
|
|
53
|
+
|
|
54
|
+
with Horizontal(id="viewport"):
|
|
55
|
+
yield self._user_experience.hierarchy_container
|
|
56
|
+
|
|
57
|
+
with Horizontal(id="footer"):
|
|
58
|
+
yield Footer()
|
|
59
|
+
yield Label(__version__)
|
|
60
|
+
|
|
61
|
+
# ----------------------------------------------------------------------
|
|
62
|
+
def on_mount(self) -> None:
|
|
63
|
+
# ----------------------------------------------------------------------
|
|
64
|
+
def OnLoaded(app_state: AppState | None) -> None:
|
|
65
|
+
if app_state is None:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
assert self._app_state is None
|
|
69
|
+
self._app_state = app_state
|
|
70
|
+
|
|
71
|
+
# ----------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
self.push_screen(
|
|
74
|
+
_LoadingModal(self._settings, self._user_experience),
|
|
75
|
+
OnLoaded,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ----------------------------------------------------------------------
|
|
80
|
+
class _LoadingModal(ModalScreen[AppState]):
|
|
81
|
+
CSS_PATH = Path(__file__).with_suffix(".tcss")
|
|
82
|
+
|
|
83
|
+
# ----------------------------------------------------------------------
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
settings: Settings,
|
|
87
|
+
user_experience_info: TextualUserExperienceInfo,
|
|
88
|
+
) -> None:
|
|
89
|
+
super().__init__()
|
|
90
|
+
|
|
91
|
+
self._settings = settings
|
|
92
|
+
self._user_experience_info = user_experience_info
|
|
93
|
+
|
|
94
|
+
self._status_label = Label("Loading...")
|
|
95
|
+
|
|
96
|
+
# ----------------------------------------------------------------------
|
|
97
|
+
def compose(self) -> ComposeResult:
|
|
98
|
+
yield LoadingIndicator()
|
|
99
|
+
yield self._status_label
|
|
100
|
+
|
|
101
|
+
# ----------------------------------------------------------------------
|
|
102
|
+
def on_mount(self) -> None:
|
|
103
|
+
loading_module = self
|
|
104
|
+
|
|
105
|
+
# ----------------------------------------------------------------------
|
|
106
|
+
def Execute() -> None:
|
|
107
|
+
current_event: AppStateObserverBase.EventType | None = None
|
|
108
|
+
|
|
109
|
+
# ----------------------------------------------------------------------
|
|
110
|
+
class AppStateObserver(AppStateObserverBase):
|
|
111
|
+
# ----------------------------------------------------------------------
|
|
112
|
+
@override
|
|
113
|
+
def OnEvent(self, event_type: AppStateObserver.EventType) -> None:
|
|
114
|
+
nonlocal current_event
|
|
115
|
+
current_event = event_type
|
|
116
|
+
|
|
117
|
+
if event_type == AppStateObserver.EventType.LoadingPlugins:
|
|
118
|
+
msg = "Loading plugins..."
|
|
119
|
+
elif event_type == AppStateObserver.EventType.StartingThreads:
|
|
120
|
+
msg = "Starting threads..."
|
|
121
|
+
else:
|
|
122
|
+
assert False, event_type # noqa: B011, PT015
|
|
123
|
+
|
|
124
|
+
loading_module._status_label.content = msg # noqa: SLF001
|
|
125
|
+
|
|
126
|
+
# ----------------------------------------------------------------------
|
|
127
|
+
@override
|
|
128
|
+
def OnException(self, exception: Exception) -> None:
|
|
129
|
+
if loading_module._settings.debug: # noqa: SLF001
|
|
130
|
+
msg = "".join(traceback.format_exception(exception))
|
|
131
|
+
else:
|
|
132
|
+
msg = str(exception)
|
|
133
|
+
|
|
134
|
+
if current_event == AppStateObserver.EventType.LoadingPlugins:
|
|
135
|
+
loading_module.app.panic(msg)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Display in a toast
|
|
139
|
+
loading_module.notify(msg, severity="error", timeout=10)
|
|
140
|
+
|
|
141
|
+
# ----------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
app_state = AppState.Create(self._settings, self._user_experience_info, AppStateObserver())
|
|
144
|
+
|
|
145
|
+
# Return the results
|
|
146
|
+
self.app.call_from_thread(lambda: self.dismiss(app_state))
|
|
147
|
+
|
|
148
|
+
# ----------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
self.run_worker(Execute, thread=True)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
$version_length: 15;
|
|
2
|
+
|
|
3
|
+
#footer {
|
|
4
|
+
height: 1;
|
|
5
|
+
|
|
6
|
+
Footer {
|
|
7
|
+
padding-right: $version_length;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
Label {
|
|
11
|
+
background: $footer-background;
|
|
12
|
+
dock: right;
|
|
13
|
+
text-align: center;
|
|
14
|
+
width: $version_length;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_LoadingModal {
|
|
19
|
+
align: center middle;
|
|
20
|
+
background: $panel;
|
|
21
|
+
height: 2;
|
|
22
|
+
padding: 1;
|
|
23
|
+
|
|
24
|
+
LoadingIndicator {
|
|
25
|
+
height: 1;
|
|
26
|
+
margin-bottom: 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Label {
|
|
30
|
+
text-align: center;
|
|
31
|
+
width: 100%;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from typer.core import TyperGroup
|
|
10
|
+
|
|
11
|
+
from MemoryFrames import TextualUserExperience
|
|
12
|
+
from MemoryFrames.PluginInfra.Settings import Settings
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ----------------------------------------------------------------------
|
|
16
|
+
class NaturalOrderGrouper(TyperGroup): # noqa: D101
|
|
17
|
+
# ----------------------------------------------------------------------
|
|
18
|
+
def list_commands(self, *args, **kwargs) -> list[str]: # noqa: ARG002, D102
|
|
19
|
+
return list(self.commands.keys()) # pragma: no cover
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ----------------------------------------------------------------------
|
|
23
|
+
app = typer.Typer(
|
|
24
|
+
cls=NaturalOrderGrouper,
|
|
25
|
+
help=__doc__,
|
|
26
|
+
no_args_is_help=True,
|
|
27
|
+
pretty_exceptions_show_locals=False,
|
|
28
|
+
pretty_exceptions_enable=False,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ----------------------------------------------------------------------
|
|
33
|
+
def _GetWorkingDir() -> Path:
|
|
34
|
+
data_dir = Path.home()
|
|
35
|
+
|
|
36
|
+
if os.name == "nt": # noqa: SIM108
|
|
37
|
+
data_dir = data_dir / "AppData" / "Local"
|
|
38
|
+
else:
|
|
39
|
+
data_dir = data_dir / ".local" / "share"
|
|
40
|
+
|
|
41
|
+
data_dir /= "MemoryFrames"
|
|
42
|
+
|
|
43
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
|
|
45
|
+
return data_dir
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ----------------------------------------------------------------------
|
|
49
|
+
class _ExperienceType(str, Enum):
|
|
50
|
+
Textual = "Textual"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ----------------------------------------------------------------------
|
|
54
|
+
@app.command("EntryPoint", no_args_is_help=False)
|
|
55
|
+
def EntryPoint(
|
|
56
|
+
content_dir: Annotated[
|
|
57
|
+
Path,
|
|
58
|
+
typer.Option(
|
|
59
|
+
"--content-dir",
|
|
60
|
+
exists=True,
|
|
61
|
+
file_okay=False,
|
|
62
|
+
help="Directory containing notes to load.",
|
|
63
|
+
resolve_path=True,
|
|
64
|
+
),
|
|
65
|
+
] = Path.cwd(), # noqa: B008
|
|
66
|
+
working_dir: Annotated[
|
|
67
|
+
Path,
|
|
68
|
+
typer.Option(
|
|
69
|
+
"--working-dir",
|
|
70
|
+
file_okay=False,
|
|
71
|
+
help="Directory to use for settings and other persisted data.",
|
|
72
|
+
resolve_path=True,
|
|
73
|
+
),
|
|
74
|
+
] = _GetWorkingDir(), # noqa: B008
|
|
75
|
+
experience: Annotated[
|
|
76
|
+
_ExperienceType,
|
|
77
|
+
typer.Option("--experience", case_sensitive=False, help="The user experience to use."),
|
|
78
|
+
] = _ExperienceType.Textual,
|
|
79
|
+
verbose: Annotated[ # noqa: FBT002
|
|
80
|
+
bool,
|
|
81
|
+
typer.Option("--verbose", help="Write verbose information to the terminal."),
|
|
82
|
+
] = False,
|
|
83
|
+
debug: Annotated[ # noqa: FBT002
|
|
84
|
+
bool,
|
|
85
|
+
typer.Option("--debug", help="Write debug information to the terminal."),
|
|
86
|
+
] = False,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Run MemoryFrames."""
|
|
89
|
+
|
|
90
|
+
content_dir.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
working_dir.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
|
|
93
|
+
settings = Settings.DeserializeOrCreate(content_dir, working_dir, verbose=verbose, debug=debug)
|
|
94
|
+
|
|
95
|
+
if experience == _ExperienceType.Textual:
|
|
96
|
+
user_experience_func = TextualUserExperience.Execute
|
|
97
|
+
else:
|
|
98
|
+
assert False, experience # noqa: B011, PT015 # pragma: no cover
|
|
99
|
+
|
|
100
|
+
user_experience_func(settings)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ----------------------------------------------------------------------
|
|
104
|
+
# ----------------------------------------------------------------------
|
|
105
|
+
# ----------------------------------------------------------------------
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
app() # pragma: no cover
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memoryframes-0.4.0 → memoryframes-0.6.0}/src/MemoryFrames/PluginInfra/UserExperienceInfo.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|