├── LICENSE ├── README.md └── sttt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 flicko 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Solution to your Terminal Transition Tribulation 2 | 3 | ever wanted to add cool transitions to your terminals? no? well let me present to you the solution to a [tribulation](https://letmegooglethat.com/?q=tribulation) you didnt even know you were in 4 | 5 | ## Features + Roadmap 6 | 7 | + [x] cubic bezier easing 8 | + [x] reactive to terminal size 9 | + [x] loop/reverse transitions 10 | + [x] set duration for transition 11 | + [x] transitions 12 | + [ ] more transitions 13 | + [x] actual cli 14 | 15 | ## Installing 16 | 17 | the entire thing is just one file so you can curl/wget it and add to path 18 | 19 | using curl: 20 | ```sh 21 | sudo curl -L https://raw.githubusercontent.com/flick0/sttt/main/sttt -o /usr/local/bin/sttt 22 | sudo chmod a+rx /usr/local/bin/sttt 23 | ``` 24 | 25 | or with wget: 26 | ```sh 27 | sudo wget https://raw.githubusercontent.com/flick0/sttt/main/sttt -o /usr/local/bin/sttt 28 | sudo chmod a+rx /usr/local/bin/sttt 29 | ``` 30 | 31 | ## Transitions 32 | 33 | + ScanLine 34 | > scanline with vertical set to `true`,reverse set to `true`, `width` as `2`, `scale_width` as `1.1`, and `scale_ratio` as `0.5` meaning the width is gonna scale by `1.1` with the maximum width at `0.5` 35 | ![scanline](https://github.com/flick0/sttt/assets/77581181/d501dc17-7b23-4704-8404-1f44ab753ee8) 36 | 37 | + Grow 38 | > ![grow](https://github.com/flick0/sttt/assets/77581181/b67bf986-99c7-4d72-9fa9-8be4d02ffa71) 39 | 40 | + Shrink 41 | > shrink with center position set to `[0.9,0.5]` meaning its at 90% width and 50% height 42 | ![shrink](https://github.com/flick0/sttt/assets/77581181/94600752-6ee0-48eb-855a-62a0c38a6093) 43 | 44 | + GrowExit 45 | > growexit with center of first circle set as `[0.3,0.3]` and `[0.7,0.7]` for the second circle 46 | ![growexit](https://github.com/flick0/sttt/assets/77581181/804732b0-63c8-470e-9581-631804aaeb77) 47 | 48 | 49 | + ShrinkExit 50 | > shrinkexit with `second_start` set to `0.9` meaning 90% of duration is used for first circle and the rest 10% for the second circle 51 | ![shrinkexit](https://github.com/flick0/sttt/assets/77581181/e452a474-3d1e-473d-b3f4-c628be14feee) 52 | 53 | ## its also reactive to terminal size changes! 54 | > GrowExit transition with loop and reverse enabled 55 | ![resize](https://github.com/flick0/sttt/assets/77581181/7d52b7d1-5968-46a7-94a2-d9e44e25bd35) 56 | 57 | 58 | 59 | ## Why? 60 | 61 | this was supposed to be a tiny little script that does only one linear transition for a [rice im working on](https://github.com/flick0/dotfiles) which ended up taking more of my time than i intended, the idea was partly inspired by [unimatrix](https://github.com/will8211/unimatrix)(the `-w` flag to be precise, which suggests you to put it in .bashrc to run when terminal launches) 62 | 63 | ## [SWWW](https://github.com/Horus645/swww)? 64 | 65 | if the name isnt clear enough( *"swww"* and *"sttt"* ) most of the inspiration comes from swww, the way transitions are made is also pretty similar 66 | to how swww does it, if somehow you havent heard of swww before, [do go check it out](https://github.com/Horus645/swww), its a wayland wallpaper daemon with fancy transitions 67 | 68 | ## Thanks! 69 | 70 | + to [bezier-easing](https://github.com/gre/bezier-easing) which i directly translated to python because apparently there are no good cubic bezier easing libs i could find for python 71 | -------------------------------------------------------------------------------- /sttt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # a solution to your terminal transition tribulations 4 | 5 | import curses 6 | import argparse 7 | from curses import wrapper 8 | 9 | from dataclasses import dataclass 10 | import time 11 | import inspect 12 | import random 13 | 14 | ############################################################################################################ 15 | # Cubic Bezier Easing directly translated from https://github.com/gre/bezier-easing/blob/master/src/index.js 16 | ############################################################################################################ 17 | 18 | NEWTON_ITERATIONS = 4 19 | NEWTON_MIN_SLOPE = 0.001 20 | SUBDIVISION_PRECISION = 0.0000001 21 | SUBDIVISION_MAX_ITERATIONS = 10 22 | 23 | kSplineTableSize = 11 24 | kSampleStepSize = 1.0 / (kSplineTableSize - 1.0) 25 | 26 | 27 | def A(aA1, aA2): 28 | return 1.0 - 3.0 * aA2 + 3.0 * aA1 29 | 30 | 31 | def B(aA1, aA2): 32 | return 3.0 * aA2 - 6.0 * aA1 33 | 34 | 35 | def C(aA1): 36 | return 3.0 * aA1 37 | 38 | 39 | def calc_bezier(aT, aA1, aA2): 40 | return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT 41 | 42 | 43 | def getSlope(aT, aA1, aA2): 44 | return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1) 45 | 46 | 47 | def binary_subdivide(aX, aA, aB, mX1, mX2): 48 | currentX, currentT, i = 0, 0, 0 49 | while True: 50 | currentT = aA + (aB - aA) / 2.0 51 | currentX = calc_bezier(currentT, mX1, mX2) - aX 52 | if currentX > 0.0: 53 | aB = currentT 54 | else: 55 | aA = currentT 56 | i += 1 57 | if abs(currentX) <= SUBDIVISION_PRECISION or i >= SUBDIVISION_MAX_ITERATIONS: 58 | break 59 | return currentT 60 | 61 | 62 | def newton_raphson_iterate(aX, aGuessT, mX1, mX2): 63 | for _ in range(NEWTON_ITERATIONS): 64 | currentSlope = getSlope(aGuessT, mX1, mX2) 65 | if abs(currentSlope) < SUBDIVISION_PRECISION: 66 | return aGuessT 67 | currentX = calc_bezier(aGuessT, mX1, mX2) - aX 68 | aGuessT -= currentX / currentSlope 69 | return aGuessT 70 | 71 | 72 | def LinearEasing(aX): 73 | return aX 74 | 75 | 76 | class CubicBezier: 77 | def __init__(self, aX1, aY1, aX2, aY2): 78 | self.mX1 = aX1 79 | self.mY1 = aY1 80 | self.mX2 = aX2 81 | self.mY2 = aY2 82 | 83 | self.sampleValues = [0] * kSplineTableSize 84 | for i in range(kSplineTableSize): 85 | self.sampleValues[i] = calc_bezier(i * kSampleStepSize, aX1, aX2) 86 | 87 | def get_t_for_x(self, aX): 88 | intervalStart = 0.0 89 | currentSample = 1 90 | lastSample = kSplineTableSize - 1 91 | 92 | while currentSample != lastSample and self.sampleValues[currentSample] <= aX: 93 | intervalStart += kSampleStepSize 94 | currentSample += 1 95 | currentSample -= 1 96 | 97 | dist = (aX - self.sampleValues[currentSample]) / ( 98 | self.sampleValues[currentSample + 1] - self.sampleValues[currentSample] 99 | ) 100 | guessForT = intervalStart + dist * kSampleStepSize 101 | 102 | initialSlope = getSlope(guessForT, self.mX1, self.mX2) 103 | if initialSlope >= NEWTON_MIN_SLOPE: 104 | return newton_raphson_iterate(aX, guessForT, self.mX1, self.mX2) 105 | elif abs(initialSlope) < SUBDIVISION_PRECISION: 106 | return guessForT 107 | else: 108 | return binary_subdivide( 109 | aX, intervalStart, intervalStart + kSampleStepSize, self.mX1, self.mX2 110 | ) 111 | 112 | def ease(self, aT): 113 | if self.mX1 == self.mY1 and self.mX2 == self.mY2: 114 | return LinearEasing(aT) 115 | if aT == 0: 116 | return 0 117 | if aT == 1: 118 | return 1 119 | return calc_bezier(self.get_t_for_x(aT), self.mY1, self.mY2) 120 | 121 | 122 | ############################################################################################################ 123 | 124 | def init_colors(): 125 | curses.start_color() 126 | curses.use_default_colors() 127 | for i in range(0, curses.COLORS): 128 | curses.init_pair(i + 1, i, -1) 129 | 130 | def set_pix(stdscr, x, y, char="█", color=8): 131 | height, width = stdscr.getmaxyx() 132 | if x >= width: 133 | return False 134 | if y >= height: 135 | return False 136 | try: 137 | stdscr.addstr(y, x, char, color) 138 | except curses.error: 139 | pass 140 | return True 141 | 142 | 143 | class Transition: 144 | screen: curses.window 145 | width: int 146 | height: int 147 | duration: float 148 | start_time: float 149 | loop: bool 150 | reverse: bool 151 | debug: bool = False 152 | bezier: callable 153 | color: int 154 | 155 | @dataclass 156 | class Options: 157 | ... 158 | 159 | def __init__( 160 | self, 161 | screen: curses.window, 162 | duration: float, 163 | bezier: str, 164 | loop: bool = False, 165 | reverse: bool = False, 166 | debug: bool = False, 167 | color: int = 8, 168 | ): 169 | self.screen = screen 170 | self.duration = duration 171 | self.start_time = time.time() 172 | self.height, self.width = screen.getmaxyx() 173 | self.loop = loop 174 | self.debug = debug 175 | self.reverse = reverse 176 | 177 | bezier = [float(i) for i in bezier.split(",")] 178 | 179 | self.bezier = CubicBezier(*bezier).ease 180 | self.options = self.Options() 181 | init_colors() 182 | self.color = curses.color_pair(color) 183 | 184 | def update_dim(self): 185 | self.height, self.width = self.screen.getmaxyx() 186 | 187 | def frame_for_instant(self, now: float): 188 | time = (now - self.start_time) / self.duration 189 | return self.frame_for_time(time) 190 | 191 | def frame_for_time(self, time: float): 192 | last_frame = False 193 | if self.loop: 194 | if self.reverse: 195 | time = time % 2 196 | if time > 1: 197 | time = 2 - time 198 | else: 199 | time = time % 1 200 | elif time < 0: 201 | time = 0 202 | elif time >= 1: 203 | time = 1 204 | last_frame = True 205 | bezier_time = self.bezier(time) 206 | self.screen.clear() 207 | for pix in range(self.height * self.width): 208 | x = pix % self.width 209 | y = pix // self.width 210 | self.draw(x, y, time, bezier_time) 211 | if self.debug: 212 | set_pix(self.screen, 0, 0, char=f"time: {time:.4f}") 213 | set_pix(self.screen, 0, 1, char=f"bezier_time: {bezier_time:.4f}") 214 | set_pix( 215 | self.screen, 0, 2, char=f"width: {self.width} height: {self.height}" 216 | ) 217 | self.screen.refresh() 218 | return not last_frame 219 | 220 | def draw(self, x, y, time, bezier_time): 221 | set_pix(self.screen, x, y) 222 | set_pix(self.screen, 0, 0, char=f"time: {time:.4f}") 223 | set_pix(self.screen, 0, 1, char=f"bezier_time: {bezier_time:.4f}") 224 | set_pix(self.screen, 0, 2, char=f"width: {self.width} height: {self.height}") 225 | 226 | 227 | ############################################################################################################ 228 | # Transitions 229 | ############################################################################################################ 230 | 231 | 232 | class ScanLine(Transition): 233 | @dataclass 234 | class Options: 235 | vertical: bool = True 236 | _vertical_help = """vertical scanline :0""" 237 | reverse: bool = False 238 | _reverse_help = """start from the bottom/right""" 239 | width: int = 2 240 | _width_help = """width of the scanline""" 241 | scale_width: float = 1.1 242 | _scale_width_help = """width is increased by this amount as 'scale_ratio' is reached""" 243 | scale_ratio: float = 0.5 244 | _scale_ratio_help = """ratio where the width of scanline is maximum""" 245 | 246 | def draw(self, x, y, time, bezier_time): 247 | width = self.width 248 | height = self.height 249 | 250 | vertical = self.options.vertical 251 | reverse = self.options.reverse 252 | thickness = self.options.width 253 | 254 | scale_width = self.options.scale_width - 1 255 | scale_ratio = self.options.scale_ratio 256 | 257 | path = [height, width][vertical] 258 | pix_pos = [y, x][vertical] 259 | 260 | if bezier_time <= scale_ratio: 261 | thickness = thickness + scale_width * path * bezier_time / scale_ratio 262 | else: 263 | thickness = thickness + scale_width * path * ( 264 | (1 - bezier_time) / (1 - scale_ratio) 265 | ) 266 | 267 | if reverse: 268 | bezier_time = 1 - bezier_time 269 | 270 | if not vertical: 271 | thickness /= 2 272 | 273 | if abs(pix_pos + thickness - (path + thickness * 2) * bezier_time) < thickness: 274 | set_pix(self.screen, x, y, color=self.color) 275 | 276 | class Doom(Transition): 277 | sink= [] 278 | # @dataclass 279 | # class Options: 280 | # vertical: bool = True 281 | # _vertical_help = """vertical scanline :0""" 282 | # reverse: bool = False 283 | # _reverse_help = """start from the bottom/right""" 284 | # width: int = 2 285 | # _width_help = """width of the scanline""" 286 | # scale_width: float = 1.1 287 | # _scale_width_help = """width is increased by this amount as 'scale_ratio' is reached""" 288 | # scale_ratio: float = 0.5 289 | # _scale_ratio_help = """ratio where the width of scanline is maximum""" 290 | 291 | def draw(self, x, y, time, bezier_time): 292 | width = self.width 293 | height = self.height 294 | 295 | if not self.sink: 296 | self.sink = {random.randint(1,width):random.randint(5,10)/10 for i in range(width//2)} 297 | 298 | bezier_time += 1 - max(self.sink.values()) 299 | 300 | if x in self.sink.keys(): 301 | set_pix(self.screen, x, int(y+height*bezier_time/self.sink[x]), color=self.color) 302 | else: 303 | nearest_sink = 0 304 | dist_sink = width 305 | for sink in self.sink.keys(): 306 | dist = abs(x-sink) 307 | if dist < dist_sink: 308 | nearest_sink = sink 309 | dist_sink = dist 310 | y = int(y+height*bezier_time/self.sink[nearest_sink] + dist_sink/max(self.sink.values())*(bezier_time)) 311 | set_pix(self.screen, x, y, color=self.color) 312 | 313 | 314 | 315 | 316 | class Grow(Transition): 317 | center_x: int 318 | center_y: int 319 | 320 | @dataclass 321 | class Options: 322 | center: tuple[float, float] = (0.5, 0.5) 323 | _center_help = """center of the circle as a ratio between 0 and 1""" 324 | 325 | def __init__(self, *args, **kwargs): 326 | super().__init__(*args, **kwargs) 327 | self.center_x = self.width * self.options.center[0] 328 | self.center_y = self.height * self.options.center[1] 329 | 330 | def update_dim(self): 331 | super().update_dim() 332 | self.center_x = self.width * self.options.center[0] 333 | self.center_y = self.height * self.options.center[1] 334 | 335 | def draw(self, x, y, time, bezier_time): 336 | dist_x = abs(x - self.center_x) / 2 337 | dist_y = abs(y - self.center_y) 338 | dist = (dist_x**2 + dist_y**2) ** 0.5 339 | 340 | max_dist = ( 341 | ((max(self.center_x, self.width - self.center_x)) / 2) ** 2 342 | + (max(self.center_y, self.height - self.center_y)) ** 2 343 | ) ** 0.5 344 | 345 | if dist > bezier_time * max_dist: 346 | set_pix(self.screen, x, y, color=self.color) 347 | 348 | 349 | class Shrink(Transition): 350 | center_x: int 351 | center_y: int 352 | 353 | @dataclass 354 | class Options: 355 | center: tuple[float, float] = (0.5, 0.5) 356 | _center_help = """center of the circle as a ratio between 0 and 1""" 357 | 358 | def __init__(self, *args, **kwargs): 359 | super().__init__(*args, **kwargs) 360 | self.center_x = self.width * self.options.center[0] 361 | self.center_y = self.height * self.options.center[1] 362 | 363 | def update_dim(self): 364 | super().update_dim() 365 | self.center_x = self.width * self.options.center[0] 366 | self.center_y = self.height * self.options.center[1] 367 | 368 | def draw(self, x, y, time, bezier_time): 369 | dist_x = abs(x - self.center_x) / 2 370 | dist_y = abs(y - self.center_y) 371 | dist = (dist_x**2 + dist_y**2) ** 0.5 372 | 373 | max_dist = ( 374 | ((max(self.center_x, self.width - self.center_x)) / 2) ** 2 375 | + (max(self.center_y, self.height - self.center_y)) ** 2 376 | ) ** 0.5 377 | 378 | if dist < (1 - bezier_time) * max_dist: 379 | set_pix(self.screen, x, y, color=self.color) 380 | 381 | 382 | class GrowExit(Transition): 383 | center_x: int 384 | center_y: int 385 | 386 | center_x2: int 387 | center_y2: int 388 | 389 | @dataclass 390 | class Options: 391 | center: tuple[float, float] = (0.5, 0.5) 392 | _center_help = """center of the first circle""" 393 | center2: tuple[float, float] = (-1, -1) 394 | _center2_help = """center of the second circle, if negative, defaults to same coord as first circle""" 395 | second_start: float = 0.5 396 | _second_start_help = """when the second circle starts to grow and the first circle ends as a ratio between 0 and 1""" 397 | 398 | def __init__(self, *args, **kwargs): 399 | super().__init__(*args, **kwargs) 400 | self.center_x = self.width * self.options.center[0] 401 | self.center_y = self.height * self.options.center[1] 402 | self.center_x2 = ( 403 | self.center_x 404 | if self.options.center2[0] < 0 405 | else self.width * self.options.center2[0] 406 | ) 407 | self.center_y2 = ( 408 | self.center_y 409 | if self.options.center2[1] < 0 410 | else self.height * self.options.center2[1] 411 | ) 412 | 413 | def update_dim(self): 414 | super().update_dim() 415 | self.center_x = self.width * self.options.center[0] 416 | self.center_y = self.height * self.options.center[1] 417 | self.center_x2 = ( 418 | self.center_x 419 | if self.options.center2[0] < 0 420 | else self.width * self.options.center2[0] 421 | ) 422 | self.center_y2 = ( 423 | self.center_y 424 | if self.options.center2[1] < 0 425 | else self.height * self.options.center2[1] 426 | ) 427 | 428 | def draw(self, x, y, time, bezier_time): 429 | second_start = self.options.second_start 430 | center_x = self.center_x 431 | center_y = self.center_y 432 | center_x2 = self.center_x2 433 | center_y2 = self.center_y2 434 | width = self.width 435 | height = self.height 436 | 437 | if bezier_time < second_start: 438 | dist_x = abs(x - center_x) / 2 439 | dist_y = abs(y - center_y) 440 | dist = (dist_x**2 + dist_y**2) ** 0.5 441 | 442 | max_dist = ( 443 | ((max(center_x, width - center_x)) / 2) ** 2 444 | + (max(center_y, height - center_y)) ** 2 445 | ) ** 0.5 446 | if dist < bezier_time / second_start * max_dist: 447 | set_pix(self.screen, x, y, color=self.color) 448 | else: 449 | dist_x = abs(x - center_x2) / 2 450 | dist_y = abs(y - center_y2) 451 | dist = (dist_x**2 + dist_y**2) ** 0.5 452 | 453 | max_dist = ( 454 | ((max(center_x2, width - center_x2)) / 2) ** 2 455 | + (max(center_y2, height - center_y2)) ** 2 456 | ) ** 0.5 457 | if dist > (bezier_time - second_start) / (1 - second_start) * max_dist: 458 | set_pix(self.screen, x, y, color=self.color) 459 | 460 | 461 | class ShrinkExit(Transition): 462 | center_x: int 463 | center_y: int 464 | 465 | center_x2: int 466 | center_y2: int 467 | 468 | @dataclass 469 | class Options: 470 | center: tuple[float, float] = (0.5, 0.5) 471 | _center_help = """center of the first circle""" 472 | center2: tuple[float, float] = (-1, -1) 473 | _center2_help = """center of the second circle, if negative, defaults to same coord as first circle""" 474 | second_start: float = 0.5 475 | _second_start_help = """when the second circle starts to grow and the first circle ends as a ratio between 0 and 1""" 476 | 477 | def __init__(self, *args, **kwargs): 478 | super().__init__(*args, **kwargs) 479 | self.center_x = self.width * self.options.center[0] 480 | self.center_y = self.height * self.options.center[1] 481 | self.center_x2 = ( 482 | self.center_x 483 | if self.options.center2[0] < 0 484 | else self.width * self.options.center2[0] 485 | ) 486 | self.center_y2 = ( 487 | self.center_y 488 | if self.options.center2[1] < 0 489 | else self.height * self.options.center2[1] 490 | ) 491 | 492 | def update_dim(self): 493 | super().update_dim() 494 | self.center_x = self.width * self.options.center[0] 495 | self.center_y = self.height * self.options.center[1] 496 | self.center_x2 = ( 497 | self.center_x 498 | if self.options.center2[0] < 0 499 | else self.width * self.options.center2[0] 500 | ) 501 | self.center_y2 = ( 502 | self.center_y 503 | if self.options.center2[1] < 0 504 | else self.height * self.options.center2[1] 505 | ) 506 | 507 | def draw(self, x, y, time, bezier_time): 508 | second_start = self.options.second_start 509 | center_x = self.center_x 510 | center_y = self.center_y 511 | center_x2 = self.center_x2 512 | center_y2 = self.center_y2 513 | width = self.width 514 | height = self.height 515 | 516 | if bezier_time < second_start: 517 | dist_x = abs(x - center_x) / 2 518 | dist_y = abs(y - center_y) 519 | dist = (dist_x**2 + dist_y**2) ** 0.5 520 | 521 | max_dist = ( 522 | ((max(center_x, width - center_x)) / 2) ** 2 523 | + (max(center_y, height - center_y)) ** 2 524 | ) ** 0.5 525 | if dist > (1 - bezier_time * (1 / second_start)) * max_dist: 526 | set_pix(self.screen, x, y, color=self.color) 527 | else: 528 | dist_x = abs(x - center_x2) / 2 529 | dist_y = abs(y - center_y2) 530 | dist = (dist_x**2 + dist_y**2) ** 0.5 531 | 532 | max_dist = ( 533 | ((max(center_x2, width - center_x2)) / 2) ** 2 534 | + (max(center_y2, height - center_y2)) ** 2 535 | ) ** 0.5 536 | if ( 537 | dist 538 | < (1 - (bezier_time - second_start) / (1 - second_start)) * max_dist 539 | ): 540 | set_pix(self.screen, x, y, color=self.color) 541 | 542 | 543 | ############################################################################################################ 544 | 545 | def _main(stdscr,args): 546 | curses.curs_set(0) 547 | curses.start_color() 548 | curses.use_default_colors() 549 | curses.init_pair(1, curses.COLOR_BLACK, -1) 550 | make_frame = True 551 | 552 | match args.transition: 553 | case "scanline": 554 | transition = ScanLine(stdscr, args.duration, loop=args.loop, reverse=args.reverse, debug=args.debug, color=args.color, bezier=args.bezier) 555 | transition_name = "scanline" 556 | case "grow": 557 | transition = Grow(stdscr, args.duration, loop=args.loop, reverse=args.reverse, debug=args.debug, color=args.color, bezier=args.bezier) 558 | transition_name = "grow" 559 | case "shrink": 560 | transition = Shrink(stdscr, args.duration, loop=args.loop, reverse=args.reverse, debug=args.debug, color=args.color, bezier=args.bezier) 561 | transition_name = "shrink" 562 | case "growexit": 563 | transition = GrowExit(stdscr, args.duration, loop=args.loop, reverse=args.reverse, debug=args.debug, color=args.color, bezier=args.bezier) 564 | transition_name = "growexit" 565 | case "shrinkexit": 566 | transition = ShrinkExit(stdscr, args.duration, loop=args.loop, reverse=args.reverse, debug=args.debug, color=args.color, bezier=args.bezier) 567 | transition_name = "shrinkexit" 568 | case "doom": 569 | transition = Doom(stdscr, args.duration, loop=args.loop, reverse=args.reverse, debug=args.debug, color=args.color, bezier=args.bezier) 570 | transition_name = "doom" 571 | case _: 572 | transition = Transition(stdscr, args.duration, loop=args.loop, reverse=args.reverse, debug=args.debug, color=args.color, bezier=args.bezier) 573 | transition_name = "transition" 574 | 575 | transition.options = transition.Options() 576 | 577 | for field in transition.Options.__dataclass_fields__.keys(): 578 | if getattr(args,f"{transition_name}_{field}"): 579 | setattr(transition.options,field,getattr(args,f"{transition_name}_{field}")) 580 | 581 | try: 582 | while make_frame: 583 | current_width, current_height = stdscr.getmaxyx() 584 | if transition.width != current_width or transition.height != current_height: 585 | transition.update_dim() 586 | if not transition.frame_for_instant(time.time()): 587 | break 588 | except KeyboardInterrupt: 589 | pass 590 | 591 | 592 | def main(): 593 | 594 | parser = argparse.ArgumentParser( 595 | prog='STTT', 596 | description='a solution to your terminal transition tribulation' 597 | ) 598 | 599 | transitions = [ScanLine, Grow, Shrink, GrowExit, ShrinkExit,Doom] 600 | transition_names = [t.__name__.lower() for t in transitions] 601 | 602 | parser.add_argument( 603 | 'transition', 604 | type=str, 605 | choices=transition_names, 606 | help='transition to use', 607 | ) 608 | 609 | parser.add_argument( 610 | '-c', 611 | '--color', 612 | type=int, 613 | choices=range(0, 13), 614 | default=8, 615 | help='terminal color code of the color to use', 616 | ) 617 | 618 | parser.add_argument( 619 | '-d', 620 | '--duration', 621 | type=float, 622 | default=1, 623 | help='duration of transition', 624 | ) 625 | 626 | parser.add_argument( 627 | '-b', 628 | '--bezier', 629 | type=str, 630 | default="0.54,0.02,0.36,0.98", 631 | help='bezier curve for transition', 632 | ) 633 | 634 | parser.add_argument( 635 | '-l', 636 | '--loop', 637 | action='store_true', 638 | help='loop transition', 639 | ) 640 | 641 | parser.add_argument( 642 | '-r', 643 | '--reverse', 644 | action='store_true', 645 | help='reverse transition', 646 | ) 647 | 648 | parser.add_argument( 649 | '--debug', 650 | action='store_true', 651 | help='show debug info', 652 | ) 653 | 654 | def to_tuple(s,t=float): 655 | return tuple(map(t,s.split(","))) 656 | 657 | for transition in transitions: 658 | for field in transition.Options.__dataclass_fields__.keys(): 659 | if isinstance(getattr(transition.Options, field),tuple): 660 | parser.add_argument( 661 | f'--{transition.__name__.lower().replace("_","-")}-{field.replace("_","-")}', 662 | type=to_tuple, 663 | help=(getattr(transition.Options, f"_{field}_help")), 664 | ) 665 | else: 666 | parser.add_argument( 667 | f'--{transition.__name__.lower().replace("_","-")}-{field.replace("_","-")}', 668 | type=type(getattr(transition.Options, field)), 669 | help=(getattr(transition.Options, f"_{field}_help")), 670 | ) 671 | 672 | 673 | 674 | args = parser.parse_args() 675 | 676 | wrapper(_main,args) 677 | 678 | if __name__ == "__main__": 679 | main() 680 | --------------------------------------------------------------------------------