libTerm 0.0.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.
- libterm-0.0.1/.idea/inspectionProfiles/profiles_settings.xml +6 -0
- libterm-0.0.1/.idea/libTerm.iml +11 -0
- libterm-0.0.1/.idea/modules.xml +8 -0
- libterm-0.0.1/.idea/vcs.xml +6 -0
- libterm-0.0.1/.idea/workspace.xml +173 -0
- libterm-0.0.1/PKG-INFO +3 -0
- libterm-0.0.1/README.md +186 -0
- libterm-0.0.1/doc/dev/project.md +0 -0
- libterm-0.0.1/pyproject.toml +8 -0
- libterm-0.0.1/setup.cfg +4 -0
- libterm-0.0.1/src/libTerm/__init__.py +2 -0
- libterm-0.0.1/src/libTerm/term/__init__.py +6 -0
- libterm-0.0.1/src/libTerm/term/cursor.py +123 -0
- libterm-0.0.1/src/libTerm/term/posix.py +183 -0
- libterm-0.0.1/src/libTerm/term/types.py +193 -0
- libterm-0.0.1/src/libTerm/term/winnt.py +90 -0
- libterm-0.0.1/src/libTerm.egg-info/PKG-INFO +3 -0
- libterm-0.0.1/src/libTerm.egg-info/SOURCES.txt +20 -0
- libterm-0.0.1/src/libTerm.egg-info/dependency_links.txt +1 -0
- libterm-0.0.1/src/libTerm.egg-info/top_level.txt +1 -0
- libterm-0.0.1/tests/__init__.py +1 -0
- libterm-0.0.1/tests/test.py +2 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="PYTHON_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$">
|
|
5
|
+
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
|
6
|
+
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
|
7
|
+
</content>
|
|
8
|
+
<orderEntry type="jdk" jdkName="Python 3.12 virtualenv at /run/media/jeroen/CCCOMA_X64FRE_EN-GB_DV9/libTerm/.venv" jdkType="Python SDK" />
|
|
9
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
10
|
+
</component>
|
|
11
|
+
</module>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="AutoImportSettings">
|
|
4
|
+
<option name="autoReloadType" value="SELECTIVE" />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="ChangeListManager">
|
|
7
|
+
<list default="true" id="bc73ff2f-338a-4651-a389-91491c2041b6" name="Changes" comment="">
|
|
8
|
+
<change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" afterDir="false" />
|
|
9
|
+
<change afterPath="$PROJECT_DIR$/.idea/libTerm.iml" afterDir="false" />
|
|
10
|
+
<change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
|
|
11
|
+
<change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
|
|
12
|
+
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
|
13
|
+
<change afterPath="$PROJECT_DIR$/doc/dev/project.md" afterDir="false" />
|
|
14
|
+
<change afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
|
|
15
|
+
<change afterPath="$PROJECT_DIR$/src/libTerm/__init__.py" afterDir="false" />
|
|
16
|
+
<change afterPath="$PROJECT_DIR$/src/libTerm/term/__init__.py" afterDir="false" />
|
|
17
|
+
<change afterPath="$PROJECT_DIR$/src/libTerm/term/cursor.py" afterDir="false" />
|
|
18
|
+
<change afterPath="$PROJECT_DIR$/src/libTerm/term/posix.py" afterDir="false" />
|
|
19
|
+
<change afterPath="$PROJECT_DIR$/src/libTerm/term/types.py" afterDir="false" />
|
|
20
|
+
<change afterPath="$PROJECT_DIR$/src/libTerm/term/winnt.py" afterDir="false" />
|
|
21
|
+
<change afterPath="$PROJECT_DIR$/src/test.py" afterDir="false" />
|
|
22
|
+
</list>
|
|
23
|
+
<option name="SHOW_DIALOG" value="false" />
|
|
24
|
+
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
25
|
+
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
26
|
+
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
27
|
+
</component>
|
|
28
|
+
<component name="FileTemplateManagerImpl">
|
|
29
|
+
<option name="RECENT_TEMPLATES">
|
|
30
|
+
<list>
|
|
31
|
+
<option value="Python Script" />
|
|
32
|
+
</list>
|
|
33
|
+
</option>
|
|
34
|
+
</component>
|
|
35
|
+
<component name="Git.Settings">
|
|
36
|
+
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
37
|
+
</component>
|
|
38
|
+
<component name="ProjectColorInfo">{
|
|
39
|
+
"associatedIndex": 1
|
|
40
|
+
}</component>
|
|
41
|
+
<component name="ProjectId" id="32XLRp94VBFj5lgNkdim9ARFbbU" />
|
|
42
|
+
<component name="ProjectLevelVcsManager">
|
|
43
|
+
<ConfirmationsSetting value="2" id="Add" />
|
|
44
|
+
</component>
|
|
45
|
+
<component name="ProjectViewState">
|
|
46
|
+
<option name="hideEmptyMiddlePackages" value="true" />
|
|
47
|
+
<option name="showLibraryContents" value="true" />
|
|
48
|
+
</component>
|
|
49
|
+
<component name="PropertiesComponent"><![CDATA[{
|
|
50
|
+
"keyToString": {
|
|
51
|
+
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
|
52
|
+
"Python.__init__ (1).executor": "Run",
|
|
53
|
+
"Python.__init__.executor": "Run",
|
|
54
|
+
"Python.test.executor": "Run",
|
|
55
|
+
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
56
|
+
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true",
|
|
57
|
+
"RunOnceActivity.git.unshallow": "true",
|
|
58
|
+
"git-widget-placeholder": "master",
|
|
59
|
+
"last_opened_file_path": "/run/media/jeroen/CCCOMA_X64FRE_EN-GB_DV9/libTerm",
|
|
60
|
+
"settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable"
|
|
61
|
+
}
|
|
62
|
+
}]]></component>
|
|
63
|
+
<component name="RecentsManager">
|
|
64
|
+
<key name="CopyFile.RECENT_KEYS">
|
|
65
|
+
<recent name="H:\libTerm" />
|
|
66
|
+
<recent name="H:\libTerm\src\libTerm" />
|
|
67
|
+
</key>
|
|
68
|
+
</component>
|
|
69
|
+
<component name="RunManager" selected="Python.test">
|
|
70
|
+
<configuration name="__init__ (1)" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
|
71
|
+
<module name="libTerm" />
|
|
72
|
+
<option name="ENV_FILES" value="" />
|
|
73
|
+
<option name="INTERPRETER_OPTIONS" value="" />
|
|
74
|
+
<option name="PARENT_ENVS" value="true" />
|
|
75
|
+
<envs>
|
|
76
|
+
<env name="PYTHONUNBUFFERED" value="1" />
|
|
77
|
+
</envs>
|
|
78
|
+
<option name="SDK_HOME" value="" />
|
|
79
|
+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/src/libTerm" />
|
|
80
|
+
<option name="IS_MODULE_SDK" value="true" />
|
|
81
|
+
<option name="ADD_CONTENT_ROOTS" value="true" />
|
|
82
|
+
<option name="ADD_SOURCE_ROOTS" value="true" />
|
|
83
|
+
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/src/libTerm/__init__.py" />
|
|
84
|
+
<option name="PARAMETERS" value="" />
|
|
85
|
+
<option name="SHOW_COMMAND_LINE" value="false" />
|
|
86
|
+
<option name="EMULATE_TERMINAL" value="true" />
|
|
87
|
+
<option name="MODULE_MODE" value="false" />
|
|
88
|
+
<option name="REDIRECT_INPUT" value="false" />
|
|
89
|
+
<option name="INPUT_FILE" value="" />
|
|
90
|
+
<method v="2" />
|
|
91
|
+
</configuration>
|
|
92
|
+
<configuration name="__init__" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
|
93
|
+
<module name="libTerm" />
|
|
94
|
+
<option name="ENV_FILES" value="" />
|
|
95
|
+
<option name="INTERPRETER_OPTIONS" value="" />
|
|
96
|
+
<option name="PARENT_ENVS" value="true" />
|
|
97
|
+
<envs>
|
|
98
|
+
<env name="PYTHONUNBUFFERED" value="1" />
|
|
99
|
+
</envs>
|
|
100
|
+
<option name="SDK_HOME" value="" />
|
|
101
|
+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/src/libTerm/term" />
|
|
102
|
+
<option name="IS_MODULE_SDK" value="true" />
|
|
103
|
+
<option name="ADD_CONTENT_ROOTS" value="true" />
|
|
104
|
+
<option name="ADD_SOURCE_ROOTS" value="true" />
|
|
105
|
+
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/src/libTerm/term/__init__.py" />
|
|
106
|
+
<option name="PARAMETERS" value="" />
|
|
107
|
+
<option name="SHOW_COMMAND_LINE" value="false" />
|
|
108
|
+
<option name="EMULATE_TERMINAL" value="false" />
|
|
109
|
+
<option name="MODULE_MODE" value="false" />
|
|
110
|
+
<option name="REDIRECT_INPUT" value="false" />
|
|
111
|
+
<option name="INPUT_FILE" value="" />
|
|
112
|
+
<method v="2" />
|
|
113
|
+
</configuration>
|
|
114
|
+
<configuration name="test" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
|
115
|
+
<module name="libTerm" />
|
|
116
|
+
<option name="ENV_FILES" value="" />
|
|
117
|
+
<option name="INTERPRETER_OPTIONS" value="" />
|
|
118
|
+
<option name="PARENT_ENVS" value="true" />
|
|
119
|
+
<envs>
|
|
120
|
+
<env name="PYTHONUNBUFFERED" value="1" />
|
|
121
|
+
</envs>
|
|
122
|
+
<option name="SDK_HOME" value="" />
|
|
123
|
+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/src" />
|
|
124
|
+
<option name="IS_MODULE_SDK" value="true" />
|
|
125
|
+
<option name="ADD_CONTENT_ROOTS" value="true" />
|
|
126
|
+
<option name="ADD_SOURCE_ROOTS" value="true" />
|
|
127
|
+
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/src/test.py" />
|
|
128
|
+
<option name="PARAMETERS" value="" />
|
|
129
|
+
<option name="SHOW_COMMAND_LINE" value="false" />
|
|
130
|
+
<option name="EMULATE_TERMINAL" value="true" />
|
|
131
|
+
<option name="MODULE_MODE" value="false" />
|
|
132
|
+
<option name="REDIRECT_INPUT" value="false" />
|
|
133
|
+
<option name="INPUT_FILE" value="" />
|
|
134
|
+
<method v="2" />
|
|
135
|
+
</configuration>
|
|
136
|
+
<recent_temporary>
|
|
137
|
+
<list>
|
|
138
|
+
<item itemvalue="Python.test" />
|
|
139
|
+
<item itemvalue="Python.__init__ (1)" />
|
|
140
|
+
<item itemvalue="Python.__init__" />
|
|
141
|
+
</list>
|
|
142
|
+
</recent_temporary>
|
|
143
|
+
</component>
|
|
144
|
+
<component name="TaskManager">
|
|
145
|
+
<task active="true" id="Default" summary="Default task">
|
|
146
|
+
<changelist id="bc73ff2f-338a-4651-a389-91491c2041b6" name="Changes" comment="" />
|
|
147
|
+
<created>1757561965114</created>
|
|
148
|
+
<option name="number" value="Default" />
|
|
149
|
+
<option name="presentableId" value="Default" />
|
|
150
|
+
<updated>1757561965114</updated>
|
|
151
|
+
</task>
|
|
152
|
+
<servers />
|
|
153
|
+
</component>
|
|
154
|
+
<component name="XDebuggerManager">
|
|
155
|
+
<breakpoint-manager>
|
|
156
|
+
<breakpoints>
|
|
157
|
+
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
|
|
158
|
+
<url>file://$PROJECT_DIR$/src/libTerm/term/posix.py</url>
|
|
159
|
+
<line>1</line>
|
|
160
|
+
<option name="timeStamp" value="1" />
|
|
161
|
+
</line-breakpoint>
|
|
162
|
+
</breakpoints>
|
|
163
|
+
</breakpoint-manager>
|
|
164
|
+
</component>
|
|
165
|
+
<component name="github-copilot-workspace">
|
|
166
|
+
<instructionFileLocations>
|
|
167
|
+
<option value=".github/instructions" />
|
|
168
|
+
</instructionFileLocations>
|
|
169
|
+
<promptFileLocations>
|
|
170
|
+
<option value=".github/prompts" />
|
|
171
|
+
</promptFileLocations>
|
|
172
|
+
</component>
|
|
173
|
+
</project>
|
libterm-0.0.1/PKG-INFO
ADDED
libterm-0.0.1/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# libTerm
|
|
2
|
+
|
|
3
|
+
A small, cross-platform Python utility library for terminal interaction and control.
|
|
4
|
+
|
|
5
|
+
libTerm provides a lightweight abstraction over terminal APIs for POSIX and Windows, exposing helpers for:
|
|
6
|
+
|
|
7
|
+
- querying and tracking terminal size
|
|
8
|
+
- reading and manipulating cursor position
|
|
9
|
+
- discovering terminal color settings
|
|
10
|
+
- entering basic terminal modes (raw/cbreak) on POSIX
|
|
11
|
+
|
|
12
|
+
The project uses a src-layout and aims to be minimal and easy to extend for CLI tools and terminal UI experiments.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Quick links
|
|
17
|
+
|
|
18
|
+
- Source: `src/libTerm`
|
|
19
|
+
- POSIX implementation: `src/libTerm/term/posix.py`
|
|
20
|
+
- Windows implementation: `src/libTerm/term/winnt.py`
|
|
21
|
+
- Types & helpers: `src/libTerm/term/types.py`
|
|
22
|
+
- Cursor helpers: `src/libTerm/term/cursor.py`
|
|
23
|
+
- Tests: `tests/`
|
|
24
|
+
- Packaging metadata: `pyproject.toml`
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Status
|
|
29
|
+
|
|
30
|
+
This library is an early-stage utility package (version 0.0.1 in `pyproject.toml`). Some APIs are partial or defensive and a few implementation stubs exist (see `winnt.py` and `cursor.py`). Use in production after reviewing and adding tests for your use case.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Install (development)
|
|
35
|
+
|
|
36
|
+
From the repository root you can install in editable mode for development:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python -m pip install -e .
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This will make the `libTerm` package importable from your environment.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Basic usage
|
|
47
|
+
|
|
48
|
+
The library exposes a `Term` object that delegates to a platform-specific implementation:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from libTerm import Term
|
|
52
|
+
|
|
53
|
+
term = Term()
|
|
54
|
+
print("size:", term.size.rc) # (cols, rows)
|
|
55
|
+
print("width:", term.size.width)
|
|
56
|
+
print("height:", term.size.height)
|
|
57
|
+
|
|
58
|
+
# Colors and cursor helpers are available under `term.color` and `term.cursor` on supported platforms
|
|
59
|
+
# Example (POSIX):
|
|
60
|
+
print(term.color.fg) # dataclass with RGB values for foreground (may be None on some terminals)
|
|
61
|
+
|
|
62
|
+
# Term modes (POSIX):
|
|
63
|
+
term.mode('ctl') # switch to non-canonical, no-echo mode (POSIX only)
|
|
64
|
+
term.mode('normal')
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Note: Windows and POSIX code paths differ. Some methods are stubs on Windows; inspect `src/libTerm/term/winnt.py` before use.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Project layout and "what goes where"
|
|
72
|
+
|
|
73
|
+
This repository follows a common Python "src" layout. Keep these rules in mind when contributing:
|
|
74
|
+
|
|
75
|
+
- `src/libTerm/` – the runtime package. All new package modules should live under here.
|
|
76
|
+
- `src/libTerm/term/` – terminal-related implementation and platform layers. Add new terminal utilities here.
|
|
77
|
+
- `tests/` – unit and integration tests. Add pytest-compatible tests here. Import the package the same way end-users will: `from libTerm import Term`.
|
|
78
|
+
- `doc/` – longer documentation and design notes. Use `doc/dev/` for contributor-facing docs.
|
|
79
|
+
- `build/` – build artifacts (ignored by VCS for contributors). Do not commit build outputs unless intentional.
|
|
80
|
+
- `pyproject.toml` – project metadata and build-system. This project uses setuptools and setuptools-scm.
|
|
81
|
+
|
|
82
|
+
Files to update when changing public behavior:
|
|
83
|
+
|
|
84
|
+
- `pyproject.toml` — update the version or rely on tags (setuptools-scm).
|
|
85
|
+
- `PKG-INFO` / packaging metadata — will be generated by packaging tools.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Naming conventions & coding practices
|
|
90
|
+
|
|
91
|
+
Follow these conventions to keep the codebase consistent and maintainable:
|
|
92
|
+
|
|
93
|
+
- Project and package name: `libTerm` (package root: `src/libTerm`).
|
|
94
|
+
- Modules should be snake_case (e.g. `cursor.py`, `types.py`, `posix.py`).
|
|
95
|
+
- Classes should use CapWords (PascalCase) and be exported from the package `__init__` only when part of the public API.
|
|
96
|
+
- Prefer small, focused modules. Keep platform-dependent code behind clear boundaries (see `term/__init__.py` which chooses implementation by `os.name`).
|
|
97
|
+
- Use dataclasses for small value types (the codebase already uses `@dataclass`).
|
|
98
|
+
- Avoid reading from stdin/stdout at import time. Prefer lazily reading when a method is called so the package can be imported safely in tests and tooling.
|
|
99
|
+
- Keep side effects (like registering atexit or writing to stdout) in initialization code that is explicit and documented.
|
|
100
|
+
- Add type hints when practical. The codebase mixes dynamic typing and dataclasses; use static typing gradually and keep tests updated.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Tests
|
|
105
|
+
|
|
106
|
+
There is a minimal `tests/test.py` file. Contributing changes should include tests covering:
|
|
107
|
+
|
|
108
|
+
- POSIX behavior (can run on CI runners using linux/mac)
|
|
109
|
+
- Windows behavior (when adding or changing `winnt.py`) — use matrix CI with a Windows runner where possible
|
|
110
|
+
|
|
111
|
+
Run tests locally (recommended to use a virtualenv):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
python -m pip install -U pytest
|
|
115
|
+
python -m pytest -q
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Add tests under `tests/` with clear names and focused assertions. Use small, isolated tests for platform-specific code and avoid relying on interactive stdin in CI.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Development workflow and contributing
|
|
123
|
+
|
|
124
|
+
Suggested developer workflow:
|
|
125
|
+
|
|
126
|
+
1. Fork the repository and create a feature branch from `main` (or master if available): `feature/your-short-description`.
|
|
127
|
+
2. Run the existing tests and linting locally.
|
|
128
|
+
3. Implement your change with small commits and add tests that demonstrate the fix/feature.
|
|
129
|
+
4. Update or add documentation in `doc/` if the public API changes.
|
|
130
|
+
5. Open a Pull Request describing the change, tests, and rationale.
|
|
131
|
+
|
|
132
|
+
Pull request checklist for contributors:
|
|
133
|
+
|
|
134
|
+
- [ ] Tests added/updated and pass locally
|
|
135
|
+
- [ ] Style checks (PEP8) applied
|
|
136
|
+
- [ ] Documentation updated where public behavior changes
|
|
137
|
+
- [ ] CI (if added) passes for target platforms
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Packaging & release
|
|
142
|
+
|
|
143
|
+
This project uses `setuptools` and `setuptools-scm` for versioning. Typical release steps:
|
|
144
|
+
|
|
145
|
+
1. Tag a release in git (for setuptools-scm to pick up version):
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
git tag -a vX.Y.Z -m "Release X.Y.Z"
|
|
149
|
+
git push --tags
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
2. Build a distribution:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
python -m pip install -U build
|
|
156
|
+
python -m build
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
3. Upload to PyPI (if intended):
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
python -m pip install -U twine
|
|
163
|
+
python -m twine upload dist/*
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Add CI workflows for automated builds and tests on push/PRs.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Known gaps & TODOs
|
|
171
|
+
|
|
172
|
+
- Some methods are partial or stubbed (for example `vCursor.__update__` in `cursor.py`). Review and implement or document intended behavior before relying on them.
|
|
173
|
+
- Windows implementation (`winnt.py`) contains simplified code paths and stubs — test on real Windows terminals before assuming parity with POSIX.
|
|
174
|
+
- Interactive stdin parsing in `cursor.py` and `types.Colors._ansiparser_` reads from `sys.stdin` synchronously. This is fragile in automated environments and should be replaced with safer, testable parsers.
|
|
175
|
+
- Add a `LICENSE` file to clarify usage and contribution rights.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Getting help
|
|
180
|
+
|
|
181
|
+
If you need help getting started, open an issue describing the platform and what you tried. Include `python --version` and OS details.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
Thank you for contributing! Your patches, tests and documentation improvements are welcome.
|
|
186
|
+
|
|
File without changes
|
libterm-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
import re
|
|
6
|
+
from libTerm.term.types import Coord
|
|
7
|
+
from time import time_ns
|
|
8
|
+
import sys
|
|
9
|
+
@dataclass()
|
|
10
|
+
class ANSI_Cursor(str, Enum):
|
|
11
|
+
esc = '\x1b'
|
|
12
|
+
q = '6n'
|
|
13
|
+
save = 's'
|
|
14
|
+
load = 'u'
|
|
15
|
+
show = '?25h'
|
|
16
|
+
hide = '?25l'
|
|
17
|
+
|
|
18
|
+
def __str__(self):
|
|
19
|
+
return '{ESC}[{CODE}'.format(ESC=self.ESC,CODE=self.value)
|
|
20
|
+
def __repr__(self):
|
|
21
|
+
return repr(self.value)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Cursor():
|
|
25
|
+
def __init__(__s, term):
|
|
26
|
+
super().__init__()
|
|
27
|
+
__s.term = term
|
|
28
|
+
__s.ansi = ANSI_Cursor
|
|
29
|
+
|
|
30
|
+
__s.re = re.compile(r"^.?\x1b\[(?P<Y>\d*);(?P<X>\d*)R", re.VERBOSE)
|
|
31
|
+
__s.position = __s.__update__
|
|
32
|
+
__s._xy=Coord(0,0)
|
|
33
|
+
__s.XY=Coord(0,0)
|
|
34
|
+
__s.history = [*(None,) * 64]
|
|
35
|
+
__s.init = __s.__update__()
|
|
36
|
+
@property
|
|
37
|
+
def xy(__s):
|
|
38
|
+
__s._xy=__s.__update__()
|
|
39
|
+
return __s._xy
|
|
40
|
+
|
|
41
|
+
def __update__(__s, get='XY'):
|
|
42
|
+
def Parser():
|
|
43
|
+
buf = ' '
|
|
44
|
+
while buf[-1] != "R":
|
|
45
|
+
buf += sys.stdin.read(1)
|
|
46
|
+
# reading the actual values, but what if a keystroke appears while reading
|
|
47
|
+
# from stdin? As dirty work around, getpos() returns if this fails: None
|
|
48
|
+
try:
|
|
49
|
+
groups = __s.re.search(buf).groupdict()
|
|
50
|
+
result = Coord(int(groups['X']), int(groups['Y']))
|
|
51
|
+
except AttributeError:
|
|
52
|
+
result = None
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
result = None
|
|
56
|
+
timeout = {}
|
|
57
|
+
timeout['limit'] = 500
|
|
58
|
+
timeout['start'] = time_ns() // 1e6
|
|
59
|
+
timeout['running'] = 0
|
|
60
|
+
while not result:
|
|
61
|
+
result = __s.term.ansi(''.join([__s.ansi.esc,'[', __s.ansi.q]), Parser)
|
|
62
|
+
__s.XY =result
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
def show(__s, state=True):
|
|
66
|
+
if state:
|
|
67
|
+
print('\x1b[?25h', end='', flush=True)
|
|
68
|
+
else:
|
|
69
|
+
__s.hide()
|
|
70
|
+
|
|
71
|
+
def hide(__s, state=True):
|
|
72
|
+
if state:
|
|
73
|
+
print('\x1b[?25l', end='', flush=True)
|
|
74
|
+
atexit.register(__s.show)
|
|
75
|
+
else:
|
|
76
|
+
__s.show()
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def x(__s):
|
|
80
|
+
__s.X=__s.__update__('X')
|
|
81
|
+
return __s.X
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def y(__s):
|
|
85
|
+
__s.Y=__s.__update__('Y')
|
|
86
|
+
return __s.Y
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class vCursor(Cursor):
|
|
90
|
+
def __init__(__s, term,cursor):
|
|
91
|
+
__s.term = term
|
|
92
|
+
__s.realcursor=cursor
|
|
93
|
+
__s.position = Coord(__s.realcursor.x,__s.realcursor.y)
|
|
94
|
+
__s.history = [*(None,) * 64]
|
|
95
|
+
__s.controled = False
|
|
96
|
+
__s.bound = True
|
|
97
|
+
__s.frozen = False
|
|
98
|
+
__s.init = __s.__update__()
|
|
99
|
+
|
|
100
|
+
def freeze(__s, state=True):
|
|
101
|
+
if state:
|
|
102
|
+
__s.frozen = True
|
|
103
|
+
__s.bind(False)
|
|
104
|
+
__s.control(False)
|
|
105
|
+
else:
|
|
106
|
+
__s.frozen = False
|
|
107
|
+
|
|
108
|
+
def __update__(__s, get='XY'):
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def show(__s, state=True):
|
|
112
|
+
if state:
|
|
113
|
+
print('\x1b[?25h', end='', flush=True)
|
|
114
|
+
else:
|
|
115
|
+
__s.hide()
|
|
116
|
+
|
|
117
|
+
def hide(__s, state=True):
|
|
118
|
+
if state:
|
|
119
|
+
print('\x1b[?25l', end='', flush=True)
|
|
120
|
+
atexit.register(__s.show)
|
|
121
|
+
else:
|
|
122
|
+
__s.show()
|
|
123
|
+
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
import os
|
|
3
|
+
import termios
|
|
4
|
+
import tty
|
|
5
|
+
import atexit
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from time import time_ns
|
|
9
|
+
from shutil import get_terminal_size
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from collections import namedtuple
|
|
13
|
+
from libTerm.term.types import Coord,color, Size,Colors
|
|
14
|
+
from libTerm.term.cursor import Cursor, vCursor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Indices for termios list.
|
|
19
|
+
IFLAG = 0
|
|
20
|
+
OFLAG = 1
|
|
21
|
+
CFLAG = 2
|
|
22
|
+
LFLAG = 3
|
|
23
|
+
ISPEED = 4
|
|
24
|
+
OSPEED = 5
|
|
25
|
+
CC = 6
|
|
26
|
+
TCSAFLUSH = termios.TCSAFLUSH
|
|
27
|
+
ECHO = termios.ECHO
|
|
28
|
+
ICANON = termios.ICANON
|
|
29
|
+
|
|
30
|
+
VMIN = 6
|
|
31
|
+
VTIME = 5
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TermAttrs():
|
|
35
|
+
def __init__(s):
|
|
36
|
+
s.stack=[]
|
|
37
|
+
s.init=None
|
|
38
|
+
s.staged=None
|
|
39
|
+
s.active=None
|
|
40
|
+
def stage(s):
|
|
41
|
+
s.staged=list(s.active)
|
|
42
|
+
def update(s,new=None):
|
|
43
|
+
if new is None:
|
|
44
|
+
new=s.staged
|
|
45
|
+
s.stack+=[list(s.active)]
|
|
46
|
+
s.active=new
|
|
47
|
+
s.staged=None
|
|
48
|
+
def restore(s):
|
|
49
|
+
if s.stack:
|
|
50
|
+
s.staged=s.stack.pop()
|
|
51
|
+
return s.staged
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Term():
|
|
56
|
+
def __init__(s,*a,**k):
|
|
57
|
+
# super().__init__()
|
|
58
|
+
s.pid = os.getpid()
|
|
59
|
+
s.ppid = os.getpid()
|
|
60
|
+
|
|
61
|
+
s.fd = sys.stdin.fileno()
|
|
62
|
+
s.tty = os.ttyname(s.fd)
|
|
63
|
+
|
|
64
|
+
s.TCSAFLUSH = termios.TCSAFLUSH
|
|
65
|
+
s.ECHO = termios.ECHO
|
|
66
|
+
s.ICANON = termios.ICANON
|
|
67
|
+
s.TCSANOW = termios.TCSANOW
|
|
68
|
+
|
|
69
|
+
s.attrs = TermAttrs()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
s.attrs.active = s.tcgetattr()
|
|
73
|
+
s.attrs.init = list([*s.attrs.active])
|
|
74
|
+
s.attrs.stack += [list(s.attrs.active)]
|
|
75
|
+
|
|
76
|
+
s._mode = 0
|
|
77
|
+
s.mode = s.__mode__
|
|
78
|
+
atexit.register(s.mode,'normal')
|
|
79
|
+
s.cursor = Cursor(s)
|
|
80
|
+
s.vcursors = {0:vCursor(s,s.cursor)}
|
|
81
|
+
s.size = Size(parent=s)
|
|
82
|
+
s.color = Colors(parent=s)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def tcgetattr(s):
|
|
86
|
+
return termios.tcgetattr(s.fd)
|
|
87
|
+
|
|
88
|
+
def tcsetattr(s,attr,when=TCSAFLUSH):
|
|
89
|
+
termios.tcsetattr(s.fd,when,attr)
|
|
90
|
+
|
|
91
|
+
def setraw(s, when=TCSAFLUSH):
|
|
92
|
+
"""Put terminal into raw mode."""
|
|
93
|
+
from termios import IGNBRK,BRKINT,IGNPAR,PARMRK,INPCK,ISTRIP,INLCR,IGNCR,ICRNL,IXON,IXANY,IXOFF,OPOST,PARENB,CSIZE,CS8,ECHO,ECHOE,ECHOK,ECHONL,ICANON,IEXTEN,ISIG,NOFLSH,TOSTOP
|
|
94
|
+
s.attrs.stage()
|
|
95
|
+
# Clear all POSIX.1-2017 input mode flags.
|
|
96
|
+
# See chapter 11 "General Terminal Interface"
|
|
97
|
+
# of POSIX.1-2017 Base Definitions.
|
|
98
|
+
s.attrs.staged[IFLAG] &= ~( IGNBRK | BRKINT | IGNPAR | PARMRK | INPCK | ISTRIP | INLCR | IGNCR | ICRNL | IXON
|
|
99
|
+
| IXANY | IXOFF)
|
|
100
|
+
# Do not post-process output.
|
|
101
|
+
s.attrs.staged[OFLAG] &= ~OPOST
|
|
102
|
+
# Disable parity generation and detection; clear character size mask;
|
|
103
|
+
# let character size be 8 bits.
|
|
104
|
+
s.attrs.staged[CFLAG] &= ~(PARENB | CSIZE)
|
|
105
|
+
s.attrs.staged[CFLAG] |= CS8
|
|
106
|
+
# Clear all POSIX.1-2017 local mode flags.
|
|
107
|
+
s.attrs.staged[LFLAG] &= ~(ECHO | ECHOE | ECHOK | ECHONL | ICANON | IEXTEN | ISIG | NOFLSH | TOSTOP)
|
|
108
|
+
# POSIX.1-2017, 11.1.7 Non-Canonical Mode Input Processing,
|
|
109
|
+
# Case B: MIN>0, TIME=0
|
|
110
|
+
# A pending read shall block until MIN (here 1) bytes are received,
|
|
111
|
+
# or a signal is received.
|
|
112
|
+
s.attrs.staged[CC] = list(s.attr.staged[CC])
|
|
113
|
+
s.attrs.staged[CC][VMIN] = 1
|
|
114
|
+
s.attrs.staged[CC][VTIME] = 0
|
|
115
|
+
s.update(when)
|
|
116
|
+
|
|
117
|
+
def setcbreak(s,when=TCSAFLUSH):
|
|
118
|
+
"""Put terminal into cbreak mode."""
|
|
119
|
+
# this code was lifted from the tty module and adapted for being a method
|
|
120
|
+
s.attrs.stage()
|
|
121
|
+
# Do not echo characters; disable canonical input.
|
|
122
|
+
s.attrs.staged[LFLAG] &= ~(ECHO | ICANON)
|
|
123
|
+
# POSIX.1-2017, 11.1.7 Non-Canonical Mode Input Processing,
|
|
124
|
+
# Case B: MIN>0, TIME=0
|
|
125
|
+
# A pending read shall block until MIN (here 1) bytes are received,
|
|
126
|
+
# or a signal is received.
|
|
127
|
+
s.attrs.staged[CC] = list(s.attrs.staged[CC])
|
|
128
|
+
s.attrs.staged[CC][VMIN] = 1
|
|
129
|
+
s.attrs.staged[CC][VTIME] = 0
|
|
130
|
+
s.update(when)
|
|
131
|
+
|
|
132
|
+
def echo(s,enable=False):
|
|
133
|
+
s.attrs.stage()
|
|
134
|
+
s.attrs.staged[3] &= ~s.ECHO
|
|
135
|
+
if enable:
|
|
136
|
+
s.attrs.staged[3] |= s.ECHO
|
|
137
|
+
s.update()
|
|
138
|
+
|
|
139
|
+
def canonical(s,enable):
|
|
140
|
+
s.attrs.stage()
|
|
141
|
+
s.attrs.staged[3] &= ~s.ICANON
|
|
142
|
+
if enable:
|
|
143
|
+
s.attrs.staged[3] |= s.ICANON
|
|
144
|
+
s.update()
|
|
145
|
+
|
|
146
|
+
def __mode__(s,mode=None):
|
|
147
|
+
def Normal():
|
|
148
|
+
# s.cursor.show(True)
|
|
149
|
+
s.echo(True)
|
|
150
|
+
s.canonical(True)
|
|
151
|
+
s.tcsetattr(s.attrs.init)
|
|
152
|
+
s._mode = nmodi.get('normal')
|
|
153
|
+
|
|
154
|
+
def Ctl():
|
|
155
|
+
# s.cursor.show(False)
|
|
156
|
+
s.echo(False)
|
|
157
|
+
s.canonical(False)
|
|
158
|
+
s._mode = nmodi.get('ctl')
|
|
159
|
+
|
|
160
|
+
nmodi={'normal' : 1,'ctl': 2 }
|
|
161
|
+
fmodi = {
|
|
162
|
+
1 : Normal,
|
|
163
|
+
2 : Ctl,
|
|
164
|
+
}
|
|
165
|
+
if mode is not None and mode != s._mode:
|
|
166
|
+
nmode=nmodi.get(mode)
|
|
167
|
+
fmodi.get(nmode)()
|
|
168
|
+
return s._mode
|
|
169
|
+
|
|
170
|
+
def update(s,when=TCSAFLUSH):
|
|
171
|
+
s.tcsetattr( s.attrs.staged,when)
|
|
172
|
+
s.attrs.update(s.tcgetattr())
|
|
173
|
+
|
|
174
|
+
def ansi(s, ansi, parser):
|
|
175
|
+
s.setcbreak()
|
|
176
|
+
try:
|
|
177
|
+
sys.stdout.write(ansi)
|
|
178
|
+
sys.stdout.flush()
|
|
179
|
+
result = parser()
|
|
180
|
+
finally:
|
|
181
|
+
s.tcsetattr(s.attrs.restore())
|
|
182
|
+
return result
|
|
183
|
+
#
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from dataclasses import dataclass,field
|
|
2
|
+
from collections import namedtuple
|
|
3
|
+
from os import get_terminal_size
|
|
4
|
+
from time import sleep, time_ns
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass()
|
|
9
|
+
class Coord(namedtuple('Coord', ['x', 'y'])):
|
|
10
|
+
__module__ = None
|
|
11
|
+
__qualname__='Coord'
|
|
12
|
+
_x: int = field(default=0)
|
|
13
|
+
_y: int = field(default=0)
|
|
14
|
+
|
|
15
|
+
def __str__(__s):
|
|
16
|
+
return f'\x1b[{__s.y + 1};{__s.x + 1}H'
|
|
17
|
+
|
|
18
|
+
def __repr__(s):
|
|
19
|
+
return f"{s.__class__.__name__}({s.x}, {s.y})"
|
|
20
|
+
|
|
21
|
+
def __len__(self):
|
|
22
|
+
return 2
|
|
23
|
+
def __iter__(self):
|
|
24
|
+
yield self.x
|
|
25
|
+
yield self.y
|
|
26
|
+
def __getitem__(s, index):
|
|
27
|
+
if 0 > index > 2:
|
|
28
|
+
raise IndexError("numberpair index out of range")
|
|
29
|
+
return (
|
|
30
|
+
((index == 0)*s.x)+
|
|
31
|
+
((index == 1)*s.y))
|
|
32
|
+
def __add__(s, other):
|
|
33
|
+
if isinstance(other,Coord2D):
|
|
34
|
+
x=s.x+other.x
|
|
35
|
+
y=s.y+other.y
|
|
36
|
+
return Coord2D(x,y)
|
|
37
|
+
elif isinstance(other,complex):
|
|
38
|
+
x=s.x+other.real
|
|
39
|
+
y=s.y+other.imag
|
|
40
|
+
return Coord2D(x,y)
|
|
41
|
+
elif isinstance(other,str):
|
|
42
|
+
return f'{s.__str__()}{other}'
|
|
43
|
+
else:
|
|
44
|
+
raise TypeError(f"cannot add {type(s)} to {type(other)}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def xy(s) -> tuple[int, int]:
|
|
50
|
+
return (s.x, s.y)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def y(s):
|
|
54
|
+
return s._y
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def x(s):
|
|
58
|
+
return s._x
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class color:
|
|
63
|
+
R: int = field(default=0, metadata={"range": (0, 65535)})
|
|
64
|
+
G: int = field(default=0, metadata={"range": (0, 65535)})
|
|
65
|
+
B: int = field(default=0, metadata={"range": (0, 65535)})
|
|
66
|
+
BIT: int = field(default=8, metadata={"set": (4, 8, 16, 32)})
|
|
67
|
+
|
|
68
|
+
def __post_init__(self):
|
|
69
|
+
for attr_name in ("R", "G", "B"):
|
|
70
|
+
value = getattr(self, attr_name)
|
|
71
|
+
if not isinstance(value, int):
|
|
72
|
+
raise ValueError(f"{attr_name.upper()} must be an integer between 0 and 65535. Got {value}.")
|
|
73
|
+
if not isinstance(getattr(self, "BIT"), int):
|
|
74
|
+
raise ValueError(f"{attr_name.upper()} must be one of 4,8,16,32. Got {value}.")
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def RGB(self) -> tuple[int, int, int]:
|
|
78
|
+
return (self.R, self.G, self.B)
|
|
79
|
+
|
|
80
|
+
# @dataclass()
|
|
81
|
+
# class TermCo(namedtuple('Co',['x','y'])):
|
|
82
|
+
# x:int=field(default=0)
|
|
83
|
+
# y:int=field(default=0)
|
|
84
|
+
# class Line(namedtuple('Line',['a','b'])):
|
|
85
|
+
# a:Co=field(default_factory=Co)
|
|
86
|
+
# b:Co=field(default_factory=Co)
|
|
87
|
+
# @classmethod
|
|
88
|
+
# def __add__(s, o):
|
|
89
|
+
# if isinstance(o,Line):
|
|
90
|
+
# if len(s.a)!=0 and len(s.b)!=0:
|
|
91
|
+
# lenx=abs(s.b.x-s.a.x)+abs(o.b.x-o.a.x)
|
|
92
|
+
# leny=abs(s.b.y-s.a.y)+abs(o.b.y-o.a.y)
|
|
93
|
+
# L=Line(s.a,Co(s.a.x+lenx,s.a.y+leny))
|
|
94
|
+
# else:
|
|
95
|
+
# return 0
|
|
96
|
+
#
|
|
97
|
+
# def __len__(s):
|
|
98
|
+
# if len(s.a) != 0 and len(s.b) != 0:
|
|
99
|
+
# lenx = abs(s.b.x - s.a.x) + abs(o.b.x - o.a.x)
|
|
100
|
+
# leny = abs(s.b.y - s.a.y) + abs(o.b.y - o.a.y)
|
|
101
|
+
# return ((lenx**2+leny**2)**(1/2))
|
|
102
|
+
|
|
103
|
+
class Size():
|
|
104
|
+
def __init__(__s, **k):
|
|
105
|
+
__s.parent = k.get('parent')
|
|
106
|
+
__s.getsize = get_terminal_size
|
|
107
|
+
__s.time = None
|
|
108
|
+
__s.last = None
|
|
109
|
+
__s.xy = Coord(1, 1)
|
|
110
|
+
__s._tmp = Coord(1, 1)
|
|
111
|
+
__s.rows = 1
|
|
112
|
+
__s.cols = 1
|
|
113
|
+
|
|
114
|
+
__s.history = []
|
|
115
|
+
__s.changed = False
|
|
116
|
+
__s.changing = False
|
|
117
|
+
|
|
118
|
+
__s.__kwargs__(**k)
|
|
119
|
+
__s.__update__()
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def width(__s):
|
|
123
|
+
__s.__update__()
|
|
124
|
+
return __s.cols
|
|
125
|
+
@property
|
|
126
|
+
def height(__s):
|
|
127
|
+
__s.__update__()
|
|
128
|
+
return __s.rows
|
|
129
|
+
@property
|
|
130
|
+
def rc(__s):
|
|
131
|
+
__s.__update__()
|
|
132
|
+
return (__s.cols, __s.rows)
|
|
133
|
+
|
|
134
|
+
def __kwargs__(__s, **k):
|
|
135
|
+
__s.term = k.get('parent')
|
|
136
|
+
|
|
137
|
+
def __update__(__s):
|
|
138
|
+
if __s.time is None:
|
|
139
|
+
__s.last = time_ns()
|
|
140
|
+
size = Coord(*__s.getsize())
|
|
141
|
+
if size != __s.xy:
|
|
142
|
+
if size != __s._tmp:
|
|
143
|
+
__s.changing = True
|
|
144
|
+
__s._tmp = size
|
|
145
|
+
__s._tmptime = time_ns()
|
|
146
|
+
if size == __s._tmp:
|
|
147
|
+
if (time_ns() - __s._tmptime) * 1e6 > 500:
|
|
148
|
+
__s.changing = False
|
|
149
|
+
__s.changed = True
|
|
150
|
+
__s.history += [__s.xy]
|
|
151
|
+
__s.xy = size
|
|
152
|
+
__s.rows = __s.xy.y
|
|
153
|
+
__s.cols = __s.xy.x
|
|
154
|
+
else:
|
|
155
|
+
__s._tmp = size
|
|
156
|
+
if size == __s.xy:
|
|
157
|
+
__s.changed = False
|
|
158
|
+
|
|
159
|
+
class Colors():
|
|
160
|
+
def __init__(__s, **k):
|
|
161
|
+
__s.parent = None
|
|
162
|
+
__s.specs = {'fg': 10, 'bg': 11}
|
|
163
|
+
__s._ansi = '\x1b]{spec};?\a'
|
|
164
|
+
__s.__kwargs__(**k)
|
|
165
|
+
__s.fg = color(255, 255, 255)
|
|
166
|
+
__s.bg = color(0, 0, 0)
|
|
167
|
+
__s.init = __s.__update__()
|
|
168
|
+
|
|
169
|
+
def __kwargs__(__s, **k):
|
|
170
|
+
__s.term = k.get('parent')
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _ansiparser_():
|
|
174
|
+
buf = ''
|
|
175
|
+
try:
|
|
176
|
+
for i in range(23):
|
|
177
|
+
buf += sys.stdin.read(1)
|
|
178
|
+
rgb = buf.split(':')[1].split('/')
|
|
179
|
+
rgb = [int(i, base=16) for i in rgb]
|
|
180
|
+
rgb = color(*rgb, 16)
|
|
181
|
+
except Exception as E:
|
|
182
|
+
# print(E)
|
|
183
|
+
rgb = None
|
|
184
|
+
return rgb
|
|
185
|
+
|
|
186
|
+
def __update__(__s):
|
|
187
|
+
for ground in __s.specs:
|
|
188
|
+
result = None
|
|
189
|
+
while not result:
|
|
190
|
+
result = __s.term.ansi(__s._ansi.format(spec=__s.specs[ground]), __s._ansiparser_)
|
|
191
|
+
__s.__setattr__(ground, result)
|
|
192
|
+
|
|
193
|
+
return {'fg': __s.fg, 'bg': __s.bg}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import ctypes
|
|
3
|
+
import sys
|
|
4
|
+
import msvcrt
|
|
5
|
+
import atexit
|
|
6
|
+
import struct
|
|
7
|
+
from libTerm.term.types import color
|
|
8
|
+
from libTerm.term.types import Size
|
|
9
|
+
|
|
10
|
+
class Colors:
|
|
11
|
+
def __init__(s,**k):
|
|
12
|
+
s.parent = k.get('parent')
|
|
13
|
+
s.fg = color(255, 255, 255)
|
|
14
|
+
s.bg = color(0, 0, 0)
|
|
15
|
+
s.refresh()
|
|
16
|
+
def refresh(s):
|
|
17
|
+
# Windows API constants
|
|
18
|
+
STD_OUTPUT_HANDLE = -11
|
|
19
|
+
handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
|
|
20
|
+
csbi = ctypes.create_string_buffer(22)
|
|
21
|
+
res = ctypes.windll.kernel32.GetConsoleScreenBufferInfo(handle, csbi)
|
|
22
|
+
if res:
|
|
23
|
+
# Unpack the color attribute as a WORD (2 bytes) at offset 4
|
|
24
|
+
attr = struct.unpack('<H', csbi.raw[4:6])[0]
|
|
25
|
+
s.fg = attr & 0x0F
|
|
26
|
+
s.bg = (attr & 0xF0) >> 4
|
|
27
|
+
s.foreground = s.fg
|
|
28
|
+
s.background = s.bg
|
|
29
|
+
else:
|
|
30
|
+
s.fg = None
|
|
31
|
+
s.bg = None
|
|
32
|
+
s.foreground = None
|
|
33
|
+
s.background = None
|
|
34
|
+
|
|
35
|
+
class Term:
|
|
36
|
+
def __init__(s, *a, **k):
|
|
37
|
+
s.pid = None # Not relevant for Windows terminal
|
|
38
|
+
s.ppid = None
|
|
39
|
+
s.fd = None
|
|
40
|
+
s.tty = None
|
|
41
|
+
s.attrs = None
|
|
42
|
+
s._mode = 0
|
|
43
|
+
s.mode = s.__mode__
|
|
44
|
+
atexit.register(s.mode, 'normal')
|
|
45
|
+
s.cursor = None
|
|
46
|
+
s.vcursors = None
|
|
47
|
+
s.size = Size(parent=s) # Reflects current terminal size
|
|
48
|
+
s.color = Colors(parent=s) # Reflects current terminal colors
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def getch(s):
|
|
52
|
+
"""Read a single character from the terminal."""
|
|
53
|
+
return msvcrt.getch().decode('utf-8', errors='ignore')
|
|
54
|
+
|
|
55
|
+
def kbhit(s):
|
|
56
|
+
"""Check if a keypress is available."""
|
|
57
|
+
return msvcrt.kbhit()
|
|
58
|
+
|
|
59
|
+
def __mode__(s, mode=None):
|
|
60
|
+
nmodi = {'normal': 1, 'ctl': 2}
|
|
61
|
+
if mode is not None and mode != s._mode:
|
|
62
|
+
s._mode = nmodi.get(mode)
|
|
63
|
+
return s._mode
|
|
64
|
+
|
|
65
|
+
def ansi(s, ansi, parser):
|
|
66
|
+
# Parse ANSI color code for foreground (e.g., \x1b[32m)
|
|
67
|
+
import re
|
|
68
|
+
match = re.search(r'\x1b\[(3[0-7])m', ansi)
|
|
69
|
+
if match:
|
|
70
|
+
s.last_ansi_fg = int(match.group(1)[1:]) # 30-37 -> 0-7
|
|
71
|
+
sys.stdout.write(ansi)
|
|
72
|
+
sys.stdout.flush()
|
|
73
|
+
return parser()
|
|
74
|
+
|
|
75
|
+
def refresh(s):
|
|
76
|
+
"""Refresh the size and color settings."""
|
|
77
|
+
s.size.refresh()
|
|
78
|
+
s.color.refresh()
|
|
79
|
+
|
|
80
|
+
# Stub methods for compatibility
|
|
81
|
+
def setraw(s, when=None):
|
|
82
|
+
pass
|
|
83
|
+
def setcbreak(s, when=None):
|
|
84
|
+
pass
|
|
85
|
+
def echo(s, enable=False):
|
|
86
|
+
pass
|
|
87
|
+
def canonical(s, enable):
|
|
88
|
+
pass
|
|
89
|
+
def update(s, when=None):
|
|
90
|
+
pass
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
.idea/libTerm.iml
|
|
4
|
+
.idea/modules.xml
|
|
5
|
+
.idea/vcs.xml
|
|
6
|
+
.idea/workspace.xml
|
|
7
|
+
.idea/inspectionProfiles/profiles_settings.xml
|
|
8
|
+
doc/dev/project.md
|
|
9
|
+
src/libTerm/__init__.py
|
|
10
|
+
src/libTerm.egg-info/PKG-INFO
|
|
11
|
+
src/libTerm.egg-info/SOURCES.txt
|
|
12
|
+
src/libTerm.egg-info/dependency_links.txt
|
|
13
|
+
src/libTerm.egg-info/top_level.txt
|
|
14
|
+
src/libTerm/term/__init__.py
|
|
15
|
+
src/libTerm/term/cursor.py
|
|
16
|
+
src/libTerm/term/posix.py
|
|
17
|
+
src/libTerm/term/types.py
|
|
18
|
+
src/libTerm/term/winnt.py
|
|
19
|
+
tests/__init__.py
|
|
20
|
+
tests/test.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
libTerm
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# /usr/bin/env pyhthon
|