pixpop 0.1.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.
Files changed (113) hide show
  1. pixpop-0.1.0/.github/workflows/docs.yml +31 -0
  2. pixpop-0.1.0/.github/workflows/release.yml +62 -0
  3. pixpop-0.1.0/.gitignore +179 -0
  4. pixpop-0.1.0/LICENSE +21 -0
  5. pixpop-0.1.0/PKG-INFO +142 -0
  6. pixpop-0.1.0/README.md +119 -0
  7. pixpop-0.1.0/pixpop-config-example.toml +19 -0
  8. pixpop-0.1.0/pyproject.toml +112 -0
  9. pixpop-0.1.0/src/assets/adjectives.txt +100 -0
  10. pixpop-0.1.0/src/assets/nouns.txt +100 -0
  11. pixpop-0.1.0/src/assets/palettes/arcade-bright-64.hex +64 -0
  12. pixpop-0.1.0/src/assets/palettes/carnival-32.hex +32 -0
  13. pixpop-0.1.0/src/assets/palettes/catppuccin-mocha-16.hex +16 -0
  14. pixpop-0.1.0/src/assets/palettes/default.hex +32 -0
  15. pixpop-0.1.0/src/assets/palettes/dungeon-mood-64.hex +64 -0
  16. pixpop-0.1.0/src/assets/palettes/gruvbox-dark-16.hex +16 -0
  17. pixpop-0.1.0/src/assets/palettes/hilda-32.hex +32 -0
  18. pixpop-0.1.0/src/assets/palettes/just-parchment-8.hex +8 -0
  19. pixpop-0.1.0/src/assets/palettes/monokai-16.hex +16 -0
  20. pixpop-0.1.0/src/assets/palettes/nord-16.hex +16 -0
  21. pixpop-0.1.0/src/assets/palettes/retro-pastel-16.hex +16 -0
  22. pixpop-0.1.0/src/assets/palettes/solarized-dark-16.hex +16 -0
  23. pixpop-0.1.0/src/assets/palettes/spriteforge-64.hex +64 -0
  24. pixpop-0.1.0/src/assets/palettes/sunset-terminal-16.hex +16 -0
  25. pixpop-0.1.0/src/assets/palettes/terranova-32.hex +32 -0
  26. pixpop-0.1.0/src/assets/palettes/twelve-fallen-leaves-12.hex +12 -0
  27. pixpop-0.1.0/src/assets/palettes/twilight-bog-16.hex +16 -0
  28. pixpop-0.1.0/src/assets/palettes/vibrantskies-48.hex +48 -0
  29. pixpop-0.1.0/src/pixpop/__init__.py +3 -0
  30. pixpop-0.1.0/src/pixpop/__main__.py +5 -0
  31. pixpop-0.1.0/src/pixpop/_io.py +27 -0
  32. pixpop-0.1.0/src/pixpop/app.py +56 -0
  33. pixpop-0.1.0/src/pixpop/canvas.py +831 -0
  34. pixpop-0.1.0/src/pixpop/config.py +119 -0
  35. pixpop-0.1.0/src/pixpop/constants.py +37 -0
  36. pixpop-0.1.0/src/pixpop/export.py +213 -0
  37. pixpop-0.1.0/src/pixpop/importers/__init__.py +6 -0
  38. pixpop-0.1.0/src/pixpop/importers/load.py +150 -0
  39. pixpop-0.1.0/src/pixpop/importers/png.py +86 -0
  40. pixpop-0.1.0/src/pixpop/names.py +67 -0
  41. pixpop-0.1.0/src/pixpop/palettes.py +96 -0
  42. pixpop-0.1.0/src/pixpop/screens/__init__.py +6 -0
  43. pixpop-0.1.0/src/pixpop/screens/help_dialog.py +107 -0
  44. pixpop-0.1.0/src/pixpop/screens/load_file_dialog.py +62 -0
  45. pixpop-0.1.0/src/pixpop/screens/rename_dialog.py +55 -0
  46. pixpop-0.1.0/src/pixpop/screens/save_file_dialog.py +227 -0
  47. pixpop-0.1.0/src/pixpop/session/__init__.py +35 -0
  48. pixpop-0.1.0/src/pixpop/session/exporter.py +159 -0
  49. pixpop-0.1.0/src/pixpop/session/importer.py +208 -0
  50. pixpop-0.1.0/src/pixpop/session/model.py +404 -0
  51. pixpop-0.1.0/src/pixpop/state/__init__.py +23 -0
  52. pixpop-0.1.0/src/pixpop/state/app_state.py +31 -0
  53. pixpop-0.1.0/src/pixpop/state/messages.py +66 -0
  54. pixpop-0.1.0/src/pixpop/state/undo.py +118 -0
  55. pixpop-0.1.0/src/pixpop/styles/main.tcss +254 -0
  56. pixpop-0.1.0/src/pixpop/styles/screens/help_dialog.tcss +32 -0
  57. pixpop-0.1.0/src/pixpop/styles/screens/rename_dialog.tcss +27 -0
  58. pixpop-0.1.0/src/pixpop/tools/__init__.py +45 -0
  59. pixpop-0.1.0/src/pixpop/tools/base.py +184 -0
  60. pixpop-0.1.0/src/pixpop/tools/circle.py +85 -0
  61. pixpop-0.1.0/src/pixpop/tools/continuous_tool.py +33 -0
  62. pixpop-0.1.0/src/pixpop/tools/ellipse.py +107 -0
  63. pixpop-0.1.0/src/pixpop/tools/eraser.py +20 -0
  64. pixpop-0.1.0/src/pixpop/tools/fine_pen.py +33 -0
  65. pixpop-0.1.0/src/pixpop/tools/line.py +66 -0
  66. pixpop-0.1.0/src/pixpop/tools/paint_bucket.py +57 -0
  67. pixpop-0.1.0/src/pixpop/tools/pen.py +17 -0
  68. pixpop-0.1.0/src/pixpop/tools/rectangle.py +45 -0
  69. pixpop-0.1.0/src/pixpop/tools/registry.py +145 -0
  70. pixpop-0.1.0/src/pixpop/tools/shape_tool.py +52 -0
  71. pixpop-0.1.0/src/pixpop/tools/spray.py +116 -0
  72. pixpop-0.1.0/src/pixpop/widgets/__init__.py +33 -0
  73. pixpop-0.1.0/src/pixpop/widgets/brush_picker.py +39 -0
  74. pixpop-0.1.0/src/pixpop/widgets/canvas_info.py +38 -0
  75. pixpop-0.1.0/src/pixpop/widgets/canvas_tabs.py +25 -0
  76. pixpop-0.1.0/src/pixpop/widgets/color_picker.py +248 -0
  77. pixpop-0.1.0/src/pixpop/widgets/flip_picker.py +72 -0
  78. pixpop-0.1.0/src/pixpop/widgets/layer_picker.py +130 -0
  79. pixpop-0.1.0/src/pixpop/widgets/normalize_picker.py +64 -0
  80. pixpop-0.1.0/src/pixpop/widgets/picker_base.py +28 -0
  81. pixpop-0.1.0/src/pixpop/widgets/picker_column.py +44 -0
  82. pixpop-0.1.0/src/pixpop/widgets/selectable_button_base.py +23 -0
  83. pixpop-0.1.0/src/pixpop/widgets/single_select_picker.py +120 -0
  84. pixpop-0.1.0/src/pixpop/widgets/spray_picker.py +39 -0
  85. pixpop-0.1.0/src/pixpop/widgets/tool_picker.py +38 -0
  86. pixpop-0.1.0/src/pixpop/workspace.py +745 -0
  87. pixpop-0.1.0/src/pixpop/workspace_io.py +202 -0
  88. pixpop-0.1.0/src/pixpop/workspace_layers.py +112 -0
  89. pixpop-0.1.0/src/pixpop/workspace_tabs.py +111 -0
  90. pixpop-0.1.0/tests/__init__.py +1 -0
  91. pixpop-0.1.0/tests/__snapshots__/test_ui_canvas/test_canvas_snapshot.svg +340 -0
  92. pixpop-0.1.0/tests/__snapshots__/test_ui_colors/test_color_picker_snapshot.svg +344 -0
  93. pixpop-0.1.0/tests/__snapshots__/test_ui_functional/test_functional_house_drawing_snapshot.svg +347 -0
  94. pixpop-0.1.0/tests/__snapshots__/test_ui_layers/test_layer_visibility_snapshot.1.svg +340 -0
  95. pixpop-0.1.0/tests/__snapshots__/test_ui_layers/test_layer_visibility_snapshot.svg +341 -0
  96. pixpop-0.1.0/tests/__snapshots__/test_ui_tools/test_brush_size_snapshot.svg +341 -0
  97. pixpop-0.1.0/tests/__snapshots__/test_ui_tools/test_circle_tool_snapshot.svg +341 -0
  98. pixpop-0.1.0/tests/__snapshots__/test_ui_tools/test_ellipse_tool_snapshot.svg +341 -0
  99. pixpop-0.1.0/tests/__snapshots__/test_ui_tools/test_fine_pen_tool_snapshot.svg +341 -0
  100. pixpop-0.1.0/tests/__snapshots__/test_ui_tools/test_line_tool_snapshot.svg +341 -0
  101. pixpop-0.1.0/tests/__snapshots__/test_ui_tools/test_pen_tool_snapshot.svg +341 -0
  102. pixpop-0.1.0/tests/__snapshots__/test_ui_tools/test_rectangle_tool_snapshot.svg +341 -0
  103. pixpop-0.1.0/tests/__snapshots__/test_ui_tools/test_spray_tool_snapshot.svg +342 -0
  104. pixpop-0.1.0/tests/conftest.py +38 -0
  105. pixpop-0.1.0/tests/snapshot_helpers.py +95 -0
  106. pixpop-0.1.0/tests/test_config.py +140 -0
  107. pixpop-0.1.0/tests/test_regressions.py +632 -0
  108. pixpop-0.1.0/tests/test_ui_canvas.py +12 -0
  109. pixpop-0.1.0/tests/test_ui_colors.py +44 -0
  110. pixpop-0.1.0/tests/test_ui_functional.py +133 -0
  111. pixpop-0.1.0/tests/test_ui_layers.py +44 -0
  112. pixpop-0.1.0/tests/test_ui_tools.py +214 -0
  113. pixpop-0.1.0/uv.lock +1245 -0
@@ -0,0 +1,31 @@
1
+ name: docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "docs/**"
8
+ - "mkdocs.yml"
9
+ - ".github/workflows/docs.yml"
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: write
14
+
15
+ jobs:
16
+ deploy:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - name: Checkout
20
+ uses: actions/checkout@v5
21
+
22
+ - name: Set up Python
23
+ uses: actions/setup-python@v6
24
+ with:
25
+ python-version: "3.14"
26
+
27
+ - name: Install MkDocs Material
28
+ run: pip install "mkdocs-material>=9.5"
29
+
30
+ - name: Deploy to GitHub Pages
31
+ run: mkdocs gh-deploy --force
@@ -0,0 +1,62 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ jobs:
7
+ build:
8
+ name: Build distributions
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v5
12
+
13
+ - name: Install uv
14
+ uses: astral-sh/setup-uv@v7
15
+ with:
16
+ enable-cache: true
17
+
18
+ - name: Build sdist and wheel
19
+ run: uv build
20
+
21
+ - name: Upload dist artifact
22
+ uses: actions/upload-artifact@v4
23
+ with:
24
+ name: dist
25
+ path: dist/
26
+ if-no-files-found: error
27
+
28
+ publish-testpypi:
29
+ name: Publish to TestPyPI
30
+ needs: build
31
+ runs-on: ubuntu-latest
32
+ environment: testpypi
33
+ permissions:
34
+ id-token: write
35
+ steps:
36
+ - name: Download dist artifact
37
+ uses: actions/download-artifact@v4
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+
42
+ - name: Publish to TestPyPI
43
+ uses: pypa/gh-action-pypi-publish@release/v1
44
+ with:
45
+ repository-url: https://test.pypi.org/legacy/
46
+
47
+ publish-pypi:
48
+ name: Publish to PyPI
49
+ needs: publish-testpypi
50
+ runs-on: ubuntu-latest
51
+ environment: pypi
52
+ permissions:
53
+ id-token: write
54
+ steps:
55
+ - name: Download dist artifact
56
+ uses: actions/download-artifact@v4
57
+ with:
58
+ name: dist
59
+ path: dist/
60
+
61
+ - name: Publish to PyPI
62
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,179 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ env/
126
+ venv/
127
+ ENV/
128
+ env.bak/
129
+ venv.bak/
130
+
131
+ # Spyder project settings
132
+ .spyderproject
133
+ .spyproject
134
+
135
+ # Rope project settings
136
+ .ropeproject
137
+
138
+ # mkdocs documentation
139
+ /site
140
+
141
+ # mypy
142
+ .mypy_cache/
143
+ .dmypy.json
144
+ dmypy.json
145
+
146
+ # Pyre type checker
147
+ .pyre/
148
+
149
+ # pytype static type analyzer
150
+ .pytype/
151
+
152
+ # Cython debug symbols
153
+ cython_debug/
154
+
155
+ # PyCharm
156
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157
+ # be added to the global gitignore or merged into this project gitignore. For a PyCharm
158
+ # project, it is recommended to include the following files in version control:
159
+ # - .idea/modules.xml
160
+ # - .idea/*.iml
161
+ # - .idea/misc.xml
162
+ # - .idea/vcs.xml
163
+ .idea/
164
+
165
+ # VSCode
166
+ .vscode/
167
+ *.code-workspace
168
+
169
+ # uv
170
+ .uv_cache/
171
+
172
+ # Local development
173
+ .DS_Store
174
+ Thumbs.db
175
+ snapshot_report.html
176
+
177
+ # My personal notes
178
+ notes/
179
+ /images/
pixpop-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 umbsublime
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.
pixpop-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.5
2
+ Name: pixpop
3
+ Version: 0.1.0
4
+ Summary: A TUI paint application built with Python and the Textual framework
5
+ Project-URL: Repository, https://github.com/umbsublime/pixpop
6
+ Author: umbsublime
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Multimedia :: Graphics :: Editors
17
+ Requires-Python: >=3.14
18
+ Requires-Dist: pillow>=12.3.0
19
+ Requires-Dist: textual-canvas>=1.1.0
20
+ Requires-Dist: textual-fspicker>=1.0.1
21
+ Requires-Dist: textual>=8.2.8
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Pixpop
25
+
26
+ ![pixpop logo](https://raw.githubusercontent.com/UmBsublime/pixpop/main/screenshots/pixpop_x4.png)
27
+
28
+ A TUI paint application built with Python 3.14 and the
29
+ [Textual](https://textual.textualize.io/) framework.
30
+
31
+ **[Documentation](https://umbsublime.github.io/pixpop/)**
32
+
33
+ ## Features
34
+
35
+ - Multiple canvases
36
+ - Multiple layers
37
+ - Full mouse and keyboard support (drawing is mouse-only)
38
+ - Configuration file for application defaults
39
+ - Save and export as .ans, .png, and .pix session files
40
+ - Open and load .ans, .png, and .pix session files
41
+ - Undo-redo stack
42
+ - Help screen and tooltips
43
+
44
+ ## Caveats
45
+
46
+ This application is designed to draw in the terminal, which comes with limitations.
47
+ We are able to draw `pixels` (half-characters), but there is no way to know if the mouse is positioned
48
+ on the top or bottom pixel. To work around this we have the fine tool.
49
+ It allows choosing the top pixel with left-click, the bottom pixel with right-click, and both pixels with middle-click.
50
+
51
+ ## Screenshots
52
+
53
+ <table>
54
+ <tr>
55
+ <td><img src="https://raw.githubusercontent.com/UmBsublime/pixpop/main/screenshots/app.png" alt="Pixpop main window"></td>
56
+ <td><img src="https://raw.githubusercontent.com/UmBsublime/pixpop/main/screenshots/help.png" alt="Pixpop help dialog"></td>
57
+ </tr>
58
+ </table>
59
+
60
+ ## Pixpop session format (.pix)
61
+
62
+ Pixpop supports saving and loading full sessions as `.pix` files. These are
63
+ gzipped JSON files that capture:
64
+
65
+ - Tabs (canvas names and order)
66
+ - Canvas size per tab
67
+ - Layers, visibility, and pixel data
68
+ - Undo/redo stacks per tab
69
+ - Shared tool state and palette selection
70
+
71
+ The `.pix` format is intended for restoring a working session rather than
72
+ exporting artwork. For artwork exports, use PNG or ASCII.
73
+
74
+
75
+
76
+ ## Setup
77
+
78
+ ```bash
79
+ git clone https://github.com/umbsublime/pixpop.git
80
+ cd pixpop
81
+ uv sync
82
+ uv run pixpop
83
+ ```
84
+
85
+ ## Project structure
86
+
87
+ ```
88
+ pixpop/
89
+ ├── src/pixpop/ # Application package
90
+ │ ├── styles/ # Textual CSS
91
+ │ ├── screens/ # Modal dialogs
92
+ │ ├── state/ # App state, messages, undo/redo
93
+ │ ├── tools/ # Drawing tools and registry
94
+ │ ├── widgets/ # UI pickers (tool/color/brush/spray/layer)
95
+ │ ├── session/ # .pix session save/load
96
+ │ └── importers/ # PNG / PIX / ANSI loaders
97
+ ├── src/assets/ # Palettes and word lists (bundled)
98
+ ├── tests/ # Snapshot + regression tests
99
+ ├── screenshots/ # Project logo and screenshots
100
+ └── pyproject.toml
101
+ ```
102
+
103
+ ## Development
104
+
105
+ ### Code quality (Ruff)
106
+
107
+ ```bash
108
+ # Lint
109
+ uv run ruff check src/
110
+
111
+ # Auto-fix
112
+ uv run ruff check --fix src/
113
+
114
+ # Format
115
+ uv run ruff format src/
116
+ ```
117
+
118
+ ## Testing
119
+
120
+ Snapshot tests cover the UI. Run them with:
121
+
122
+ ```bash
123
+ uv run pytest
124
+ ```
125
+
126
+ Update baselines for intentional UI changes:
127
+
128
+ ```bash
129
+ uv run pytest --snapshot-update
130
+ ```
131
+
132
+ Always verify changes by running the app:
133
+
134
+ ```bash
135
+ uv run pixpop
136
+ ```
137
+
138
+ ## Powered by
139
+
140
+ - [textual](https://github.com/Textualize/textual): TUI framework powering the app shell and widgets.
141
+ - [textual-canvas](https://github.com/davep/textual-canvas): Pixel canvas widget used for drawing operations.
142
+ - [textual-fspicker](https://github.com/davep/textual-fspicker): File picker dialogs for save/load flows.
pixpop-0.1.0/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # Pixpop
2
+
3
+ ![pixpop logo](https://raw.githubusercontent.com/UmBsublime/pixpop/main/screenshots/pixpop_x4.png)
4
+
5
+ A TUI paint application built with Python 3.14 and the
6
+ [Textual](https://textual.textualize.io/) framework.
7
+
8
+ **[Documentation](https://umbsublime.github.io/pixpop/)**
9
+
10
+ ## Features
11
+
12
+ - Multiple canvases
13
+ - Multiple layers
14
+ - Full mouse and keyboard support (drawing is mouse-only)
15
+ - Configuration file for application defaults
16
+ - Save and export as .ans, .png, and .pix session files
17
+ - Open and load .ans, .png, and .pix session files
18
+ - Undo-redo stack
19
+ - Help screen and tooltips
20
+
21
+ ## Caveats
22
+
23
+ This application is designed to draw in the terminal, which comes with limitations.
24
+ We are able to draw `pixels` (half-characters), but there is no way to know if the mouse is positioned
25
+ on the top or bottom pixel. To work around this we have the fine tool.
26
+ It allows choosing the top pixel with left-click, the bottom pixel with right-click, and both pixels with middle-click.
27
+
28
+ ## Screenshots
29
+
30
+ <table>
31
+ <tr>
32
+ <td><img src="https://raw.githubusercontent.com/UmBsublime/pixpop/main/screenshots/app.png" alt="Pixpop main window"></td>
33
+ <td><img src="https://raw.githubusercontent.com/UmBsublime/pixpop/main/screenshots/help.png" alt="Pixpop help dialog"></td>
34
+ </tr>
35
+ </table>
36
+
37
+ ## Pixpop session format (.pix)
38
+
39
+ Pixpop supports saving and loading full sessions as `.pix` files. These are
40
+ gzipped JSON files that capture:
41
+
42
+ - Tabs (canvas names and order)
43
+ - Canvas size per tab
44
+ - Layers, visibility, and pixel data
45
+ - Undo/redo stacks per tab
46
+ - Shared tool state and palette selection
47
+
48
+ The `.pix` format is intended for restoring a working session rather than
49
+ exporting artwork. For artwork exports, use PNG or ASCII.
50
+
51
+
52
+
53
+ ## Setup
54
+
55
+ ```bash
56
+ git clone https://github.com/umbsublime/pixpop.git
57
+ cd pixpop
58
+ uv sync
59
+ uv run pixpop
60
+ ```
61
+
62
+ ## Project structure
63
+
64
+ ```
65
+ pixpop/
66
+ ├── src/pixpop/ # Application package
67
+ │ ├── styles/ # Textual CSS
68
+ │ ├── screens/ # Modal dialogs
69
+ │ ├── state/ # App state, messages, undo/redo
70
+ │ ├── tools/ # Drawing tools and registry
71
+ │ ├── widgets/ # UI pickers (tool/color/brush/spray/layer)
72
+ │ ├── session/ # .pix session save/load
73
+ │ └── importers/ # PNG / PIX / ANSI loaders
74
+ ├── src/assets/ # Palettes and word lists (bundled)
75
+ ├── tests/ # Snapshot + regression tests
76
+ ├── screenshots/ # Project logo and screenshots
77
+ └── pyproject.toml
78
+ ```
79
+
80
+ ## Development
81
+
82
+ ### Code quality (Ruff)
83
+
84
+ ```bash
85
+ # Lint
86
+ uv run ruff check src/
87
+
88
+ # Auto-fix
89
+ uv run ruff check --fix src/
90
+
91
+ # Format
92
+ uv run ruff format src/
93
+ ```
94
+
95
+ ## Testing
96
+
97
+ Snapshot tests cover the UI. Run them with:
98
+
99
+ ```bash
100
+ uv run pytest
101
+ ```
102
+
103
+ Update baselines for intentional UI changes:
104
+
105
+ ```bash
106
+ uv run pytest --snapshot-update
107
+ ```
108
+
109
+ Always verify changes by running the app:
110
+
111
+ ```bash
112
+ uv run pixpop
113
+ ```
114
+
115
+ ## Powered by
116
+
117
+ - [textual](https://github.com/Textualize/textual): TUI framework powering the app shell and widgets.
118
+ - [textual-canvas](https://github.com/davep/textual-canvas): Pixel canvas widget used for drawing operations.
119
+ - [textual-fspicker](https://github.com/davep/textual-fspicker): File picker dialogs for save/load flows.
@@ -0,0 +1,19 @@
1
+ max_layers = 10
2
+ min_canvas_width = 4
3
+ min_canvas_height = 4
4
+ #default_canvas_width = 128
5
+ #default_canvas_height = 128
6
+ #max_canvas_width = 1024
7
+ #max_canvas_height = 1024
8
+ default_palette_name = "default"
9
+ checker_size_width = 8
10
+ checker_size_height = 8
11
+ # Options: solic/checker
12
+ background_mode = "checker"
13
+ # Only works when background_mode=solid
14
+ background_color = "#2b2b2b"
15
+ checker_color_a = "#2b2b2b"
16
+ checker_color_b = "#313131"
17
+ recent_colors_max = 8
18
+ undo_max_entries = 50
19
+ theme_name = "twilight-bog"
@@ -0,0 +1,112 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [tool.hatch.build.targets.sdist]
6
+ exclude = [
7
+ "/.venv",
8
+ "/.venv-linux",
9
+ "/.pytest_cache",
10
+ "/.ruff_cache",
11
+ "/.idea",
12
+ "/snapshot_report.html",
13
+ "/textual.log",
14
+ "/omarchy.ans",
15
+ "/omarchy.pix",
16
+ "/screenshots",
17
+ "/docs",
18
+ "/site",
19
+ "/mkdocs.yml",
20
+ ]
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/pixpop"]
24
+ # Bundle the shared asset files (word lists, palettes) that live at
25
+ # src/assets/ outside the package directory.
26
+ force-include = { "src/assets" = "pixpop/assets" }
27
+ exclude = [
28
+ "/.venv",
29
+ "/.venv-linux",
30
+ "/.pytest_cache",
31
+ "/.ruff_cache",
32
+ "/.idea",
33
+ "/snapshot_report.html",
34
+ "/textual.log",
35
+ ]
36
+
37
+ [project]
38
+ name = "pixpop"
39
+ version = "0.1.0"
40
+ description = "A TUI paint application built with Python and the Textual framework"
41
+ readme = "README.md"
42
+ requires-python = ">=3.14"
43
+ license = {text = "MIT"}
44
+ license-files = ["LICENSE"]
45
+ dependencies = [
46
+ "pillow>=12.3.0",
47
+ "textual-fspicker>=1.0.1",
48
+ "textual>=8.2.8",
49
+ "textual-canvas>=1.1.0",
50
+ ]
51
+ authors = [
52
+ {name = "umbsublime"},
53
+ ]
54
+ classifiers = [
55
+ "Development Status :: 3 - Alpha",
56
+ "Environment :: Console",
57
+ "Intended Audience :: End Users/Desktop",
58
+ "License :: OSI Approved :: MIT License",
59
+ "Programming Language :: Python :: 3",
60
+ "Programming Language :: Python :: 3.14",
61
+ "Programming Language :: Python :: 3 :: Only",
62
+ "Topic :: Multimedia :: Graphics :: Editors",
63
+ ]
64
+
65
+ [project.urls]
66
+ Repository = "https://github.com/umbsublime/pixpop"
67
+
68
+ [dependency-groups]
69
+ dev = [
70
+ "pytest-textual-snapshot>=1.1.0",
71
+ "pytest-xdist>=3.8.0",
72
+ "ruff>=0.16.9",
73
+ "textual-dev>=1.8.0",
74
+ ]
75
+ docs = [
76
+ "mkdocs-material>=9.5",
77
+ ]
78
+
79
+ [project.scripts]
80
+ pixpop = "pixpop.app:main"
81
+
82
+ [tool.ruff]
83
+ target-version = "py314"
84
+ line-length = 88
85
+
86
+ [tool.ruff.lint]
87
+ select = [
88
+ "E", # pycodestyle errors
89
+ "W", # pycodestyle warnings
90
+ "F", # pyflakes
91
+ "I", # isort
92
+ "B", # flake8-bugbear
93
+ ]
94
+ ignore = [
95
+ "E501", # line too long, handled by the formatter
96
+ ]
97
+
98
+ [tool.ruff.format]
99
+ quote-style = "double"
100
+ indent-style = "space"
101
+
102
+ [tool.pytest.ini_options]
103
+ addopts = "-n 4"
104
+ markers = [
105
+ "canvas: canvas UI snapshots",
106
+ "colors: color picker snapshots",
107
+ "functional: multi-step functional scenarios",
108
+ "layers: layer UI snapshots",
109
+ "tools: tool UI snapshots",
110
+ ]
111
+
112
+