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.
@@ -0,0 +1,6 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
@@ -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,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/libTerm.iml" filepath="$PROJECT_DIR$/.idea/libTerm.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
+ </component>
6
+ </project>
@@ -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
+ &quot;associatedIndex&quot;: 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
@@ -0,0 +1,3 @@
1
+ Metadata-Version: 2.4
2
+ Name: libTerm
3
+ Version: 0.0.1
@@ -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
@@ -0,0 +1,8 @@
1
+ #/usr/bin/env python -m setuptools
2
+ [project]
3
+ name = "libTerm"
4
+ version = "0.0.1"
5
+ [build-system]
6
+ requires = ["setuptools", "setuptools-scm"]
7
+ build-backend = "setuptools.build_meta"
8
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ from libTerm.term import Term
2
+
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python
2
+ import os
3
+ if os.name == 'nt':
4
+ from libTerm.term.winnt import Term
5
+ else:
6
+ from libTerm.term.posix import Term
@@ -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,3 @@
1
+ Metadata-Version: 2.4
2
+ Name: libTerm
3
+ Version: 0.0.1
@@ -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
+ libTerm
@@ -0,0 +1 @@
1
+ # /usr/bin/env pyhthon
@@ -0,0 +1,2 @@
1
+ from libTerm import Term
2
+