scientia-core 0.1.1__py3-none-any.whl

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.
__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ __author__ = "Scientia Omnibus"
2
+ __email__ = "levmarkpost@gmail.com"
3
+ __version__ = "0.1.0"
4
+ __licence__ = "MIT"
__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from src.app import run
2
+
3
+ if __name__ == "__main__":
4
+ run()
app/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from src.app.app import run
2
+
3
+ __all__ = ["run"]
app/app.py ADDED
@@ -0,0 +1,21 @@
1
+ from textual.app import App
2
+
3
+ from src.data import load_config
4
+ from src.screens import Main
5
+
6
+
7
+ class ScientiaCore(App[None]):
8
+ TITLE = "Scientia Omnibus"
9
+ ENABLE_COMMAND_PALETTE = False
10
+
11
+ def __init__(self) -> None:
12
+ super().__init__()
13
+ self.dark = not load_config().light_mode
14
+
15
+ def on_mount(self) -> None:
16
+ self.theme = "rose-pine"
17
+ self.push_screen(Main())
18
+
19
+
20
+ def run() -> None:
21
+ ScientiaCore().run()
data/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """Provides tools for saving and loading application data."""
2
+
3
+ from src.data.bookmarks import Bookmark, load_bookmarks, save_bookmarks
4
+ from src.data.config import Config, load_config, save_config
5
+ from src.data.history import load_history, save_history
6
+
7
+ __all__ = [
8
+ "Bookmark",
9
+ "Config",
10
+ "load_bookmarks",
11
+ "load_config",
12
+ "load_history",
13
+ "save_bookmarks",
14
+ "save_config",
15
+ "save_history",
16
+ ]
data/bookmarks.py ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from json import JSONEncoder, dumps, loads
4
+ from pathlib import Path
5
+ from typing import Any, NamedTuple
6
+
7
+ from src.data.data_directory import data_directory
8
+
9
+
10
+ class Bookmark(NamedTuple):
11
+ title: str
12
+ location: Path
13
+
14
+
15
+ def bookmarks_file() -> Path:
16
+ return data_directory() / "bookmarks.json"
17
+
18
+
19
+ class BookmarkEncoder(JSONEncoder):
20
+ def default(self, o: object) -> Any:
21
+ return str(o) if isinstance(o, Path) else o
22
+
23
+
24
+ def save_bookmarks(bookmarks: list[Bookmark]) -> None:
25
+ bookmarks_file().write_text(dumps(bookmarks, indent=4, cls=BookmarkEncoder))
26
+
27
+
28
+ def load_bookmarks() -> list[Bookmark]:
29
+ return (
30
+ [
31
+ Bookmark(title, Path(location))
32
+ for (title, location) in loads(bookmarks.read_text())
33
+ ]
34
+ if (bookmarks := bookmarks_file()).exists()
35
+ else []
36
+ )
data/config.py ADDED
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass, field
4
+ from functools import lru_cache
5
+ from json import dumps, loads
6
+ from pathlib import Path
7
+
8
+ from xdg import xdg_config_home
9
+
10
+
11
+ @dataclass
12
+ class Config:
13
+ light_mode: bool = False
14
+ """Should we run in light mode?"""
15
+
16
+ markdown_extensions: list[str] = field(default_factory=lambda: [".md", ".markdown"])
17
+ """What Markdown extensions will we look for?"""
18
+
19
+ navigation_left: bool = True
20
+ """Should navigation be docked to the left side of the screen?"""
21
+
22
+
23
+ def config_file() -> Path:
24
+ (config_dir := xdg_config_home() / "scientia-omnibus").mkdir(
25
+ parents=True, exist_ok=True
26
+ )
27
+ return config_dir / "configuration.json"
28
+
29
+
30
+ def save_config(config: Config) -> Config:
31
+ load_config.cache_clear()
32
+ config_file().write_text(dumps(asdict(config), indent=4))
33
+ return load_config()
34
+
35
+
36
+ @lru_cache(maxsize=None)
37
+ def load_config() -> Config:
38
+ source_file = config_file()
39
+ return (
40
+ Config(**loads(source_file.read_text()))
41
+ if source_file.exists()
42
+ else save_config(Config())
43
+ )
data/data_directory.py ADDED
@@ -0,0 +1,10 @@
1
+ from pathlib import Path
2
+
3
+ from xdg import xdg_data_home
4
+
5
+
6
+ def data_directory() -> Path:
7
+ (target_directory := xdg_data_home() / "scientia-omnibus").mkdir(
8
+ parents=True, exist_ok=True
9
+ )
10
+ return target_directory
data/history.py ADDED
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from json import JSONEncoder, dumps, loads
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from src.data.data_directory import data_directory
8
+
9
+
10
+ def history_file() -> Path:
11
+ return data_directory() / "history.json"
12
+
13
+
14
+ class HistoryEncoder(JSONEncoder):
15
+ def default(self, o: object) -> Any:
16
+ return str(o) if isinstance(o, Path) else o
17
+
18
+
19
+ def save_history(history: list[Path]) -> None:
20
+ history_file().write_text(dumps(history, indent=4, cls=HistoryEncoder))
21
+
22
+
23
+ def load_history() -> list[Path]:
24
+ return (
25
+ [Path(location) for location in loads(history.read_text())]
26
+ if (history := history_file()).exists()
27
+ else []
28
+ )
dialogs/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """Provides useful dialogs for the application."""
2
+
3
+ from src.dialogs.error import ErrorDialog
4
+ from src.dialogs.help_dialog import HelpDialog
5
+ from src.dialogs.information import InformationDialog
6
+ from src.dialogs.input_dialog import InputDialog
7
+ from src.dialogs.yes_no_dialog import YesNoDialog
8
+
9
+ __all__ = [
10
+ "ErrorDialog",
11
+ "InformationDialog",
12
+ "InputDialog",
13
+ "HelpDialog",
14
+ "YesNoDialog",
15
+ ]
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from textual import on
6
+ from textual.app import ComposeResult
7
+ from textual.binding import Binding
8
+ from textual.containers import Horizontal, Vertical
9
+ from textual.screen import ModalScreen
10
+ from textual.widgets import Button, Label, OptionList
11
+
12
+ from src.data.data_directory import data_directory
13
+
14
+
15
+ class DirectoryPicker(ModalScreen[Path]):
16
+ """A modal dialog for selecting a knowledge directory."""
17
+
18
+ DEFAULT_CSS = """
19
+ DirectoryPicker {
20
+ align: center middle;
21
+ }
22
+
23
+ DirectoryPicker > Vertical {
24
+ background: $panel;
25
+ height: auto;
26
+ width: auto;
27
+ border: thick $primary;
28
+ }
29
+
30
+ DirectoryPicker > Vertical > * {
31
+ width: auto;
32
+ height: auto;
33
+ }
34
+
35
+ DirectoryPicker Label {
36
+ margin-left: 2;
37
+ }
38
+
39
+ DirectoryPicker OptionList {
40
+ width: 40;
41
+ margin: 1;
42
+ }
43
+
44
+ DirectoryPicker Button {
45
+ margin-right: 1;
46
+ }
47
+
48
+ DirectoryPicker #buttons {
49
+ width: 100%;
50
+ align-horizontal: right;
51
+ padding-right: 1;
52
+ }
53
+ """
54
+
55
+ BINDINGS = [
56
+ Binding("escape", "app.pop_screen", "", show=False),
57
+ ]
58
+
59
+ def __init__(self) -> None:
60
+ super().__init__()
61
+ self.paths = [item.name for item in data_directory().iterdir() if item.is_dir()]
62
+
63
+ def compose(self) -> ComposeResult:
64
+ with Vertical():
65
+ yield Label("Select from existing:")
66
+ yield OptionList(*self.paths, id="choices")
67
+ with Horizontal(id="buttons"):
68
+ yield Button("Cancel", id="cancel")
69
+ yield Button("OK", id="ok", variant="primary")
70
+
71
+ @on(Button.Pressed, "#cancel")
72
+ def cancel_choice(self) -> None:
73
+ self.app.pop_screen()
74
+
75
+ @on(Button.Pressed, "#ok")
76
+ def accept_choice(self) -> None:
77
+ if self.query_one(OptionList).highlighted is not None:
78
+ self.dismiss(
79
+ data_directory() / self.paths[self.query_one(OptionList).highlighted]
80
+ )
dialogs/error.py ADDED
@@ -0,0 +1,21 @@
1
+ from textual.widgets._button import ButtonVariant
2
+
3
+ from src.dialogs.text_dialog import TextDialog
4
+
5
+
6
+ class ErrorDialog(TextDialog):
7
+ DEFAULT_CSS = """
8
+ ErrorDialog > Vertical {
9
+ background: $error 15%;
10
+ border: thick $error 50%;
11
+ }
12
+
13
+ ErrorDialog #message {
14
+ border-top: solid $panel;
15
+ border-bottom: solid $panel;
16
+ }
17
+ """
18
+
19
+ @property
20
+ def button_style(self) -> ButtonVariant:
21
+ return "error"
dialogs/help_dialog.py ADDED
@@ -0,0 +1,168 @@
1
+ import webbrowser
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.binding import Binding
5
+ from textual.containers import Center, Vertical, VerticalScroll
6
+ from textual.screen import ModalScreen
7
+ from textual.widgets import Button, Markdown
8
+ from typing_extensions import Final
9
+
10
+ from src import __version__
11
+
12
+ HELP: Final[str] = f"""\
13
+ # Scientia Omnibus v{__version__} Help
14
+
15
+ Scientia Omnibus — a terminal program for viewing your knowledge base.
16
+
17
+ ## Interface
18
+
19
+ The application consists of three main zones:
20
+
21
+ - **Omnibox** (top bar) — a command line, like a browser address bar.
22
+ - **Navigation** (sidebar) — four tabs: Contents, Local, Bookmarks, History.
23
+ - **Viewer** (main area) — displays documents.
24
+
25
+ The sidebar can be hidden/shown (`Ctrl+N`), tabs can be switched,
26
+ or the panel can be moved to the opposite side (`\\`).
27
+
28
+ ## Global Keys
29
+
30
+ | Key | Action |
31
+ | -- | -- |
32
+ | `/` or `:` | Focus on omnibox (command line) |
33
+ | `Escape` | Return to omnibox / clear omnibox / exit |
34
+ | `Ctrl+G` | Download or update knowledge base repositories |
35
+ | `Ctrl+O` | Change the search directory |
36
+ | `Ctrl+N` | Show/hide navigation sidebar |
37
+ | `Ctrl+B` | Show bookmarks |
38
+ | `Ctrl+L` | Show local files |
39
+ | `Ctrl+T` | Show document table of contents |
40
+ | `Ctrl+Y` | Show history |
41
+ | `Ctrl+Left` | Go back in viewing history |
42
+ | `Ctrl+Right` | Go forward in viewing history |
43
+ | `Ctrl+D` | Add current document to bookmarks |
44
+ | `Ctrl+R` | Reload current document |
45
+ | `Ctrl+Q` | Quit application |
46
+ | `F1` | This help |
47
+ | `F2` | About |
48
+ | `F10` | Toggle dark/light theme |
49
+
50
+ ## Navigation Panel
51
+
52
+ | Key | Action |
53
+ | -- | -- |
54
+ | `,` / `a` / `h` / `Ctrl+Left` / `Shift+Left` | Previous tab |
55
+ | `.` / `d` / `l` / `Ctrl+Right` / `Shift+Right` | Next tab |
56
+ | `\\` | Move panel left/right |
57
+
58
+ ## Document Viewer
59
+
60
+ When focus is in the viewer:
61
+
62
+ | Key | Action |
63
+ | -- | -- |
64
+ | `w` / `k` | Scroll up |
65
+ | `s` / `j` | Scroll down |
66
+ | `Space` | Page down |
67
+ | `b` | Page up |
68
+
69
+ ## Bookmarks
70
+
71
+ | Key | Action |
72
+ | -- | -- |
73
+ | `Delete` | Delete bookmark |
74
+ | `r` | Rename bookmark |
75
+
76
+ ## History
77
+
78
+ | Key | Action |
79
+ | -- | -- |
80
+ | `Delete` | Delete history entry |
81
+ | `Backspace` | Clear entire history |
82
+
83
+ ## Search
84
+
85
+ As you type in the omnibox, fuzzy search automatically scans the knowledge directory
86
+ for matching `.md` files. Results appear in a dropdown list below the omnibox.
87
+
88
+ - `Down` arrow — move focus to the results list.
89
+ - `Up` arrow (at the top of results) — return focus to the omnibox.
90
+ - `Enter` — open the selected file in the viewer.
91
+ - `Escape` — close the results dropdown.
92
+
93
+ ## Commands
94
+
95
+ Press `/` or click the omnibox, then type one of the commands:
96
+
97
+ | Command | Aliases | Description |
98
+ | -- | -- | -- |
99
+ | `about` | `a` | Show application information |
100
+ | `bookmarks` | `b`, `bm` | Show bookmarks list |
101
+ | `contents` | `c`, `toc` | Show document table of contents |
102
+ | `help` | `?` | Show this help |
103
+ | `history` | `h` | Show history |
104
+ | `local` | `l` | Show local files |
105
+ | `quit` | `q` | Quit the program |
106
+
107
+ ## Knowledge Base Sync
108
+
109
+ `Ctrl+G` opens a dialog to download or update knowledge repositories. Four repositories are available:
110
+
111
+ - **humanities-sciences**
112
+ - **social-sciences**
113
+ - **natural-sciences**
114
+ - **formal-sciences**
115
+
116
+ Select a repository and press `OK`. If the repository already exists locally, it will be
117
+ force-synced — any local changes are overwritten with the remote version. If it does not
118
+ exist, it will be cloned from GitHub (`github.com/Scientia-Omnibus`).
119
+
120
+ After syncing, the file tree navigates to the downloaded repository so you can browse
121
+ its contents immediately.
122
+ """
123
+
124
+
125
+ class HelpDialog(ModalScreen[None]):
126
+ DEFAULT_CSS = """
127
+ HelpDialog {
128
+ align: center middle;
129
+ }
130
+
131
+ HelpDialog > Vertical {
132
+ border: thick $primary 50%;
133
+ width: 80%;
134
+ height: 80%;
135
+ background: $boost;
136
+ }
137
+
138
+ HelpDialog > Vertical > VerticalScroll {
139
+ height: 1fr;
140
+ margin: 1 2;
141
+ }
142
+
143
+ HelpDialog > Vertical > Center {
144
+ padding: 1;
145
+ height: auto;
146
+ }
147
+ """
148
+
149
+ BINDINGS = [
150
+ Binding("escape,f1", "dismiss(None)", "", show=False),
151
+ ]
152
+
153
+ def compose(self) -> ComposeResult:
154
+ with Vertical():
155
+ with VerticalScroll():
156
+ yield Markdown(HELP)
157
+ with Center():
158
+ yield Button("Close", variant="primary")
159
+
160
+ def on_mount(self) -> None:
161
+ self.query_one(Markdown).can_focus_children = False
162
+ self.query_one("Vertical > VerticalScroll").focus()
163
+
164
+ def on_button_pressed(self) -> None:
165
+ self.dismiss(None)
166
+
167
+ def on_markdown_link_clicked(self, event: Markdown.LinkClicked) -> None:
168
+ webbrowser.open(event.href)
dialogs/information.py ADDED
@@ -0,0 +1,9 @@
1
+ from src.dialogs.text_dialog import TextDialog
2
+
3
+
4
+ class InformationDialog(TextDialog):
5
+ DEFAULT_CSS = """
6
+ InformationDialog > Vertical {
7
+ border: thick $primary 50%;
8
+ }
9
+ """
@@ -0,0 +1,88 @@
1
+ """Provides a modal dialog for getting a value from the user."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual import on
6
+ from textual.app import ComposeResult
7
+ from textual.binding import Binding
8
+ from textual.containers import Horizontal, Vertical
9
+ from textual.screen import ModalScreen
10
+ from textual.widgets import Button, Input, Label
11
+
12
+
13
+ class InputDialog(ModalScreen[str]):
14
+ """A modal dialog for getting a single input from the user."""
15
+
16
+ DEFAULT_CSS = """
17
+ InputDialog {
18
+ align: center middle;
19
+ }
20
+
21
+ InputDialog > Vertical {
22
+ background: $panel;
23
+ height: auto;
24
+ width: auto;
25
+ border: thick $primary;
26
+ }
27
+
28
+ InputDialog > Vertical > * {
29
+ width: auto;
30
+ height: auto;
31
+ }
32
+
33
+ InputDialog Input {
34
+ width: 40;
35
+ margin: 1;
36
+ }
37
+
38
+ InputDialog Label {
39
+ margin-left: 2;
40
+ }
41
+
42
+ InputDialog Button {
43
+ margin-right: 1;
44
+ }
45
+
46
+ InputDialog #buttons {
47
+ width: 100%;
48
+ align-horizontal: right;
49
+ padding-right: 1;
50
+ }
51
+ """
52
+
53
+ BINDINGS = [
54
+ Binding("escape", "app.pop_screen", "", show=False),
55
+ ]
56
+
57
+ def __init__(self, prompt: str, initial: str | None = None) -> None:
58
+ """Initialise the input dialog.
59
+
60
+ Args:
61
+ prompt: The prompt for the input.
62
+ initial: The initial value for the input.
63
+ """
64
+ super().__init__()
65
+ self._prompt = prompt
66
+ self._initial = initial
67
+
68
+ def compose(self) -> ComposeResult:
69
+ with Vertical():
70
+ with Vertical(id="input"):
71
+ yield Label(self._prompt)
72
+ yield Input(self._initial or "")
73
+ with Horizontal(id="buttons"):
74
+ yield Button("OK", id="ok", variant="primary")
75
+ yield Button("Cancel", id="cancel")
76
+
77
+ def on_mount(self) -> None:
78
+ self.query_one(Input).focus()
79
+
80
+ @on(Button.Pressed, "#cancel")
81
+ def cancel_input(self) -> None:
82
+ self.app.pop_screen()
83
+
84
+ @on(Input.Submitted)
85
+ @on(Button.Pressed, "#ok")
86
+ def accept_input(self) -> None:
87
+ if value := self.query_one(Input).value.strip():
88
+ self.dismiss(value)
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from textual import on
4
+ from textual.app import ComposeResult
5
+ from textual.binding import Binding
6
+ from textual.containers import Horizontal, Vertical
7
+ from textual.screen import ModalScreen
8
+ from textual.widgets import Button, Label, OptionList
9
+
10
+
11
+ class KnowledgeSync(ModalScreen[str]):
12
+ """A modal dialog for syncing knowledge base repositories."""
13
+
14
+ DEFAULT_CSS = """
15
+ KnowledgeSync {
16
+ align: center middle;
17
+ }
18
+
19
+ KnowledgeSync > Vertical {
20
+ background: $panel;
21
+ height: auto;
22
+ width: auto;
23
+ border: thick $primary;
24
+ }
25
+
26
+ KnowledgeSync > Vertical > * {
27
+ width: auto;
28
+ height: auto;
29
+ }
30
+
31
+ KnowledgeSync Label {
32
+ margin-left: 2;
33
+ }
34
+
35
+ KnowledgeSync OptionList {
36
+ width: 40;
37
+ margin: 1;
38
+ }
39
+
40
+ KnowledgeSync Button {
41
+ margin-right: 1;
42
+ }
43
+
44
+ KnowledgeSync #buttons {
45
+ width: 100%;
46
+ align-horizontal: right;
47
+ padding-right: 1;
48
+ }
49
+ """
50
+
51
+ BINDINGS = [
52
+ Binding("escape", "app.pop_screen", "", show=False),
53
+ ]
54
+
55
+ def __init__(self) -> None:
56
+ super().__init__()
57
+ self.sciences = ["formal-sciences"]
58
+
59
+ def compose(self) -> ComposeResult:
60
+ with Vertical():
61
+ yield Label("Select from existing to update or download:")
62
+ yield OptionList(*self.sciences, id="choices")
63
+ with Horizontal(id="buttons"):
64
+ yield Button("Cancel", id="cancel")
65
+ yield Button("OK", id="ok", variant="primary")
66
+
67
+ @on(Button.Pressed, "#cancel")
68
+ def cancel_choice(self) -> None:
69
+ self.app.pop_screen()
70
+
71
+ @on(Button.Pressed, "#ok")
72
+ def accept_choice(self) -> None:
73
+ option_list = self.query_one(OptionList)
74
+ if (
75
+ highlighted := option_list.highlighted
76
+ ) is not None and 0 <= highlighted < len(self.sciences):
77
+ self.dismiss(self.sciences[highlighted])