prosaic-app 0.1.3__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.
Files changed (30) hide show
  1. prosaic_app-0.1.3/LICENSE +21 -0
  2. prosaic_app-0.1.3/PKG-INFO +168 -0
  3. prosaic_app-0.1.3/README.md +136 -0
  4. prosaic_app-0.1.3/prosaic/__init__.py +3 -0
  5. prosaic_app-0.1.3/prosaic/__main__.py +149 -0
  6. prosaic_app-0.1.3/prosaic/app.py +283 -0
  7. prosaic_app-0.1.3/prosaic/config.py +114 -0
  8. prosaic_app-0.1.3/prosaic/core/__init__.py +17 -0
  9. prosaic_app-0.1.3/prosaic/core/markdown.py +78 -0
  10. prosaic_app-0.1.3/prosaic/core/metrics.py +89 -0
  11. prosaic_app-0.1.3/prosaic/screens/__init__.py +6 -0
  12. prosaic_app-0.1.3/prosaic/screens/dashboard.py +166 -0
  13. prosaic_app-0.1.3/prosaic/screens/editor.py +243 -0
  14. prosaic_app-0.1.3/prosaic/themes/__init__.py +16 -0
  15. prosaic_app-0.1.3/prosaic/themes/dark.tcss +558 -0
  16. prosaic_app-0.1.3/prosaic/themes/light.tcss +562 -0
  17. prosaic_app-0.1.3/prosaic/widgets/__init__.py +8 -0
  18. prosaic_app-0.1.3/prosaic/widgets/file_tree.py +55 -0
  19. prosaic_app-0.1.3/prosaic/widgets/outline.py +47 -0
  20. prosaic_app-0.1.3/prosaic/widgets/spell_text_area.py +204 -0
  21. prosaic_app-0.1.3/prosaic/widgets/statusbar.py +122 -0
  22. prosaic_app-0.1.3/prosaic/wizard.py +135 -0
  23. prosaic_app-0.1.3/prosaic_app.egg-info/PKG-INFO +168 -0
  24. prosaic_app-0.1.3/prosaic_app.egg-info/SOURCES.txt +28 -0
  25. prosaic_app-0.1.3/prosaic_app.egg-info/dependency_links.txt +1 -0
  26. prosaic_app-0.1.3/prosaic_app.egg-info/entry_points.txt +2 -0
  27. prosaic_app-0.1.3/prosaic_app.egg-info/requires.txt +12 -0
  28. prosaic_app-0.1.3/prosaic_app.egg-info/top_level.txt +5 -0
  29. prosaic_app-0.1.3/pyproject.toml +54 -0
  30. prosaic_app-0.1.3/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Deepansh Khurana
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,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: prosaic-app
3
+ Version: 0.1.3
4
+ Summary: A writer-first terminal writing app
5
+ Author: Deepansh Khurana
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/deepanshkhurana/prosaic
8
+ Project-URL: Repository, https://github.com/deepanshkhurana/prosaic
9
+ Keywords: writing,terminal,markdown,tui
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Text Editors
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: textual[syntax]>=0.50.0
22
+ Requires-Dist: click>=8.1.0
23
+ Requires-Dist: platformdirs>=4.0.0
24
+ Requires-Dist: gitpython>=3.1.0
25
+ Requires-Dist: pyspellchecker>=0.8.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
28
+ Requires-Dist: textual-dev>=1.0.0; extra == "dev"
29
+ Provides-Extra: grammar
30
+ Requires-Dist: language-tool-python>=2.8.0; extra == "grammar"
31
+ Dynamic: license-file
32
+
33
+ # Prosaic
34
+
35
+ A writer-first terminal writing app built with Python and Textual, built with the assistance of LLM/Copilot tools.
36
+
37
+ ## Motivation
38
+
39
+ I write on multiple devices (an iPad, a laptop, even more), and I wanted a quick way to start every day, get some frontmatter ready for my daily pieces on [journal.coffee](https://journal.coffee), jot down notes and even work on books. I have tried several software, some stellar, some half-assed, some bloated, some minimal, but I felt like I needed something of my own.
40
+
41
+ So, I decided that the best way to go about it is to have a TUI. I can access it from anywhere, and mostly, it will do the job. This way, I can access it from a terminal app on the iPad or Windows (cue: Termix on a browser) and on the Macbook. Hence, Prosaic was born.
42
+
43
+ **Full disclosure:** I did rely on LLMs to make it, but as much as I could, I tried to get it to follow best practices, good architecture, and clean code principles.
44
+
45
+ ## Publish with Ode
46
+
47
+ Looking somewhere to publish your writing that is philosophically compatible with Prosaic? Check out [Ode](https://ode.dimwit.me/). You can also go to the [GitHub](https://github.com/DeepanshKhurana/ode/) directly.
48
+
49
+ > Ode is for writers who want to publish in an aesthetically pleasing website, ignoring the bells and whistles of the modern internet. It is opinionated, minimal, and easy to use, guided by an [Ethos](https://docs.ode.dimwit.me/ethos) that prioritizes the craft of writing and the joy of reading over metrics and engagement.
50
+
51
+ ## Screenshots
52
+
53
+ | | |
54
+ |---|---|
55
+ | ![](media/light.png) | ![](media/dark.png) |
56
+ | ![](media/light2.png) | ![](media/dark2.png) |
57
+ | ![](media/light3.png) | ![](media/dark3.png) |
58
+
59
+ ## Installation
60
+
61
+ ```bash
62
+ # Install (requires Python 3.11+)
63
+ pipx install prosaic
64
+
65
+ # Run (first launch runs setup wizard)
66
+ prosaic
67
+
68
+ # Re-run setup wizard anytime
69
+ prosaic --setup
70
+ ```
71
+
72
+ ## Features
73
+
74
+ - **Markdown-first**: Live outline, word counting
75
+ - **Focus mode**: Hide everything except your writing
76
+ - **Reader mode**: Distraction-free reading
77
+ - **Continue writing**: Resume your last edited document
78
+ - **Daily metrics**: Track words and characters written each day
79
+ - **Git-ready**: Archive is Git-initialized for versioning
80
+
81
+ ## Keybindings
82
+
83
+ | Category | Key | Action |
84
+ |----------|-----|--------|
85
+ | Dashboard | `c` | Continue writing (if last file exists) |
86
+ | Dashboard | `p` | Write a piece |
87
+ | Dashboard | `b` | Work on a book |
88
+ | Dashboard | `n` | Add a note |
89
+ | Dashboard | `r` | Read notes |
90
+ | Dashboard | `f` | Find files |
91
+ | Dashboard | `?` | Help |
92
+ | Dashboard | `q` | Quit |
93
+ | Editor | `Ctrl+e` | Toggle file tree |
94
+ | Editor | `Ctrl+o` | Toggle outline |
95
+ | Editor | `Ctrl+s` | Save |
96
+ | Editor | `Ctrl+q` | Go home |
97
+ | Editor | `F5` | Focus mode |
98
+ | Editor | `F6` | Reader mode |
99
+ | Writing | `Ctrl+z` | Undo |
100
+ | Writing | `Ctrl+y` | Redo |
101
+ | Writing | `Ctrl+x` | Cut |
102
+ | Writing | `Ctrl+c` | Copy |
103
+ | Writing | `Ctrl+v` | Paste |
104
+ | Writing | `Ctrl+a` | Select all |
105
+
106
+ ## Themes
107
+
108
+ - **Prosaic Light** (default): Warm white background with brick accents
109
+ - **Prosaic Dark**: Deep charcoal with warm tan accents
110
+
111
+ ```bash
112
+ # Light mode (default)
113
+ prosaic
114
+
115
+ # Dark mode
116
+ prosaic --dark
117
+ ```
118
+
119
+ ## Configuration
120
+
121
+ Config location (in order of priority):
122
+ 1. `PROSAIC_CONFIG_DIR` env var (explicit override)
123
+ 2. `$XDG_CONFIG_HOME/prosaic/` (Linux standard)
124
+ 3. `~/.config/prosaic/` (default)
125
+
126
+ Override with environment variable:
127
+
128
+ ```bash
129
+ PROSAIC_CONFIG_DIR=~/custom/path prosaic
130
+ ```
131
+
132
+ ### Git Integration
133
+
134
+ If your chosen archive directory already contains a git repository, the wizard will:
135
+
136
+ - Detect the existing `.git` directory
137
+ - Inherit the repository (no re-initialization)
138
+ - Read the remote URL if configured
139
+ - Prompt for a remote URL if none exists
140
+ - Store this info in `settings.json`
141
+
142
+ Example `settings.json`:
143
+
144
+ ```json
145
+ {
146
+ "setup_complete": true,
147
+ "archive_dir": "/Users/you/Prosaic",
148
+ "init_git": true,
149
+ "git_remote": "git@github.com:you/writing.git",
150
+ "git_inherited": true,
151
+ "last_file": "/Users/you/Prosaic/pieces/2026-02-21-example.md"
152
+ }
153
+ ```
154
+
155
+ ## Archive Structure
156
+
157
+ ```
158
+ ~/Prosaic/ # Default archive (configurable)
159
+ pieces/ # Pieces with preloaded markdown frontmatter
160
+ books/ # Long-form projects with Outline already open
161
+ notes.md # Quick notes with auto date headers
162
+ metrics.json # Daily statistics for archival and display
163
+ .git/ # Version control
164
+ ```
165
+
166
+ ## License
167
+
168
+ MIT
@@ -0,0 +1,136 @@
1
+ # Prosaic
2
+
3
+ A writer-first terminal writing app built with Python and Textual, built with the assistance of LLM/Copilot tools.
4
+
5
+ ## Motivation
6
+
7
+ I write on multiple devices (an iPad, a laptop, even more), and I wanted a quick way to start every day, get some frontmatter ready for my daily pieces on [journal.coffee](https://journal.coffee), jot down notes and even work on books. I have tried several software, some stellar, some half-assed, some bloated, some minimal, but I felt like I needed something of my own.
8
+
9
+ So, I decided that the best way to go about it is to have a TUI. I can access it from anywhere, and mostly, it will do the job. This way, I can access it from a terminal app on the iPad or Windows (cue: Termix on a browser) and on the Macbook. Hence, Prosaic was born.
10
+
11
+ **Full disclosure:** I did rely on LLMs to make it, but as much as I could, I tried to get it to follow best practices, good architecture, and clean code principles.
12
+
13
+ ## Publish with Ode
14
+
15
+ Looking somewhere to publish your writing that is philosophically compatible with Prosaic? Check out [Ode](https://ode.dimwit.me/). You can also go to the [GitHub](https://github.com/DeepanshKhurana/ode/) directly.
16
+
17
+ > Ode is for writers who want to publish in an aesthetically pleasing website, ignoring the bells and whistles of the modern internet. It is opinionated, minimal, and easy to use, guided by an [Ethos](https://docs.ode.dimwit.me/ethos) that prioritizes the craft of writing and the joy of reading over metrics and engagement.
18
+
19
+ ## Screenshots
20
+
21
+ | | |
22
+ |---|---|
23
+ | ![](media/light.png) | ![](media/dark.png) |
24
+ | ![](media/light2.png) | ![](media/dark2.png) |
25
+ | ![](media/light3.png) | ![](media/dark3.png) |
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ # Install (requires Python 3.11+)
31
+ pipx install prosaic
32
+
33
+ # Run (first launch runs setup wizard)
34
+ prosaic
35
+
36
+ # Re-run setup wizard anytime
37
+ prosaic --setup
38
+ ```
39
+
40
+ ## Features
41
+
42
+ - **Markdown-first**: Live outline, word counting
43
+ - **Focus mode**: Hide everything except your writing
44
+ - **Reader mode**: Distraction-free reading
45
+ - **Continue writing**: Resume your last edited document
46
+ - **Daily metrics**: Track words and characters written each day
47
+ - **Git-ready**: Archive is Git-initialized for versioning
48
+
49
+ ## Keybindings
50
+
51
+ | Category | Key | Action |
52
+ |----------|-----|--------|
53
+ | Dashboard | `c` | Continue writing (if last file exists) |
54
+ | Dashboard | `p` | Write a piece |
55
+ | Dashboard | `b` | Work on a book |
56
+ | Dashboard | `n` | Add a note |
57
+ | Dashboard | `r` | Read notes |
58
+ | Dashboard | `f` | Find files |
59
+ | Dashboard | `?` | Help |
60
+ | Dashboard | `q` | Quit |
61
+ | Editor | `Ctrl+e` | Toggle file tree |
62
+ | Editor | `Ctrl+o` | Toggle outline |
63
+ | Editor | `Ctrl+s` | Save |
64
+ | Editor | `Ctrl+q` | Go home |
65
+ | Editor | `F5` | Focus mode |
66
+ | Editor | `F6` | Reader mode |
67
+ | Writing | `Ctrl+z` | Undo |
68
+ | Writing | `Ctrl+y` | Redo |
69
+ | Writing | `Ctrl+x` | Cut |
70
+ | Writing | `Ctrl+c` | Copy |
71
+ | Writing | `Ctrl+v` | Paste |
72
+ | Writing | `Ctrl+a` | Select all |
73
+
74
+ ## Themes
75
+
76
+ - **Prosaic Light** (default): Warm white background with brick accents
77
+ - **Prosaic Dark**: Deep charcoal with warm tan accents
78
+
79
+ ```bash
80
+ # Light mode (default)
81
+ prosaic
82
+
83
+ # Dark mode
84
+ prosaic --dark
85
+ ```
86
+
87
+ ## Configuration
88
+
89
+ Config location (in order of priority):
90
+ 1. `PROSAIC_CONFIG_DIR` env var (explicit override)
91
+ 2. `$XDG_CONFIG_HOME/prosaic/` (Linux standard)
92
+ 3. `~/.config/prosaic/` (default)
93
+
94
+ Override with environment variable:
95
+
96
+ ```bash
97
+ PROSAIC_CONFIG_DIR=~/custom/path prosaic
98
+ ```
99
+
100
+ ### Git Integration
101
+
102
+ If your chosen archive directory already contains a git repository, the wizard will:
103
+
104
+ - Detect the existing `.git` directory
105
+ - Inherit the repository (no re-initialization)
106
+ - Read the remote URL if configured
107
+ - Prompt for a remote URL if none exists
108
+ - Store this info in `settings.json`
109
+
110
+ Example `settings.json`:
111
+
112
+ ```json
113
+ {
114
+ "setup_complete": true,
115
+ "archive_dir": "/Users/you/Prosaic",
116
+ "init_git": true,
117
+ "git_remote": "git@github.com:you/writing.git",
118
+ "git_inherited": true,
119
+ "last_file": "/Users/you/Prosaic/pieces/2026-02-21-example.md"
120
+ }
121
+ ```
122
+
123
+ ## Archive Structure
124
+
125
+ ```
126
+ ~/Prosaic/ # Default archive (configurable)
127
+ pieces/ # Pieces with preloaded markdown frontmatter
128
+ books/ # Long-form projects with Outline already open
129
+ notes.md # Quick notes with auto date headers
130
+ metrics.json # Daily statistics for archival and display
131
+ .git/ # Version control
132
+ ```
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,3 @@
1
+ """Prosaic - A writer-first terminal writing app."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,149 @@
1
+ """Entry point for Prosaic."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+ from textual.app import App
7
+ from textual.binding import Binding
8
+
9
+ from prosaic.app import HelpScreen, NewBookModal, NewPieceModal
10
+ from prosaic.config import (
11
+ ensure_workspace,
12
+ get_notes_path,
13
+ get_workspace_dir,
14
+ save_config,
15
+ set_last_file,
16
+ )
17
+ from prosaic.core.metrics import MetricsTracker
18
+ from prosaic.screens import DashboardScreen, EditorScreen
19
+ from prosaic.themes import PROSAIC_DARK_CSS, PROSAIC_LIGHT_CSS
20
+ from prosaic.widgets import SpellCheckTextArea
21
+ from prosaic.wizard import needs_setup, run_setup, setup_workspace
22
+
23
+
24
+ class ProsaicApp(App):
25
+ """Main Prosaic application."""
26
+
27
+ TITLE = "prosaic"
28
+ CSS = PROSAIC_LIGHT_CSS
29
+ BINDINGS = [
30
+ Binding("ctrl+q", "smart_quit", "quit", show=False, priority=True),
31
+ ]
32
+
33
+ def __init__(
34
+ self,
35
+ light_mode: bool = True,
36
+ initial_file: Path | None = None,
37
+ ) -> None:
38
+ super().__init__()
39
+ self.light_mode = light_mode
40
+ self.initial_file = initial_file
41
+ self.notes_path = get_notes_path()
42
+ ProsaicApp.CSS = PROSAIC_LIGHT_CSS if light_mode else PROSAIC_DARK_CSS
43
+
44
+ def on_mount(self) -> None:
45
+ ensure_workspace()
46
+ self.metrics = MetricsTracker(get_workspace_dir())
47
+ self.install_screen(DashboardScreen(self.metrics), name="dashboard")
48
+
49
+ if self.initial_file:
50
+ self._open_editor(self.initial_file)
51
+ else:
52
+ self.push_screen("dashboard")
53
+
54
+ def _open_editor(self, file_path: Path | None = None, show_all_panes: bool = False) -> None:
55
+ if file_path:
56
+ set_last_file(file_path)
57
+
58
+ self.push_screen(
59
+ EditorScreen(
60
+ self.metrics,
61
+ initial_file=file_path,
62
+ light_mode=self.light_mode,
63
+ show_all_panes=show_all_panes,
64
+ )
65
+ )
66
+
67
+ def _open_notes(self) -> None:
68
+ self.push_screen(
69
+ EditorScreen(
70
+ self.metrics,
71
+ initial_file=self.notes_path,
72
+ light_mode=self.light_mode,
73
+ add_note=True,
74
+ )
75
+ )
76
+
77
+ def _open_notes_readonly(self) -> None:
78
+ self.push_screen(
79
+ EditorScreen(
80
+ self.metrics,
81
+ initial_file=self.notes_path,
82
+ light_mode=self.light_mode,
83
+ reader_mode_initial=True,
84
+ )
85
+ )
86
+
87
+ def _handle_new_piece(self, result: Path | None) -> None:
88
+ if result:
89
+ self._open_editor(result)
90
+
91
+ def _handle_new_book(self, result: Path | None) -> None:
92
+ if result:
93
+ self._open_editor(result)
94
+
95
+ def action_new_piece(self) -> None:
96
+ self.push_screen(NewPieceModal(), callback=self._handle_new_piece)
97
+
98
+ def action_new_book(self) -> None:
99
+ self.push_screen(NewBookModal(), callback=self._handle_new_book)
100
+
101
+ def toggle_theme(self) -> None:
102
+ self.light_mode = not self.light_mode
103
+ ProsaicApp.CSS = PROSAIC_LIGHT_CSS if self.light_mode else PROSAIC_DARK_CSS
104
+ self.refresh_css(animate=False)
105
+
106
+ try:
107
+ screen = self.screen
108
+ if isinstance(screen, EditorScreen):
109
+ ta = screen.query_one("#editor", SpellCheckTextArea)
110
+ ta.theme = "prosaic_light" if self.light_mode else "prosaic_dark"
111
+ ta._build_highlight_map()
112
+ except Exception:
113
+ pass
114
+
115
+ async def action_quit(self) -> None:
116
+ self.exit()
117
+
118
+ def action_smart_quit(self) -> None:
119
+ screen = self.screen
120
+ if isinstance(screen, EditorScreen):
121
+ screen.action_go_home()
122
+ elif isinstance(screen, DashboardScreen):
123
+ self.exit()
124
+ elif len(self.screen_stack) > 1:
125
+ self.pop_screen()
126
+ else:
127
+ self.exit()
128
+
129
+
130
+ @click.command()
131
+ @click.option("--light/--dark", default=True, help="Use light or dark theme")
132
+ @click.option("--setup", is_flag=True, help="Run setup wizard again")
133
+ @click.argument("file", required=False, type=click.Path())
134
+ def main(light: bool, setup: bool, file: str | None) -> None:
135
+ """Prosaic - A writer-first terminal writing app."""
136
+ if setup or needs_setup():
137
+ config = run_setup()
138
+ save_config(config)
139
+ setup_workspace(config)
140
+
141
+ app = ProsaicApp(
142
+ light_mode=light,
143
+ initial_file=Path(file) if file else None,
144
+ )
145
+ app.run()
146
+
147
+
148
+ if __name__ == "__main__":
149
+ main()