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 +4 -0
- __main__.py +4 -0
- app/__init__.py +3 -0
- app/app.py +21 -0
- data/__init__.py +16 -0
- data/bookmarks.py +36 -0
- data/config.py +43 -0
- data/data_directory.py +10 -0
- data/history.py +28 -0
- dialogs/__init__.py +15 -0
- dialogs/directory_picker.py +80 -0
- dialogs/error.py +21 -0
- dialogs/help_dialog.py +168 -0
- dialogs/information.py +9 -0
- dialogs/input_dialog.py +88 -0
- dialogs/knowledge_sync.py +77 -0
- dialogs/text_dialog.py +70 -0
- dialogs/yes_no_dialog.py +99 -0
- scientia_core-0.1.1.dist-info/METADATA +29 -0
- scientia_core-0.1.1.dist-info/RECORD +38 -0
- scientia_core-0.1.1.dist-info/WHEEL +5 -0
- scientia_core-0.1.1.dist-info/entry_points.txt +2 -0
- scientia_core-0.1.1.dist-info/licenses/LICENSE +21 -0
- scientia_core-0.1.1.dist-info/top_level.txt +8 -0
- screens/__init__.py +5 -0
- screens/main.py +347 -0
- utils/__init__.py +5 -0
- utils/type_tests.py +21 -0
- widgets/__init__.py +7 -0
- widgets/navigation.py +215 -0
- widgets/navigation_panes/__init__.py +13 -0
- widgets/navigation_panes/bookmarks.py +116 -0
- widgets/navigation_panes/history.py +112 -0
- widgets/navigation_panes/local_files.py +66 -0
- widgets/navigation_panes/navigation_pane.py +13 -0
- widgets/navigation_panes/table_of_contents.py +47 -0
- widgets/omnibox.py +140 -0
- widgets/viewer.py +183 -0
__init__.py
ADDED
__main__.py
ADDED
app/__init__.py
ADDED
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
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
dialogs/input_dialog.py
ADDED
|
@@ -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])
|