ft-ps-visu 1.0.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.
- ft_ps_visu-1.0.0/LICENSE +21 -0
- ft_ps_visu-1.0.0/PKG-INFO +240 -0
- ft_ps_visu-1.0.0/README.md +213 -0
- ft_ps_visu-1.0.0/ft_ps_visu/__init__.py +3 -0
- ft_ps_visu-1.0.0/ft_ps_visu/__main__.py +4 -0
- ft_ps_visu-1.0.0/ft_ps_visu/cli.py +536 -0
- ft_ps_visu-1.0.0/ft_ps_visu.egg-info/PKG-INFO +240 -0
- ft_ps_visu-1.0.0/ft_ps_visu.egg-info/SOURCES.txt +11 -0
- ft_ps_visu-1.0.0/ft_ps_visu.egg-info/dependency_links.txt +1 -0
- ft_ps_visu-1.0.0/ft_ps_visu.egg-info/entry_points.txt +2 -0
- ft_ps_visu-1.0.0/ft_ps_visu.egg-info/top_level.txt +1 -0
- ft_ps_visu-1.0.0/pyproject.toml +37 -0
- ft_ps_visu-1.0.0/setup.cfg +4 -0
ft_ps_visu-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Italo Almeida
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ft_ps_visu
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A terminal visualizer for the 42 push_swap project with real-time TUI, controlled disorder generation, and interactive playback.
|
|
5
|
+
Author: Italo Almeida
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/italoalmeida0/ft_ps_visu
|
|
8
|
+
Project-URL: Issues, https://github.com/italoalmeida0/ft_ps_visu/issues
|
|
9
|
+
Keywords: 42,push_swap,visualizer,terminal,tui,sorting,algorithm
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Education :: Testing
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Classifier: Topic :: Terminals
|
|
23
|
+
Requires-Python: >=3.7
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# ft_ps_visu
|
|
29
|
+
|
|
30
|
+
A terminal visualizer for the **42 push_swap** project. It generates controlled random sequences with specific disorder levels, runs your `push_swap` executable, and renders a real-time TUI (Terminal User Interface) with interactive playback controls.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
<img src="i1.png" width="500">
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- **Real-time TUI visualization** — watch your `push_swap` algorithm sort stacks live in the terminal.
|
|
39
|
+
- **Controlled disorder generation** — creates sequences with precise inversion percentages.
|
|
40
|
+
- **Four test modes** — adaptive, simple, medium, and complex.
|
|
41
|
+
- **Interactive controls** — play/pause, forward/reverse, step-by-step, speed adjustment, and more.
|
|
42
|
+
- **True-color bars** — gradient-colored bars representing values for easy visual tracking.
|
|
43
|
+
- **Responsive layout** — adapts to terminal resize events.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- Python 3.7+
|
|
50
|
+
- A compiled `push_swap` executable that accepts a `--<mode>` flag and the numbers to sort
|
|
51
|
+
- A terminal that supports:
|
|
52
|
+
- ANSI escape codes
|
|
53
|
+
- True-color (24-bit RGB) for the best experience
|
|
54
|
+
- Alternate screen buffer (`\033[?1049h` / `\033[?1049l`)
|
|
55
|
+
|
|
56
|
+
> **Note:** On Windows, use Windows Terminal, WSL, or any modern terminal emulator. The classic `cmd.exe` console may not render Unicode block characters or true-color correctly.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
> **Tip:** If you run into installation errors, try updating `pip` first:
|
|
63
|
+
> ```bash
|
|
64
|
+
> pip install --upgrade pip
|
|
65
|
+
> # or
|
|
66
|
+
> pip3 install --upgrade pip
|
|
67
|
+
> ```
|
|
68
|
+
|
|
69
|
+
### Option 1: Install from PyPI (recommended)
|
|
70
|
+
|
|
71
|
+
Using `pip`:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install ft_ps_visu
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Using `pip3`:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip3 install ft_ps_visu
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
User-local install (no sudo required — `pip`):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install --user ft_ps_visu
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Using `python3 -m pip`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python3 -m pip install ft_ps_visu
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> **Note:** When using `--user`, the `ft_ps_visu` binary is installed to a user-local `bin/` directory. Make sure this directory is on your `PATH`, or use the `python3 -m` execution methods shown below.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### Option 2: Install from source
|
|
100
|
+
|
|
101
|
+
Clone this repository:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
git clone https://github.com/italoalmeida0/ft_ps_visu.git
|
|
105
|
+
cd ft_ps_visu
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Editable / development mode (`pip`):
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pip install -e .
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Normal install (`pip`):
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install .
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### Option 3: Run with `pipx` (isolated, no install required)
|
|
123
|
+
|
|
124
|
+
If you have [`pipx`](https://pypa.github.io/pipx/) installed, you can run the visualizer directly without permanently installing it:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pipx run ft_ps_visu ./push_swap
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Or install it into an isolated environment:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
pipx install ft_ps_visu
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Then run normally:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
ft_ps_visu ./push_swap
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
Make sure your `push_swap` binary is compiled and executable:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
make
|
|
148
|
+
chmod +x push_swap
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Usage
|
|
154
|
+
|
|
155
|
+
### Basic usage
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
ft_ps_visu ./push_swap
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
With custom number of elements:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
ft_ps_visu ./push_swap 100
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
With custom disorder percentage (0–55):
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
ft_ps_visu ./push_swap 500 30
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
If `ft_ps_visu` is **not** found on your `PATH`, run via the module:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
python3 -m ft_ps_visu ./push_swap
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Or using the `.cli` submodule directly:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
python3 -m ft_ps_visu.cli ./push_swap
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
From the cloned source directory (no install required):
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
python3 ft_ps_visu/cli.py ./push_swap
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Controls
|
|
194
|
+
|
|
195
|
+
| Key | Action |
|
|
196
|
+
|-----|--------|
|
|
197
|
+
| `P` | Play / Pause |
|
|
198
|
+
| `O` | Toggle Forward / Reverse direction |
|
|
199
|
+
| `N` | Next step (forward one operation) |
|
|
200
|
+
| `B` | Back step (reverse one operation) |
|
|
201
|
+
| `X` | Increase speed |
|
|
202
|
+
| `Z` | Decrease speed |
|
|
203
|
+
| `G` | Re-generate data with current settings |
|
|
204
|
+
| `M` | Cycle through modes (adaptive → simple → medium → complex) |
|
|
205
|
+
| `A` | Decrease number of elements |
|
|
206
|
+
| `S` | Increase number of elements |
|
|
207
|
+
| `D` | Decrease disorder percentage |
|
|
208
|
+
| `F` | Increase disorder percentage |
|
|
209
|
+
| `Q` | Quit |
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Modes / Flags
|
|
214
|
+
|
|
215
|
+
Your `push_swap` must support the following flags (passed as `--<mode>` before the numbers):
|
|
216
|
+
|
|
217
|
+
| Mode | Disorder range | Description |
|
|
218
|
+
|------------|----------------|------------------------------------------|
|
|
219
|
+
| `simple` | 15.0% – 19.9% | Nearly sorted sequences |
|
|
220
|
+
| `medium` | 20.0% – 49.9% | Moderately shuffled sequences |
|
|
221
|
+
| `complex` | 50.0% – 55.0% | Heavily shuffled sequences |
|
|
222
|
+
| `adaptive` | 15.0% – 55.0% | Random disorder across the full spectrum |
|
|
223
|
+
|
|
224
|
+
> **Note:** If your `push_swap` does **not** implement these flags, the visualizer will still work if your program ignores unknown flags and simply sorts the provided numbers.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## How it works
|
|
229
|
+
|
|
230
|
+
1. **Generate** a random sequence with the desired size and disorder level.
|
|
231
|
+
2. **Run** your `push_swap` executable with the sequence.
|
|
232
|
+
3. **Capture** the operations printed to `stdout`.
|
|
233
|
+
4. **Render** a TUI showing both stacks as colored bars.
|
|
234
|
+
5. **Animate** the operations at the chosen speed, allowing forward and reverse playback.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# ft_ps_visu
|
|
2
|
+
|
|
3
|
+
A terminal visualizer for the **42 push_swap** project. It generates controlled random sequences with specific disorder levels, runs your `push_swap` executable, and renders a real-time TUI (Terminal User Interface) with interactive playback controls.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
<img src="i1.png" width="500">
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Real-time TUI visualization** — watch your `push_swap` algorithm sort stacks live in the terminal.
|
|
12
|
+
- **Controlled disorder generation** — creates sequences with precise inversion percentages.
|
|
13
|
+
- **Four test modes** — adaptive, simple, medium, and complex.
|
|
14
|
+
- **Interactive controls** — play/pause, forward/reverse, step-by-step, speed adjustment, and more.
|
|
15
|
+
- **True-color bars** — gradient-colored bars representing values for easy visual tracking.
|
|
16
|
+
- **Responsive layout** — adapts to terminal resize events.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Python 3.7+
|
|
23
|
+
- A compiled `push_swap` executable that accepts a `--<mode>` flag and the numbers to sort
|
|
24
|
+
- A terminal that supports:
|
|
25
|
+
- ANSI escape codes
|
|
26
|
+
- True-color (24-bit RGB) for the best experience
|
|
27
|
+
- Alternate screen buffer (`\033[?1049h` / `\033[?1049l`)
|
|
28
|
+
|
|
29
|
+
> **Note:** On Windows, use Windows Terminal, WSL, or any modern terminal emulator. The classic `cmd.exe` console may not render Unicode block characters or true-color correctly.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
> **Tip:** If you run into installation errors, try updating `pip` first:
|
|
36
|
+
> ```bash
|
|
37
|
+
> pip install --upgrade pip
|
|
38
|
+
> # or
|
|
39
|
+
> pip3 install --upgrade pip
|
|
40
|
+
> ```
|
|
41
|
+
|
|
42
|
+
### Option 1: Install from PyPI (recommended)
|
|
43
|
+
|
|
44
|
+
Using `pip`:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install ft_ps_visu
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Using `pip3`:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip3 install ft_ps_visu
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
User-local install (no sudo required — `pip`):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install --user ft_ps_visu
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Using `python3 -m pip`:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python3 -m pip install ft_ps_visu
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> **Note:** When using `--user`, the `ft_ps_visu` binary is installed to a user-local `bin/` directory. Make sure this directory is on your `PATH`, or use the `python3 -m` execution methods shown below.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### Option 2: Install from source
|
|
73
|
+
|
|
74
|
+
Clone this repository:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git clone https://github.com/italoalmeida0/ft_ps_visu.git
|
|
78
|
+
cd ft_ps_visu
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Editable / development mode (`pip`):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install -e .
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Normal install (`pip`):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pip install .
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### Option 3: Run with `pipx` (isolated, no install required)
|
|
96
|
+
|
|
97
|
+
If you have [`pipx`](https://pypa.github.io/pipx/) installed, you can run the visualizer directly without permanently installing it:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pipx run ft_ps_visu ./push_swap
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Or install it into an isolated environment:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pipx install ft_ps_visu
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Then run normally:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
ft_ps_visu ./push_swap
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
Make sure your `push_swap` binary is compiled and executable:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
make
|
|
121
|
+
chmod +x push_swap
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Usage
|
|
127
|
+
|
|
128
|
+
### Basic usage
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
ft_ps_visu ./push_swap
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
With custom number of elements:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
ft_ps_visu ./push_swap 100
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
With custom disorder percentage (0–55):
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
ft_ps_visu ./push_swap 500 30
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
If `ft_ps_visu` is **not** found on your `PATH`, run via the module:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
python3 -m ft_ps_visu ./push_swap
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Or using the `.cli` submodule directly:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
python3 -m ft_ps_visu.cli ./push_swap
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
From the cloned source directory (no install required):
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
python3 ft_ps_visu/cli.py ./push_swap
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Controls
|
|
167
|
+
|
|
168
|
+
| Key | Action |
|
|
169
|
+
|-----|--------|
|
|
170
|
+
| `P` | Play / Pause |
|
|
171
|
+
| `O` | Toggle Forward / Reverse direction |
|
|
172
|
+
| `N` | Next step (forward one operation) |
|
|
173
|
+
| `B` | Back step (reverse one operation) |
|
|
174
|
+
| `X` | Increase speed |
|
|
175
|
+
| `Z` | Decrease speed |
|
|
176
|
+
| `G` | Re-generate data with current settings |
|
|
177
|
+
| `M` | Cycle through modes (adaptive → simple → medium → complex) |
|
|
178
|
+
| `A` | Decrease number of elements |
|
|
179
|
+
| `S` | Increase number of elements |
|
|
180
|
+
| `D` | Decrease disorder percentage |
|
|
181
|
+
| `F` | Increase disorder percentage |
|
|
182
|
+
| `Q` | Quit |
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Modes / Flags
|
|
187
|
+
|
|
188
|
+
Your `push_swap` must support the following flags (passed as `--<mode>` before the numbers):
|
|
189
|
+
|
|
190
|
+
| Mode | Disorder range | Description |
|
|
191
|
+
|------------|----------------|------------------------------------------|
|
|
192
|
+
| `simple` | 15.0% – 19.9% | Nearly sorted sequences |
|
|
193
|
+
| `medium` | 20.0% – 49.9% | Moderately shuffled sequences |
|
|
194
|
+
| `complex` | 50.0% – 55.0% | Heavily shuffled sequences |
|
|
195
|
+
| `adaptive` | 15.0% – 55.0% | Random disorder across the full spectrum |
|
|
196
|
+
|
|
197
|
+
> **Note:** If your `push_swap` does **not** implement these flags, the visualizer will still work if your program ignores unknown flags and simply sorts the provided numbers.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## How it works
|
|
202
|
+
|
|
203
|
+
1. **Generate** a random sequence with the desired size and disorder level.
|
|
204
|
+
2. **Run** your `push_swap` executable with the sequence.
|
|
205
|
+
3. **Capture** the operations printed to `stdout`.
|
|
206
|
+
4. **Render** a TUI showing both stacks as colored bars.
|
|
207
|
+
5. **Animate** the operations at the chosen speed, allowing forward and reverse playback.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import random
|
|
6
|
+
import subprocess
|
|
7
|
+
import signal
|
|
8
|
+
import select
|
|
9
|
+
import termios
|
|
10
|
+
import tty
|
|
11
|
+
import shutil
|
|
12
|
+
from collections import deque
|
|
13
|
+
|
|
14
|
+
# ==========================================
|
|
15
|
+
# CONFIGURATION & INITIALIZATION
|
|
16
|
+
# ==========================================
|
|
17
|
+
def parse_arguments():
|
|
18
|
+
if len(sys.argv) < 2 or len(sys.argv) > 4:
|
|
19
|
+
print(f"Usage: {sys.argv[0]} <path_to_push_swap> [number_of_elements] [max_disorder_percentage]")
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
target_executable = sys.argv[1]
|
|
23
|
+
if not os.path.isfile(target_executable) or not os.access(target_executable, os.X_OK):
|
|
24
|
+
print(f"Error: '{target_executable}' is not a valid or executable file.")
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
n_elems = 500
|
|
28
|
+
if len(sys.argv) >= 3:
|
|
29
|
+
try:
|
|
30
|
+
n_elems = int(sys.argv[2])
|
|
31
|
+
if n_elems <= 0:
|
|
32
|
+
raise ValueError
|
|
33
|
+
except ValueError:
|
|
34
|
+
print("Error: number_of_elements must be a positive integer.")
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
max_disorder = 50
|
|
38
|
+
if len(sys.argv) == 4:
|
|
39
|
+
try:
|
|
40
|
+
max_disorder = int(sys.argv[3])
|
|
41
|
+
if max_disorder < 0 or max_disorder > 55:
|
|
42
|
+
raise ValueError
|
|
43
|
+
except ValueError:
|
|
44
|
+
print("Error: max_disorder_percentage must be between 0 and 55.")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
return target_executable, n_elems, max_disorder
|
|
48
|
+
|
|
49
|
+
# ==========================================
|
|
50
|
+
# TERMINAL CONTEXT MANAGER
|
|
51
|
+
# ==========================================
|
|
52
|
+
class TerminalTUI:
|
|
53
|
+
def __init__(self):
|
|
54
|
+
self.fd = sys.stdin.fileno()
|
|
55
|
+
self.old_settings = None
|
|
56
|
+
|
|
57
|
+
def __enter__(self):
|
|
58
|
+
self.old_settings = termios.tcgetattr(self.fd)
|
|
59
|
+
tty.setcbreak(self.fd)
|
|
60
|
+
sys.stdout.write("\033[?1049h\033[?25l")
|
|
61
|
+
sys.stdout.flush()
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
65
|
+
sys.stdout.write("\033[0m\033[?25h\033[?1049l")
|
|
66
|
+
sys.stdout.flush()
|
|
67
|
+
termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old_settings)
|
|
68
|
+
|
|
69
|
+
def get_key(timeout):
|
|
70
|
+
r, _, _ = select.select([sys.stdin], [], [], timeout)
|
|
71
|
+
if r:
|
|
72
|
+
data = os.read(sys.stdin.fileno(), 3)
|
|
73
|
+
if len(data) >= 1:
|
|
74
|
+
return data.decode('utf-8', 'ignore')
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
# ==========================================
|
|
78
|
+
# VISUALIZER ENGINE
|
|
79
|
+
# ==========================================
|
|
80
|
+
class PushSwapVisualizer:
|
|
81
|
+
def __init__(self, target_executable, n_elems, max_disorder):
|
|
82
|
+
self.target_executable = target_executable
|
|
83
|
+
self.n_elems = n_elems
|
|
84
|
+
self.disorder = max_disorder
|
|
85
|
+
self.actual_disorder = 0.0
|
|
86
|
+
|
|
87
|
+
self.allowed_sizes = [10, 50, 100, 200, 500, 1000]
|
|
88
|
+
if self.n_elems not in self.allowed_sizes:
|
|
89
|
+
self.allowed_sizes.append(self.n_elems)
|
|
90
|
+
self.allowed_sizes.sort()
|
|
91
|
+
|
|
92
|
+
self.stack_a = deque()
|
|
93
|
+
self.stack_b = deque()
|
|
94
|
+
self.ops = []
|
|
95
|
+
self.total_ops = 0
|
|
96
|
+
self.op_idx = 0
|
|
97
|
+
|
|
98
|
+
# New mode/flags integration
|
|
99
|
+
self.flags = ["--adaptive", "--simple", "--medium", "--complex"]
|
|
100
|
+
self.flag_idx = 0
|
|
101
|
+
|
|
102
|
+
self.force_redraw = True
|
|
103
|
+
self.auto_play = False
|
|
104
|
+
self.play_dir = "FWD"
|
|
105
|
+
|
|
106
|
+
self.fps = 30
|
|
107
|
+
self.frame_delay = 1.0 / self.fps
|
|
108
|
+
self.speeds = [1, 5, 10, 30, 60, 150, 300, 600, 1200, 3000, 6000, 12000, 50000, 100000]
|
|
109
|
+
self.speed_idx = 5
|
|
110
|
+
self.accumulator = 0
|
|
111
|
+
|
|
112
|
+
def compute_disorder(self, sequence):
|
|
113
|
+
n = len(sequence)
|
|
114
|
+
if n < 2:
|
|
115
|
+
return 0.0
|
|
116
|
+
|
|
117
|
+
def count_inversions(arr):
|
|
118
|
+
if len(arr) <= 1:
|
|
119
|
+
return arr, 0
|
|
120
|
+
mid = len(arr) // 2
|
|
121
|
+
left, inv_left = count_inversions(arr[:mid])
|
|
122
|
+
right, inv_right = count_inversions(arr[mid:])
|
|
123
|
+
merged, inv_merge = merge(left, right)
|
|
124
|
+
return merged, inv_left + inv_right + inv_merge
|
|
125
|
+
|
|
126
|
+
def merge(left, right):
|
|
127
|
+
merged = []
|
|
128
|
+
inv_count = 0
|
|
129
|
+
i, j = 0, 0
|
|
130
|
+
while i < len(left) and j < len(right):
|
|
131
|
+
if left[i] <= right[j]:
|
|
132
|
+
merged.append(left[i])
|
|
133
|
+
i += 1
|
|
134
|
+
else:
|
|
135
|
+
merged.append(right[j])
|
|
136
|
+
inv_count += len(left) - i
|
|
137
|
+
j += 1
|
|
138
|
+
merged += left[i:]
|
|
139
|
+
merged += right[j:]
|
|
140
|
+
return merged, inv_count
|
|
141
|
+
|
|
142
|
+
_, mistakes = count_inversions(list(sequence))
|
|
143
|
+
total_pairs = (n * (n - 1)) / 2.0
|
|
144
|
+
|
|
145
|
+
return (mistakes / total_pairs) * 100.0
|
|
146
|
+
|
|
147
|
+
def generate_data(self):
|
|
148
|
+
self.auto_play = False
|
|
149
|
+
self.op_idx = 0
|
|
150
|
+
self.accumulator = 0
|
|
151
|
+
self.ops = []
|
|
152
|
+
self.total_ops = 0
|
|
153
|
+
|
|
154
|
+
sys.stdout.write("\033[2J\033[H\033[1;36mGenerating data and running push_swap... Please wait.\033[0m\r\n")
|
|
155
|
+
sys.stdout.flush()
|
|
156
|
+
|
|
157
|
+
raw_sequence = random.sample(range(-1000000, 1000000), self.n_elems)
|
|
158
|
+
raw_sequence.sort()
|
|
159
|
+
|
|
160
|
+
n = self.n_elems
|
|
161
|
+
total_pairs = (n * (n - 1)) / 2.0
|
|
162
|
+
target_inv = int((self.disorder / 100.0) * total_pairs)
|
|
163
|
+
|
|
164
|
+
if target_inv > 0:
|
|
165
|
+
inv = [0] * n
|
|
166
|
+
indices = list(range(n))
|
|
167
|
+
random.shuffle(indices)
|
|
168
|
+
|
|
169
|
+
remaining = target_inv
|
|
170
|
+
for i in indices:
|
|
171
|
+
max_cap = n - 1 - i
|
|
172
|
+
take = random.randint(0, min(remaining, max_cap))
|
|
173
|
+
inv[i] = take
|
|
174
|
+
remaining -= take
|
|
175
|
+
|
|
176
|
+
if remaining > 0:
|
|
177
|
+
random.shuffle(indices)
|
|
178
|
+
for i in indices:
|
|
179
|
+
max_cap = n - 1 - i
|
|
180
|
+
space = max_cap - inv[i]
|
|
181
|
+
if space > 0:
|
|
182
|
+
take = min(remaining, space)
|
|
183
|
+
inv[i] += take
|
|
184
|
+
remaining -= take
|
|
185
|
+
if remaining == 0:
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
result_sequence = []
|
|
189
|
+
for i in range(n - 1, -1, -1):
|
|
190
|
+
val = raw_sequence[i]
|
|
191
|
+
insert_pos = inv[i]
|
|
192
|
+
result_sequence.insert(insert_pos, val)
|
|
193
|
+
|
|
194
|
+
raw_sequence = result_sequence
|
|
195
|
+
|
|
196
|
+
self.actual_disorder = self.compute_disorder(raw_sequence)
|
|
197
|
+
|
|
198
|
+
str_seq = [str(x) for x in raw_sequence]
|
|
199
|
+
current_flag = self.flags[self.flag_idx]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
result = subprocess.run(
|
|
203
|
+
[self.target_executable, current_flag] + str_seq,
|
|
204
|
+
capture_output=True,
|
|
205
|
+
text=True,
|
|
206
|
+
check=False
|
|
207
|
+
)
|
|
208
|
+
self.ops = result.stdout.strip().split()
|
|
209
|
+
self.total_ops = len(self.ops)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
sorted_seq = sorted(raw_sequence)
|
|
213
|
+
rank_map = {val: i + 1 for i, val in enumerate(sorted_seq)}
|
|
214
|
+
ranks_sequence = [rank_map[val] for val in raw_sequence]
|
|
215
|
+
|
|
216
|
+
self.stack_a = deque(ranks_sequence)
|
|
217
|
+
self.stack_b = deque()
|
|
218
|
+
self.force_redraw = True
|
|
219
|
+
|
|
220
|
+
def handle_resize(self, signum, frame):
|
|
221
|
+
self.force_redraw = True
|
|
222
|
+
|
|
223
|
+
def exec_op(self, op):
|
|
224
|
+
if op == "sa" and len(self.stack_a) >= 2:
|
|
225
|
+
self.stack_a[0], self.stack_a[1] = self.stack_a[1], self.stack_a[0]
|
|
226
|
+
elif op == "sb" and len(self.stack_b) >= 2:
|
|
227
|
+
self.stack_b[0], self.stack_b[1] = self.stack_b[1], self.stack_b[0]
|
|
228
|
+
elif op == "ss":
|
|
229
|
+
self.exec_op("sa"); self.exec_op("sb")
|
|
230
|
+
elif op == "pa" and len(self.stack_b) >= 1:
|
|
231
|
+
self.stack_a.appendleft(self.stack_b.popleft())
|
|
232
|
+
elif op == "pb" and len(self.stack_a) >= 1:
|
|
233
|
+
self.stack_b.appendleft(self.stack_a.popleft())
|
|
234
|
+
elif op == "ra" and len(self.stack_a) >= 2:
|
|
235
|
+
self.stack_a.append(self.stack_a.popleft())
|
|
236
|
+
elif op == "rb" and len(self.stack_b) >= 2:
|
|
237
|
+
self.stack_b.append(self.stack_b.popleft())
|
|
238
|
+
elif op == "rr":
|
|
239
|
+
self.exec_op("ra"); self.exec_op("rb")
|
|
240
|
+
elif op == "rra" and len(self.stack_a) >= 2:
|
|
241
|
+
self.stack_a.appendleft(self.stack_a.pop())
|
|
242
|
+
elif op == "rrb" and len(self.stack_b) >= 2:
|
|
243
|
+
self.stack_b.appendleft(self.stack_b.pop())
|
|
244
|
+
elif op == "rrr":
|
|
245
|
+
self.exec_op("rra"); self.exec_op("rrb")
|
|
246
|
+
|
|
247
|
+
def exec_inv_op(self, op):
|
|
248
|
+
inv_map = {
|
|
249
|
+
"sa": "sa", "sb": "sb", "ss": "ss",
|
|
250
|
+
"pa": "pb", "pb": "pa",
|
|
251
|
+
"ra": "rra", "rb": "rrb", "rr": "rrr",
|
|
252
|
+
"rra": "ra", "rrb": "rb", "rrr": "rr"
|
|
253
|
+
}
|
|
254
|
+
if op in inv_map:
|
|
255
|
+
self.exec_op(inv_map[op])
|
|
256
|
+
|
|
257
|
+
def get_rgb(self, val, max_val):
|
|
258
|
+
if val <= 0: return "0;0;0"
|
|
259
|
+
ratio = val / max_val
|
|
260
|
+
if ratio < 0.25:
|
|
261
|
+
return f"0;{int((ratio/0.25)*255)};255"
|
|
262
|
+
elif ratio < 0.5:
|
|
263
|
+
return f"0;255;{int((1-(ratio-0.25)/0.25)*255)}"
|
|
264
|
+
elif ratio < 0.75:
|
|
265
|
+
return f"{int(((ratio-0.5)/0.25)*255)};255;0"
|
|
266
|
+
else:
|
|
267
|
+
return f"255;{int((1-(ratio-0.75)/0.25)*255)};0"
|
|
268
|
+
|
|
269
|
+
def build_bar(self, val1, val2, max_w):
|
|
270
|
+
if val1 == -1 and val2 == -1:
|
|
271
|
+
return " " * max_w
|
|
272
|
+
|
|
273
|
+
l1 = int((val1 * max_w) / self.n_elems) if val1 > 0 else 0
|
|
274
|
+
l2 = int((val2 * max_w) / self.n_elems) if val2 > 0 else 0
|
|
275
|
+
|
|
276
|
+
if val1 > 0 and l1 == 0: l1 = 1
|
|
277
|
+
if val2 > 0 and l2 == 0: l2 = 1
|
|
278
|
+
|
|
279
|
+
rgb1 = self.get_rgb(val1, self.n_elems)
|
|
280
|
+
rgb2 = self.get_rgb(val2, self.n_elems)
|
|
281
|
+
|
|
282
|
+
min_l = min(l1, l2)
|
|
283
|
+
max_l = max(l1, l2)
|
|
284
|
+
|
|
285
|
+
out = ""
|
|
286
|
+
if min_l > 0:
|
|
287
|
+
out += f"\033[38;2;{rgb1}m\033[48;2;{rgb2}m" + ("▀" * min_l) + "\033[0m"
|
|
288
|
+
|
|
289
|
+
if l1 > l2:
|
|
290
|
+
out += f"\033[38;2;{rgb1}m\033[49m" + ("▀" * (l1 - min_l)) + "\033[0m"
|
|
291
|
+
elif l2 > l1:
|
|
292
|
+
out += f"\033[38;2;{rgb2}m\033[49m" + ("▄" * (l2 - min_l)) + "\033[0m"
|
|
293
|
+
|
|
294
|
+
spaces = max_w - max_l
|
|
295
|
+
if spaces > 0:
|
|
296
|
+
out += " " * spaces
|
|
297
|
+
|
|
298
|
+
return out
|
|
299
|
+
|
|
300
|
+
def layout_items(self, items, default_chunk, cols):
|
|
301
|
+
chunks = [items[i:i + default_chunk] for i in range(0, len(items), default_chunk)]
|
|
302
|
+
fits = all(sum(it[0] for it in chunk) + (len(chunk) - 1) * 3 + 2 <= cols - 2 for chunk in chunks)
|
|
303
|
+
|
|
304
|
+
if fits:
|
|
305
|
+
return chunks
|
|
306
|
+
|
|
307
|
+
for chunk_size in range(4, 0, -1):
|
|
308
|
+
chunks = [items[i:i + chunk_size] for i in range(0, len(items), chunk_size)]
|
|
309
|
+
fits = all(sum(it[0] for it in chunk) + (len(chunk) - 1) * 3 + 2 <= cols - 2 for chunk in chunks)
|
|
310
|
+
if fits:
|
|
311
|
+
return chunks
|
|
312
|
+
|
|
313
|
+
return [[item] for item in items]
|
|
314
|
+
|
|
315
|
+
def sample_stack(self, stack, m_slots):
|
|
316
|
+
length = len(stack)
|
|
317
|
+
if length == 0: return []
|
|
318
|
+
if length <= m_slots: return list(stack)
|
|
319
|
+
return [stack[int(k * (length - 1) / (m_slots - 1))] if m_slots > 1 else stack[0] for k in range(m_slots)]
|
|
320
|
+
|
|
321
|
+
def draw_screen(self):
|
|
322
|
+
cols, lines = shutil.get_terminal_size()
|
|
323
|
+
|
|
324
|
+
clear_cmd = "\033[H"
|
|
325
|
+
if self.force_redraw:
|
|
326
|
+
clear_cmd = "\033[2J\033[H"
|
|
327
|
+
self.force_redraw = False
|
|
328
|
+
|
|
329
|
+
c_rst = "\033[0m"; c_dim = "\033[2m"; c_bold = "\033[1m"
|
|
330
|
+
c_cyan = "\033[1;36m"; c_yellow = "\033[1;33m"; c_green = "\033[1;32m"
|
|
331
|
+
c_red = "\033[1;31m"; c_magenta = "\033[1;35m"; c_frame = "\033[38;5;60m"
|
|
332
|
+
|
|
333
|
+
speed_val = self.speeds[self.speed_idx]
|
|
334
|
+
auto_str = "ON" if self.auto_play else "OFF"
|
|
335
|
+
auto_col = c_green if self.auto_play else c_red
|
|
336
|
+
dir_col = c_cyan if self.play_dir == "FWD" else c_magenta
|
|
337
|
+
mode_str = self.flags[self.flag_idx]
|
|
338
|
+
|
|
339
|
+
top_items = [
|
|
340
|
+
(len("push_swap visualizer"), f"{c_cyan}push_swap visualizer{c_rst}"),
|
|
341
|
+
(len(f"Nums: {self.n_elems}"), f"{c_yellow}Nums: {self.n_elems}{c_rst}"),
|
|
342
|
+
(len(f"Mode: {mode_str}"), f"{c_green}Mode: {mode_str}{c_rst}"),
|
|
343
|
+
(len(f"Disorder: {self.actual_disorder:.1f}%"), f"{c_magenta}Disorder: {self.actual_disorder:.1f}%{c_rst}"),
|
|
344
|
+
(len(f"Ops: {self.op_idx}/{self.total_ops}"), f"{c_bold}Ops: {self.op_idx}/{self.total_ops}{c_rst}"),
|
|
345
|
+
(len(f"Auto: {auto_str} ({self.play_dir})"), f"Auto: {auto_col}{auto_str}{c_rst} ({dir_col}{self.play_dir}{c_rst})"),
|
|
346
|
+
(len(f"Speed: {speed_val}/s"), f"{c_bold}Speed: {speed_val}/s{c_rst}")
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
bottom_items = [
|
|
350
|
+
(len("[P] Play/Pause"), f"[{c_dim}P{c_rst}] Play/Pause"),
|
|
351
|
+
(len("[O] Fwd/Rev"), f"[{c_dim}O{c_rst}] Fwd/Rev"),
|
|
352
|
+
(len("[N] Next"), f"[{c_dim}N{c_rst}] Next"),
|
|
353
|
+
(len("[B] Back"), f"[{c_dim}B{c_rst}] Back"),
|
|
354
|
+
(len("[Z] Speed-"), f"[{c_dim}Z{c_rst}] Speed-"),
|
|
355
|
+
(len("[X] Speed+"), f"[{c_dim}X{c_rst}] Speed+"),
|
|
356
|
+
(len("[G] Re-gen"), f"[{c_yellow}G{c_rst}] Re-gen"),
|
|
357
|
+
(len("[M] Mode"), f"[{c_green}M{c_rst}] Mode"),
|
|
358
|
+
(len("[A] Nums-"), f"[{c_cyan}A{c_rst}] Nums-"),
|
|
359
|
+
(len("[S] Nums+"), f"[{c_cyan}S{c_rst}] Nums+"),
|
|
360
|
+
(len("[D] Disorder-"), f"[{c_magenta}D{c_rst}] Disorder-"),
|
|
361
|
+
(len("[F] Disorder+"), f"[{c_magenta}F{c_rst}] Disorder+"),
|
|
362
|
+
(len("[Q] Quit"), f"[{c_red}Q{c_rst}] Quit")
|
|
363
|
+
]
|
|
364
|
+
|
|
365
|
+
top_chunks = self.layout_items(top_items, 3, cols)
|
|
366
|
+
bottom_chunks = self.layout_items(bottom_items, 6, cols)
|
|
367
|
+
|
|
368
|
+
occupied_lines = len(top_chunks) + len(bottom_chunks) + 5
|
|
369
|
+
max_lines = max(lines - occupied_lines, 1)
|
|
370
|
+
max_slots = max_lines * 2
|
|
371
|
+
|
|
372
|
+
out = [clear_cmd, "\033[0m"]
|
|
373
|
+
|
|
374
|
+
# --- DRAW TOP BOX ---
|
|
375
|
+
out.append(f"{c_frame}╭{'─' * (cols-2)}╮{c_rst}\033[K\r\n")
|
|
376
|
+
for chunk in top_chunks:
|
|
377
|
+
plain_len = sum(item[0] for item in chunk) + (len(chunk) - 1) * 3
|
|
378
|
+
pad = cols - 2 - plain_len
|
|
379
|
+
pad_l = max(pad // 2, 0)
|
|
380
|
+
pad_r = max(pad - pad_l, 0)
|
|
381
|
+
colored_str = f" {c_dim}|{c_rst} ".join(item[1] for item in chunk)
|
|
382
|
+
out.append(f"{c_frame}│{c_rst}{' ' * pad_l}{colored_str}{' ' * pad_r}{c_frame}│{c_rst}\033[K\r\n")
|
|
383
|
+
out.append(f"{c_frame}╰{'─' * (cols-2)}╯{c_rst}\033[K\r\n")
|
|
384
|
+
|
|
385
|
+
# --- DRAW STACKS ---
|
|
386
|
+
half_cols = max((cols - 5) // 2, 0)
|
|
387
|
+
hdr_a = f"{c_rst}STACK A{c_rst}"
|
|
388
|
+
hdr_b = f"{c_rst}STACK B{c_rst}"
|
|
389
|
+
pad_a_len = max(half_cols - 7, 0)
|
|
390
|
+
out.append(f" {hdr_a}{' ' * pad_a_len} {c_dim}│{c_rst} {hdr_b}\033[K\r\n")
|
|
391
|
+
|
|
392
|
+
disp_sa = self.sample_stack(self.stack_a, max_slots)
|
|
393
|
+
disp_sb = self.sample_stack(self.stack_b, max_slots)
|
|
394
|
+
|
|
395
|
+
disp_sa_len = len(disp_sa)
|
|
396
|
+
disp_sb_len = len(disp_sb)
|
|
397
|
+
|
|
398
|
+
for i in range(max_lines):
|
|
399
|
+
idx1, idx2 = i * 2, i * 2 + 1
|
|
400
|
+
|
|
401
|
+
a1 = disp_sa[idx1] if idx1 < disp_sa_len else -1
|
|
402
|
+
a2 = disp_sa[idx2] if idx2 < disp_sa_len else -1
|
|
403
|
+
b1 = disp_sb[idx1] if idx1 < disp_sb_len else -1
|
|
404
|
+
b2 = disp_sb[idx2] if idx2 < disp_sb_len else -1
|
|
405
|
+
|
|
406
|
+
str_a = self.build_bar(a1, a2, half_cols)
|
|
407
|
+
str_b = self.build_bar(b1, b2, half_cols)
|
|
408
|
+
|
|
409
|
+
out.append(f" {str_a} \033[0m{c_dim}│\033[0m {str_b} \033[0m\033[K\r\n")
|
|
410
|
+
|
|
411
|
+
# --- DRAW BOTTOM BOX ---
|
|
412
|
+
out.append(f"{c_frame}╭{'─' * (cols-2)}╮{c_rst}\033[K\r\n")
|
|
413
|
+
for chunk in bottom_chunks:
|
|
414
|
+
plain_len = sum(item[0] for item in chunk) + (len(chunk) - 1) * 3
|
|
415
|
+
pad = cols - 2 - plain_len
|
|
416
|
+
pad_l = max(pad // 2, 0)
|
|
417
|
+
pad_r = max(pad - pad_l, 0)
|
|
418
|
+
colored_str = f" {c_dim}|{c_rst} ".join(item[1] for item in chunk)
|
|
419
|
+
out.append(f"{c_frame}│{c_rst}{' ' * pad_l}{colored_str}{' ' * pad_r}{c_frame}│{c_rst}\033[K\r\n")
|
|
420
|
+
out.append(f"{c_frame}╰{'─' * (cols-2)}╯{c_rst}\033[K")
|
|
421
|
+
|
|
422
|
+
sys.stdout.write("".join(out))
|
|
423
|
+
sys.stdout.flush()
|
|
424
|
+
|
|
425
|
+
def change_elems(self, direction):
|
|
426
|
+
try:
|
|
427
|
+
curr_idx = self.allowed_sizes.index(self.n_elems)
|
|
428
|
+
except ValueError:
|
|
429
|
+
self.allowed_sizes.append(self.n_elems)
|
|
430
|
+
self.allowed_sizes.sort()
|
|
431
|
+
curr_idx = self.allowed_sizes.index(self.n_elems)
|
|
432
|
+
|
|
433
|
+
new_idx = curr_idx + direction
|
|
434
|
+
if 0 <= new_idx < len(self.allowed_sizes):
|
|
435
|
+
self.n_elems = self.allowed_sizes[new_idx]
|
|
436
|
+
self.generate_data()
|
|
437
|
+
|
|
438
|
+
def change_disorder(self, direction):
|
|
439
|
+
new_val = self.disorder + (direction * 5)
|
|
440
|
+
if 0 <= new_val <= 55:
|
|
441
|
+
self.disorder = new_val
|
|
442
|
+
self.generate_data()
|
|
443
|
+
|
|
444
|
+
def run(self):
|
|
445
|
+
self.generate_data()
|
|
446
|
+
|
|
447
|
+
signal.signal(signal.SIGWINCH, self.handle_resize)
|
|
448
|
+
|
|
449
|
+
def quit_signal(sig, frame):
|
|
450
|
+
sys.exit(0)
|
|
451
|
+
|
|
452
|
+
signal.signal(signal.SIGINT, quit_signal)
|
|
453
|
+
signal.signal(signal.SIGTERM, quit_signal)
|
|
454
|
+
|
|
455
|
+
with TerminalTUI():
|
|
456
|
+
while True:
|
|
457
|
+
if self.auto_play:
|
|
458
|
+
target_ops_sec = self.speeds[self.speed_idx]
|
|
459
|
+
self.accumulator += target_ops_sec
|
|
460
|
+
ops_this_frame = self.accumulator // self.fps
|
|
461
|
+
self.accumulator %= self.fps
|
|
462
|
+
|
|
463
|
+
for _ in range(ops_this_frame):
|
|
464
|
+
if self.play_dir == "FWD":
|
|
465
|
+
if self.op_idx < self.total_ops:
|
|
466
|
+
self.exec_op(self.ops[self.op_idx])
|
|
467
|
+
self.op_idx += 1
|
|
468
|
+
else:
|
|
469
|
+
self.auto_play = False
|
|
470
|
+
self.accumulator = 0
|
|
471
|
+
break
|
|
472
|
+
else:
|
|
473
|
+
if self.op_idx > 0:
|
|
474
|
+
self.op_idx -= 1
|
|
475
|
+
self.exec_inv_op(self.ops[self.op_idx])
|
|
476
|
+
else:
|
|
477
|
+
self.auto_play = False
|
|
478
|
+
self.accumulator = 0
|
|
479
|
+
break
|
|
480
|
+
|
|
481
|
+
self.draw_screen()
|
|
482
|
+
|
|
483
|
+
key = get_key(self.frame_delay)
|
|
484
|
+
if key:
|
|
485
|
+
if key.startswith("\x1b"):
|
|
486
|
+
continue
|
|
487
|
+
|
|
488
|
+
k = key.lower()
|
|
489
|
+
if k == 'p':
|
|
490
|
+
self.auto_play = not self.auto_play
|
|
491
|
+
if not self.auto_play: self.accumulator = 0
|
|
492
|
+
elif k == 'o':
|
|
493
|
+
self.play_dir = "REV" if self.play_dir == "FWD" else "FWD"
|
|
494
|
+
elif k == 'x':
|
|
495
|
+
if self.speed_idx < len(self.speeds) - 1: self.speed_idx += 1
|
|
496
|
+
elif k == 'z':
|
|
497
|
+
if self.speed_idx > 0: self.speed_idx -= 1
|
|
498
|
+
elif k == 'n':
|
|
499
|
+
self.auto_play = False
|
|
500
|
+
self.accumulator = 0
|
|
501
|
+
if self.op_idx < self.total_ops:
|
|
502
|
+
self.exec_op(self.ops[self.op_idx])
|
|
503
|
+
self.op_idx += 1
|
|
504
|
+
elif k == 'b':
|
|
505
|
+
self.auto_play = False
|
|
506
|
+
self.accumulator = 0
|
|
507
|
+
if self.op_idx > 0:
|
|
508
|
+
self.op_idx -= 1
|
|
509
|
+
self.exec_inv_op(self.ops[self.op_idx])
|
|
510
|
+
elif k == 'g':
|
|
511
|
+
self.generate_data()
|
|
512
|
+
elif k == 'm':
|
|
513
|
+
self.flag_idx = (self.flag_idx + 1) % len(self.flags)
|
|
514
|
+
self.generate_data()
|
|
515
|
+
elif k == 'a':
|
|
516
|
+
self.change_elems(-1)
|
|
517
|
+
elif k == 's':
|
|
518
|
+
self.change_elems(1)
|
|
519
|
+
elif k == 'd':
|
|
520
|
+
self.change_disorder(-1)
|
|
521
|
+
elif k == 'f':
|
|
522
|
+
self.change_disorder(1)
|
|
523
|
+
elif k == 'q':
|
|
524
|
+
break
|
|
525
|
+
|
|
526
|
+
# ==========================================
|
|
527
|
+
# MAIN ENTRY
|
|
528
|
+
# ==========================================
|
|
529
|
+
def main():
|
|
530
|
+
target, elements, max_disorder = parse_arguments()
|
|
531
|
+
visualizer = PushSwapVisualizer(target, elements, max_disorder)
|
|
532
|
+
visualizer.run()
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
if __name__ == "__main__":
|
|
536
|
+
main()
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ft_ps_visu
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A terminal visualizer for the 42 push_swap project with real-time TUI, controlled disorder generation, and interactive playback.
|
|
5
|
+
Author: Italo Almeida
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/italoalmeida0/ft_ps_visu
|
|
8
|
+
Project-URL: Issues, https://github.com/italoalmeida0/ft_ps_visu/issues
|
|
9
|
+
Keywords: 42,push_swap,visualizer,terminal,tui,sorting,algorithm
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Education :: Testing
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Classifier: Topic :: Terminals
|
|
23
|
+
Requires-Python: >=3.7
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# ft_ps_visu
|
|
29
|
+
|
|
30
|
+
A terminal visualizer for the **42 push_swap** project. It generates controlled random sequences with specific disorder levels, runs your `push_swap` executable, and renders a real-time TUI (Terminal User Interface) with interactive playback controls.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
<img src="i1.png" width="500">
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- **Real-time TUI visualization** — watch your `push_swap` algorithm sort stacks live in the terminal.
|
|
39
|
+
- **Controlled disorder generation** — creates sequences with precise inversion percentages.
|
|
40
|
+
- **Four test modes** — adaptive, simple, medium, and complex.
|
|
41
|
+
- **Interactive controls** — play/pause, forward/reverse, step-by-step, speed adjustment, and more.
|
|
42
|
+
- **True-color bars** — gradient-colored bars representing values for easy visual tracking.
|
|
43
|
+
- **Responsive layout** — adapts to terminal resize events.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- Python 3.7+
|
|
50
|
+
- A compiled `push_swap` executable that accepts a `--<mode>` flag and the numbers to sort
|
|
51
|
+
- A terminal that supports:
|
|
52
|
+
- ANSI escape codes
|
|
53
|
+
- True-color (24-bit RGB) for the best experience
|
|
54
|
+
- Alternate screen buffer (`\033[?1049h` / `\033[?1049l`)
|
|
55
|
+
|
|
56
|
+
> **Note:** On Windows, use Windows Terminal, WSL, or any modern terminal emulator. The classic `cmd.exe` console may not render Unicode block characters or true-color correctly.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
> **Tip:** If you run into installation errors, try updating `pip` first:
|
|
63
|
+
> ```bash
|
|
64
|
+
> pip install --upgrade pip
|
|
65
|
+
> # or
|
|
66
|
+
> pip3 install --upgrade pip
|
|
67
|
+
> ```
|
|
68
|
+
|
|
69
|
+
### Option 1: Install from PyPI (recommended)
|
|
70
|
+
|
|
71
|
+
Using `pip`:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install ft_ps_visu
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Using `pip3`:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip3 install ft_ps_visu
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
User-local install (no sudo required — `pip`):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install --user ft_ps_visu
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Using `python3 -m pip`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python3 -m pip install ft_ps_visu
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> **Note:** When using `--user`, the `ft_ps_visu` binary is installed to a user-local `bin/` directory. Make sure this directory is on your `PATH`, or use the `python3 -m` execution methods shown below.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### Option 2: Install from source
|
|
100
|
+
|
|
101
|
+
Clone this repository:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
git clone https://github.com/italoalmeida0/ft_ps_visu.git
|
|
105
|
+
cd ft_ps_visu
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Editable / development mode (`pip`):
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pip install -e .
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Normal install (`pip`):
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install .
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### Option 3: Run with `pipx` (isolated, no install required)
|
|
123
|
+
|
|
124
|
+
If you have [`pipx`](https://pypa.github.io/pipx/) installed, you can run the visualizer directly without permanently installing it:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pipx run ft_ps_visu ./push_swap
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Or install it into an isolated environment:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
pipx install ft_ps_visu
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Then run normally:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
ft_ps_visu ./push_swap
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
Make sure your `push_swap` binary is compiled and executable:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
make
|
|
148
|
+
chmod +x push_swap
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Usage
|
|
154
|
+
|
|
155
|
+
### Basic usage
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
ft_ps_visu ./push_swap
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
With custom number of elements:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
ft_ps_visu ./push_swap 100
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
With custom disorder percentage (0–55):
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
ft_ps_visu ./push_swap 500 30
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
If `ft_ps_visu` is **not** found on your `PATH`, run via the module:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
python3 -m ft_ps_visu ./push_swap
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Or using the `.cli` submodule directly:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
python3 -m ft_ps_visu.cli ./push_swap
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
From the cloned source directory (no install required):
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
python3 ft_ps_visu/cli.py ./push_swap
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Controls
|
|
194
|
+
|
|
195
|
+
| Key | Action |
|
|
196
|
+
|-----|--------|
|
|
197
|
+
| `P` | Play / Pause |
|
|
198
|
+
| `O` | Toggle Forward / Reverse direction |
|
|
199
|
+
| `N` | Next step (forward one operation) |
|
|
200
|
+
| `B` | Back step (reverse one operation) |
|
|
201
|
+
| `X` | Increase speed |
|
|
202
|
+
| `Z` | Decrease speed |
|
|
203
|
+
| `G` | Re-generate data with current settings |
|
|
204
|
+
| `M` | Cycle through modes (adaptive → simple → medium → complex) |
|
|
205
|
+
| `A` | Decrease number of elements |
|
|
206
|
+
| `S` | Increase number of elements |
|
|
207
|
+
| `D` | Decrease disorder percentage |
|
|
208
|
+
| `F` | Increase disorder percentage |
|
|
209
|
+
| `Q` | Quit |
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Modes / Flags
|
|
214
|
+
|
|
215
|
+
Your `push_swap` must support the following flags (passed as `--<mode>` before the numbers):
|
|
216
|
+
|
|
217
|
+
| Mode | Disorder range | Description |
|
|
218
|
+
|------------|----------------|------------------------------------------|
|
|
219
|
+
| `simple` | 15.0% – 19.9% | Nearly sorted sequences |
|
|
220
|
+
| `medium` | 20.0% – 49.9% | Moderately shuffled sequences |
|
|
221
|
+
| `complex` | 50.0% – 55.0% | Heavily shuffled sequences |
|
|
222
|
+
| `adaptive` | 15.0% – 55.0% | Random disorder across the full spectrum |
|
|
223
|
+
|
|
224
|
+
> **Note:** If your `push_swap` does **not** implement these flags, the visualizer will still work if your program ignores unknown flags and simply sorts the provided numbers.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## How it works
|
|
229
|
+
|
|
230
|
+
1. **Generate** a random sequence with the desired size and disorder level.
|
|
231
|
+
2. **Run** your `push_swap` executable with the sequence.
|
|
232
|
+
3. **Capture** the operations printed to `stdout`.
|
|
233
|
+
4. **Render** a TUI showing both stacks as colored bars.
|
|
234
|
+
5. **Animate** the operations at the chosen speed, allowing forward and reverse playback.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
ft_ps_visu/__init__.py
|
|
5
|
+
ft_ps_visu/__main__.py
|
|
6
|
+
ft_ps_visu/cli.py
|
|
7
|
+
ft_ps_visu.egg-info/PKG-INFO
|
|
8
|
+
ft_ps_visu.egg-info/SOURCES.txt
|
|
9
|
+
ft_ps_visu.egg-info/dependency_links.txt
|
|
10
|
+
ft_ps_visu.egg-info/entry_points.txt
|
|
11
|
+
ft_ps_visu.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ft_ps_visu
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ft_ps_visu"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A terminal visualizer for the 42 push_swap project with real-time TUI, controlled disorder generation, and interactive playback."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.7"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Italo Almeida"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["42", "push_swap", "visualizer", "terminal", "tui", "sorting", "algorithm"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.7",
|
|
22
|
+
"Programming Language :: Python :: 3.8",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: Education :: Testing",
|
|
28
|
+
"Topic :: Software Development :: Testing",
|
|
29
|
+
"Topic :: Terminals",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
ft_ps_visu = "ft_ps_visu.cli:main"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/italoalmeida0/ft_ps_visu"
|
|
37
|
+
Issues = "https://github.com/italoalmeida0/ft_ps_visu/issues"
|