scientia-core 0.1.1__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.
- scientia_core-0.1.1/LICENSE +21 -0
- scientia_core-0.1.1/PKG-INFO +29 -0
- scientia_core-0.1.1/README.md +52 -0
- scientia_core-0.1.1/pyproject.toml +60 -0
- scientia_core-0.1.1/setup.cfg +4 -0
- scientia_core-0.1.1/src/__init__.py +4 -0
- scientia_core-0.1.1/src/__main__.py +4 -0
- scientia_core-0.1.1/src/app/__init__.py +3 -0
- scientia_core-0.1.1/src/app/app.py +21 -0
- scientia_core-0.1.1/src/data/__init__.py +16 -0
- scientia_core-0.1.1/src/data/bookmarks.py +36 -0
- scientia_core-0.1.1/src/data/config.py +43 -0
- scientia_core-0.1.1/src/data/data_directory.py +10 -0
- scientia_core-0.1.1/src/data/history.py +28 -0
- scientia_core-0.1.1/src/dialogs/__init__.py +15 -0
- scientia_core-0.1.1/src/dialogs/directory_picker.py +80 -0
- scientia_core-0.1.1/src/dialogs/error.py +21 -0
- scientia_core-0.1.1/src/dialogs/help_dialog.py +168 -0
- scientia_core-0.1.1/src/dialogs/information.py +9 -0
- scientia_core-0.1.1/src/dialogs/input_dialog.py +88 -0
- scientia_core-0.1.1/src/dialogs/knowledge_sync.py +77 -0
- scientia_core-0.1.1/src/dialogs/text_dialog.py +70 -0
- scientia_core-0.1.1/src/dialogs/yes_no_dialog.py +99 -0
- scientia_core-0.1.1/src/scientia_core.egg-info/PKG-INFO +29 -0
- scientia_core-0.1.1/src/scientia_core.egg-info/SOURCES.txt +41 -0
- scientia_core-0.1.1/src/scientia_core.egg-info/dependency_links.txt +1 -0
- scientia_core-0.1.1/src/scientia_core.egg-info/entry_points.txt +2 -0
- scientia_core-0.1.1/src/scientia_core.egg-info/requires.txt +6 -0
- scientia_core-0.1.1/src/scientia_core.egg-info/top_level.txt +8 -0
- scientia_core-0.1.1/src/screens/__init__.py +5 -0
- scientia_core-0.1.1/src/screens/main.py +347 -0
- scientia_core-0.1.1/src/utils/__init__.py +5 -0
- scientia_core-0.1.1/src/utils/type_tests.py +21 -0
- scientia_core-0.1.1/src/widgets/__init__.py +7 -0
- scientia_core-0.1.1/src/widgets/navigation.py +215 -0
- scientia_core-0.1.1/src/widgets/navigation_panes/__init__.py +13 -0
- scientia_core-0.1.1/src/widgets/navigation_panes/bookmarks.py +116 -0
- scientia_core-0.1.1/src/widgets/navigation_panes/history.py +112 -0
- scientia_core-0.1.1/src/widgets/navigation_panes/local_files.py +66 -0
- scientia_core-0.1.1/src/widgets/navigation_panes/navigation_pane.py +13 -0
- scientia_core-0.1.1/src/widgets/navigation_panes/table_of_contents.py +47 -0
- scientia_core-0.1.1/src/widgets/omnibox.py +140 -0
- scientia_core-0.1.1/src/widgets/viewer.py +183 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Scientia Omnibus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scientia-core
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Scientia Omnibus -- a knowledge base viewer
|
|
5
|
+
Author-email: MarkLevkovich <levmarkpost@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Scientia-Omnibus/scientia-core
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: Intended Audience :: Information Technology
|
|
13
|
+
Classifier: Intended Audience :: Other Audience
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 10
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 11
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: gitpython>=3.1.50
|
|
24
|
+
Requires-Dist: rapidfuzz>=3.14.5
|
|
25
|
+
Requires-Dist: ruff>=0.15.17
|
|
26
|
+
Requires-Dist: textual[syntax]>=8.2.7
|
|
27
|
+
Requires-Dist: typing-extensions>=4.5
|
|
28
|
+
Requires-Dist: xdg<7,>=6
|
|
29
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# scientia-core
|
|
2
|
+
|
|
3
|
+
Offline terminal knowledge reader. Download Markdown knowledge packs once, read without internet. Built with [Textual](https://textual.textualize.io/).
|
|
4
|
+
|
|
5
|
+
Ships under 10 MB. Runs on anything with a terminal — from a $5 Raspberry Pi to a 15-year-old netbook.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# via uv (recommended)
|
|
13
|
+
uv tool install scientia-core
|
|
14
|
+
|
|
15
|
+
# via pip
|
|
16
|
+
pip install scientia-core
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then run:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
scientia-core
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> For keybindings and commands, see **[USAGE.md](USAGE.md)**.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- **Fully offline** — download once, read anywhere, no network required
|
|
30
|
+
- **Keyboard-driven** — omnibox (`/` or `:`), tab navigation, commands
|
|
31
|
+
- **4 navigation tabs** — Contents, Local Files, Bookmarks, History
|
|
32
|
+
- **Fuzzy search** across all document titles in your knowledge pack
|
|
33
|
+
- **GitHub sync** — pull knowledge base updates with `Ctrl+G`
|
|
34
|
+
- **Dark / light themes** — toggle with `F10`
|
|
35
|
+
|
|
36
|
+
## Tech
|
|
37
|
+
|
|
38
|
+
- Python 3.12+, [Textual](https://textual.textualize.io/) TUI framework
|
|
39
|
+
- [rapidfuzz](https://github.com/maxbachmann/RapidFuzz) for fuzzy matching
|
|
40
|
+
- [GitPython](https://github.com/gitpython-developers/GitPython) for sync
|
|
41
|
+
|
|
42
|
+
## TODO
|
|
43
|
+
|
|
44
|
+
- [ ] BM25 full-text search across document contents
|
|
45
|
+
- [ ] Auto-update mechanism
|
|
46
|
+
- [ ] Standalone Windows package (`.exe`)
|
|
47
|
+
- [ ] Linux AppImage build
|
|
48
|
+
- [ ] More knowledge pack sources
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scientia-core"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Scientia Omnibus -- a knowledge base viewer"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "MarkLevkovich", email = "levmarkpost@gmail.com" }
|
|
11
|
+
]
|
|
12
|
+
license = { text = "MIT" }
|
|
13
|
+
requires-python = ">=3.12"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: End Users/Desktop",
|
|
19
|
+
"Intended Audience :: Information Technology",
|
|
20
|
+
"Intended Audience :: Other Audience",
|
|
21
|
+
"Operating System :: MacOS",
|
|
22
|
+
"Operating System :: Microsoft :: Windows :: Windows 10",
|
|
23
|
+
"Operating System :: Microsoft :: Windows :: Windows 11",
|
|
24
|
+
"Operating System :: POSIX :: Linux",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Software Development :: Documentation"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
dependencies = [
|
|
31
|
+
"gitpython>=3.1.50",
|
|
32
|
+
"rapidfuzz>=3.14.5",
|
|
33
|
+
"ruff>=0.15.17",
|
|
34
|
+
"textual[syntax]>=8.2.7",
|
|
35
|
+
"typing-extensions>=4.5",
|
|
36
|
+
"xdg>=6,<7",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/Scientia-Omnibus/scientia-core"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
scientia-core = "src.app.app:run"
|
|
44
|
+
|
|
45
|
+
[tool.uv]
|
|
46
|
+
package = true
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
target-version = "py312"
|
|
50
|
+
line-length = 88
|
|
51
|
+
exclude = [
|
|
52
|
+
".git",
|
|
53
|
+
".venv",
|
|
54
|
+
"build",
|
|
55
|
+
"dist",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.ruff.format]
|
|
59
|
+
quote-style = "double"
|
|
60
|
+
indent-style = "space"
|
|
@@ -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()
|
|
@@ -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
|
+
]
|
|
@@ -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
|
+
)
|
|
@@ -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
|
+
)
|
|
@@ -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
|
+
)
|
|
@@ -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
|
+
)
|
|
@@ -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"
|
|
@@ -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)
|