casca 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
casca-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Casca Contributors
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.
casca-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: casca
3
+ Version: 0.1.0
4
+ Summary: Native Python CLI UI library with CSS-like styling
5
+ Author-email: Abdallah <abdalla.zain2004@gmail.com>
6
+ License: MIT
7
+ Keywords: tui,cli,terminal,ui,css,python
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Topic :: Software Development :: User Interfaces
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: chatbot
18
+ Requires-Dist: groq>=0.20.0; extra == "chatbot"
19
+ Provides-Extra: docs
20
+ Requires-Dist: mkdocs>=1.6.0; extra == "docs"
21
+ Requires-Dist: mkdocs-material>=9.5.0; extra == "docs"
22
+ Requires-Dist: pymdown-extensions>=10.8.0; extra == "docs"
23
+ Dynamic: license-file
24
+
25
+ # Casca
26
+
27
+ Casca is a lightweight, native Python TUI library with a DOM-like widget tree and CSS-style theming.
28
+
29
+ ## Features
30
+
31
+ - Zero core dependencies (standard library runtime)
32
+ - Declarative widgets (`Container`, `Label`, `Input`, `Button`, `Card`, `Header`, `ScrollView`)
33
+ - CSS-like style parsing and cascade
34
+ - Keyboard and mouse event handling in raw terminal mode
35
+ - Real examples including login screen, dashboard, and Groq chatbot
36
+
37
+ ## Quick Start
38
+
39
+ ```python
40
+ from casca import Container, Label, run_app
41
+
42
+ CSS = """
43
+ #root {
44
+ padding: 1;
45
+ border: ascii;
46
+ }
47
+ """
48
+
49
+
50
+
51
+ ui_tree = Container(
52
+ Label("Hello from Casca"),
53
+ id="root",
54
+ )
55
+
56
+ if __name__ == "__main__":
57
+ run_app(ui_tree, css=CSS)
58
+ ```
59
+
60
+ ## Run Use Cases
61
+
62
+ ```bash
63
+ python3 -m use_cases.login_screen
64
+ python3 -m use_cases.dashboard
65
+ python3 -m use_cases.chatbot
66
+ ```
67
+
68
+ `use_cases.chatbot` requires `groq` and a `GROQ_API_KEY`.
69
+
70
+
71
+ ## Project Layout
72
+
73
+ - `casca/`: core library
74
+ - `use_cases/`: real apps built with Casca
75
+ - `examples/`: smaller demos
76
+ - `docs/`: MkDocs content
77
+
78
+ ## License
79
+
80
+ MIT
casca-0.1.0/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # Casca
2
+
3
+ Casca is a lightweight, native Python TUI library with a DOM-like widget tree and CSS-style theming.
4
+
5
+ ## Features
6
+
7
+ - Zero core dependencies (standard library runtime)
8
+ - Declarative widgets (`Container`, `Label`, `Input`, `Button`, `Card`, `Header`, `ScrollView`)
9
+ - CSS-like style parsing and cascade
10
+ - Keyboard and mouse event handling in raw terminal mode
11
+ - Real examples including login screen, dashboard, and Groq chatbot
12
+
13
+ ## Quick Start
14
+
15
+ ```python
16
+ from casca import Container, Label, run_app
17
+
18
+ CSS = """
19
+ #root {
20
+ padding: 1;
21
+ border: ascii;
22
+ }
23
+ """
24
+
25
+
26
+
27
+ ui_tree = Container(
28
+ Label("Hello from Casca"),
29
+ id="root",
30
+ )
31
+
32
+ if __name__ == "__main__":
33
+ run_app(ui_tree, css=CSS)
34
+ ```
35
+
36
+ ## Run Use Cases
37
+
38
+ ```bash
39
+ python3 -m use_cases.login_screen
40
+ python3 -m use_cases.dashboard
41
+ python3 -m use_cases.chatbot
42
+ ```
43
+
44
+ `use_cases.chatbot` requires `groq` and a `GROQ_API_KEY`.
45
+
46
+
47
+ ## Project Layout
48
+
49
+ - `casca/`: core library
50
+ - `use_cases/`: real apps built with Casca
51
+ - `examples/`: smaller demos
52
+ - `docs/`: MkDocs content
53
+
54
+ ## License
55
+
56
+ MIT
@@ -0,0 +1,37 @@
1
+ from .core.app import App
2
+ from .core.events import KeyEvent, ResizeEvent
3
+ from .core.terminal import RawTerminal, get_terminal_size
4
+ from .core.ansi import color, Color, BackColor, move_cursor, CLEAR_SCREEN, RESET
5
+ from .dom.node import Widget
6
+ from .components.base import Container, Label, Button
7
+ from .components.forms import Checkbox, Input
8
+ from .components.panels import Card, Header
9
+ from .components.scroll import ScrollView
10
+ from .style.css import parse_css
11
+
12
+ __all__ = [
13
+ "App",
14
+ "KeyEvent",
15
+ "ResizeEvent",
16
+ "RawTerminal",
17
+ "get_terminal_size",
18
+ "color",
19
+ "Color",
20
+ "BackColor",
21
+ "move_cursor",
22
+ "CLEAR_SCREEN",
23
+ "RESET",
24
+ "Widget",
25
+ "Container",
26
+ "Label",
27
+ "Button",
28
+ "Checkbox",
29
+ "Input",
30
+ "Card",
31
+ "Header",
32
+ "ScrollView",
33
+ "parse_css",
34
+ ]
35
+
36
+ from .core.app import run_app
37
+ __all__.append('run_app')
@@ -0,0 +1 @@
1
+ from .scroll import ScrollView
@@ -0,0 +1,78 @@
1
+ from ..dom.node import Widget
2
+ from ..core.ansi import move_cursor
3
+ from ..core.events import MouseEvent
4
+ import textwrap
5
+
6
+ class Container(Widget):
7
+ """A generic div-like container for laying out children."""
8
+ tag = 'container'
9
+
10
+
11
+ class Label(Widget):
12
+ """Displays text. Implements text-wrapping based on layout boundaries."""
13
+ tag = 'label'
14
+
15
+ def __init__(self, text: str, *args, **kwargs):
16
+ super().__init__(*args, **kwargs)
17
+ self.text = text
18
+ self.lines = []
19
+
20
+ def calculate_layout(self, available_width: int, available_height: int) -> tuple[int, int]:
21
+ padding = int(self.style.get('padding', 0))
22
+ margin = int(self.style.get('margin', 0))
23
+
24
+ # Calculate available inner width to check wrapping
25
+ inner_width = available_width - (margin * 2) - (padding * 2)
26
+ w_prop = self.props.get('width') or self.style.get('width')
27
+ if w_prop:
28
+ inner_width = int(w_prop) - (padding * 2)
29
+
30
+ # Parse Text
31
+ self.lines = []
32
+ for line in self.text.split('\\n'):
33
+ if inner_width > 0:
34
+ wrapped = textwrap.wrap(line, max(1, inner_width))
35
+ if not wrapped:
36
+ self.lines.append("")
37
+ else:
38
+ self.lines.extend(wrapped)
39
+ else:
40
+ self.lines.append(line)
41
+
42
+ self.content_height = len(self.lines)
43
+ self.content_width = max([len(l) for l in self.lines] + [0])
44
+
45
+ self.width = self.content_width + padding * 2
46
+ self.height = self.content_height + padding * 2
47
+
48
+ return self.width + margin * 2, self.height + margin * 2
49
+
50
+ def render_content(self, app):
51
+ padding = int(self.style.get('padding', 0))
52
+
53
+ for i, line in enumerate(self.lines):
54
+ app.draw_text(self.x + padding + 1, self.y + padding + i + 1, line)
55
+
56
+
57
+ class Button(Label):
58
+ """Clickable element. Triggers on_click function."""
59
+ tag = 'button'
60
+
61
+ def __init__(self, text: str, *args, on_click=None, **kwargs):
62
+ super().__init__(text, *args, **kwargs)
63
+ self.on_click = on_click
64
+
65
+ def handle_input(self, event) -> bool:
66
+ if event.key == '\r' or event.key == ' ': # Enter or Space
67
+ if self.on_click:
68
+ self.on_click(event)
69
+ return True
70
+ return super().handle_input(event)
71
+
72
+ def on_mouse(self, event: MouseEvent) -> bool:
73
+ # Trigger on mouse release for primary button
74
+ if event.button == 0 and not event.pressed:
75
+ if self.on_click:
76
+ self.on_click(event)
77
+ return True
78
+ return False
@@ -0,0 +1,101 @@
1
+ from ..dom.node import Widget
2
+ from ..core.events import KeyEvent, MouseEvent
3
+ from ..core.ansi import move_cursor
4
+ from .base import Label, Container
5
+
6
+ class Checkbox(Widget):
7
+ """A selectable checkbox."""
8
+ tag = 'checkbox'
9
+
10
+ def __init__(self, label_text: str, checked: bool = False, *args, on_change=None, **kwargs):
11
+ super().__init__(*args, **kwargs)
12
+ self.label_text = label_text
13
+ self.checked = checked
14
+ self.on_change = on_change
15
+
16
+ def calculate_layout(self, available_width: int, available_height: int) -> tuple[int, int]:
17
+ self.content_height = 1
18
+ self.content_width = len(self.label_text) + 4 # "[x] " + text
19
+
20
+ padding = int(self.style.get('padding', 0))
21
+ margin = int(self.style.get('margin', 0))
22
+
23
+ self.width = self.content_width + padding * 2
24
+ self.height = self.content_height + padding * 2
25
+ return self.width + margin * 2, self.height + margin * 2
26
+
27
+ def render_content(self, app):
28
+ padding = int(self.style.get('padding', 0))
29
+ state_char = 'x' if self.checked else ' '
30
+
31
+ app.draw_text(self.x + padding + 1, self.y + padding + 1, f"[{state_char}] {self.label_text}")
32
+
33
+ def handle_input(self, event: KeyEvent) -> bool:
34
+ if event.key == '\r' or event.key == ' ': # Toggle on Enter or Space
35
+ self.checked = not self.checked
36
+ if self.on_change:
37
+ self.on_change(self.checked)
38
+ return True
39
+ return super().handle_input(event)
40
+
41
+ def on_mouse(self, event: MouseEvent) -> bool:
42
+ # Toggle on left click release
43
+ if event.button == 0 and not event.pressed:
44
+ self.checked = not self.checked
45
+ if self.on_change:
46
+ self.on_change(self.checked)
47
+ return True
48
+ return False
49
+
50
+ class Input(Widget):
51
+ """A simple one-line text input field."""
52
+ tag = 'input'
53
+
54
+ def __init__(self, placeholder: str = "", default_value: str = "", *args, on_submit=None, **kwargs):
55
+ super().__init__(*args, **kwargs)
56
+ self.placeholder = placeholder
57
+ self.value = default_value
58
+ self.on_submit = on_submit
59
+
60
+ # Make input take up full remaining width by default unless specified
61
+ if 'width' not in self.props:
62
+ self.style['width'] = "20"
63
+
64
+ def calculate_layout(self, available_width: int, available_height: int) -> tuple[int, int]:
65
+ self.content_height = 1
66
+ padding = int(self.style.get('padding', 0))
67
+ margin = int(self.style.get('margin', 0))
68
+
69
+ self.width = self.props.get('width', int(self.style.get('width', available_width)))
70
+ self.height = self.content_height + padding * 2
71
+
72
+ return self.width + margin * 2, self.height + margin * 2
73
+
74
+ def render_content(self, app):
75
+ padding = int(self.style.get('padding', 0))
76
+
77
+ display_text = self.value if self.value else self.placeholder
78
+
79
+ if self.focused:
80
+ display_text += "█"
81
+
82
+ app.draw_text(self.x + padding + 1, self.y + padding + 1, display_text[:self.width - (padding * 2)])
83
+
84
+ def on_mouse(self, event: MouseEvent) -> bool:
85
+ if event.pressed:
86
+ return True
87
+ return False
88
+
89
+ def handle_input(self, event: KeyEvent) -> bool:
90
+ # Simplistic typing
91
+ if event.key in ('\x08', '\x7f', '\b'): # Backspace / Delete
92
+ self.value = self.value[:-1]
93
+ return True
94
+ elif event.key in ('\r', '\n'):
95
+ if self.on_submit:
96
+ self.on_submit(self.value)
97
+ return True
98
+ elif event.is_printable:
99
+ self.value += event.key
100
+ return True
101
+ return super().handle_input(event)
@@ -0,0 +1,26 @@
1
+ from .base import Container, Label
2
+
3
+ class Card(Container):
4
+ """A Container with predefined standard styling parameters for typical Card UI."""
5
+ tag = 'card'
6
+
7
+ def __init__(self, title: str, *children, **kwargs):
8
+ # Insert a Label as the first child to serve as the card title
9
+ title_label = Label(f" {title} ", classes="card-title")
10
+ super().__init__(title_label, *children, **kwargs)
11
+
12
+ def resolve_styles(self, stylesheet: dict):
13
+ # We can dynamically force a border on cards if not overriden
14
+ if 'border' not in self.style:
15
+ self.style['border'] = 'ascii'
16
+ super().resolve_styles(stylesheet)
17
+
18
+
19
+ class Header(Container):
20
+ """A wide container designed to sit at the top of an app."""
21
+ tag = 'header'
22
+
23
+ def resolve_styles(self, stylesheet: dict):
24
+ # A header should naturally want to span 100% of width
25
+ # (Though our layout engine is basic right now, we can set width to a high number or handled by parent layout later)
26
+ super().resolve_styles(stylesheet)
@@ -0,0 +1,148 @@
1
+ from ..dom.node import Widget
2
+ from ..core.events import KeyEvent, MouseEvent
3
+ from ..core.ansi import RESET
4
+
5
+ class ScrollView(Widget):
6
+ """A container that clips its children vertically and allows scrolling."""
7
+ tag = 'scrollview'
8
+
9
+ def __init__(self, *args, **kwargs):
10
+ super().__init__(*args, **kwargs)
11
+ self.scroll_y = 0
12
+ self.max_scroll = 0
13
+
14
+ def calculate_layout(self, available_width: int, available_height: int) -> tuple[int, int]:
15
+ padding = int(self.style.get('padding', 0))
16
+ margin = int(self.style.get('margin', 0))
17
+ border_width = 1 if 'border' in self.style else 0
18
+
19
+ self.width = self.props.get('width', int(self.style.get('width', available_width)))
20
+
21
+ set_height = self.props.get('height') or self.style.get('height')
22
+ if set_height:
23
+ self.height = int(set_height)
24
+ else:
25
+ self.height = available_height
26
+
27
+ inner_width = max(0, self.width - 2*(padding + border_width))
28
+
29
+ current_y = 0
30
+ max_child_width = 0
31
+
32
+ # Calculate children sizes assuming infinite height
33
+ for child in self.children:
34
+ child.x = self.x + padding + border_width
35
+ child.y = self.y + padding + border_width + current_y
36
+
37
+ child_w, child_h = child.calculate_layout(inner_width, 9999)
38
+ current_y += child_h
39
+ max_child_width = max(max_child_width, child_w)
40
+
41
+ self.content_height = current_y
42
+ self.content_width = max_child_width
43
+
44
+ inner_height = max(0, self.height - 2*(padding + border_width))
45
+ self.max_scroll = max(0, self.content_height - inner_height)
46
+
47
+
48
+ # Determine if content grew to auto-scroll if requested
49
+ was_at_bottom = hasattr(self, '_last_max') and self.scroll_y >= self._last_max
50
+ is_first_layout = not hasattr(self, '_last_max')
51
+
52
+ if self.props.get('scroll_to_bottom'):
53
+ if is_first_layout or was_at_bottom:
54
+ self.scroll_y = self.max_scroll
55
+
56
+ self._last_max = self.max_scroll
57
+
58
+ if self.scroll_y > self.max_scroll:
59
+ self.scroll_y = self.max_scroll
60
+ if self.scroll_y < 0:
61
+ self.scroll_y = 0
62
+
63
+ # Apply scroll offset conceptually (modifying y directly for rendering)
64
+ # Note: If layout recalculates periodically, this is fine because we recalculate before render
65
+ self._apply_scroll_offset()
66
+
67
+ return self.width + margin * 2, self.height + margin * 2
68
+
69
+ def _apply_scroll_offset(self):
70
+ def shift(node, offset):
71
+ node.y -= offset
72
+ for c in node.children:
73
+ shift(c, offset)
74
+
75
+ for child in self.children:
76
+ shift(child, self.scroll_y)
77
+
78
+ def render(self, app):
79
+ padding = int(self.style.get('padding', 0))
80
+ border_width = 1 if 'border' in self.style else 0
81
+
82
+ min_y = self.y + padding + border_width + 1
83
+ max_y = min_y + max(0, self.height - 2*(padding + border_width))
84
+ min_x = self.x + padding + border_width + 1
85
+ max_x = min_x + max(0, self.width - 2*(padding + border_width))
86
+
87
+ prev_clip = getattr(app, 'clip_rect', None)
88
+ # We need to intersect with existing clip rect if any (skip for simplicity for now, assume 1 scrollview depth)
89
+ app.clip_rect = (min_y, max_y, min_x, max_x)
90
+
91
+ self.apply_style(app)
92
+
93
+ if 'background' in self.style:
94
+ for r in range(self.height):
95
+ app.draw_text(self.x + 1, self.y + r + 1, " " * self.width, apply_clip=False)
96
+ self.apply_style(app)
97
+
98
+ if self.style.get('border', 'none') != 'none':
99
+ self._draw_border(app)
100
+
101
+ self._draw_scrollbar(app)
102
+
103
+ for child in self.children:
104
+ child.render(app)
105
+ self.apply_style(app)
106
+
107
+ app.clip_rect = prev_clip
108
+ app.write(RESET)
109
+
110
+ def _draw_scrollbar(self, app):
111
+ inner_height = max(0, self.height - 2)
112
+ if self.max_scroll > 0 and inner_height > 0:
113
+ thumb_size = max(1, int(inner_height * (inner_height / (self.content_height or 1))))
114
+ thumb_pos = int((self.scroll_y / self.max_scroll) * (inner_height - thumb_size))
115
+
116
+ for i in range(inner_height):
117
+ char = '█' if thumb_pos <= i < thumb_pos + thumb_size else '▒'
118
+ app.draw_text(self.x + self.width - 1, self.y + 2 + i, char, apply_clip=False)
119
+
120
+ def handle_input(self, event: KeyEvent) -> bool:
121
+ # Check kids first
122
+ if super().handle_input(event):
123
+ return True
124
+
125
+ # Scroll up and down on arrow keys if we have standard keys mapping
126
+ if event.key == '\x1b[A': # Up Arrow
127
+ self.scroll_y = max(0, self.scroll_y - 1)
128
+ return True
129
+ elif event.key == '\x1b[B': # Down arrow
130
+ self.scroll_y = min(self.max_scroll, self.scroll_y + 1)
131
+ return True
132
+
133
+ return False
134
+
135
+ def handle_wheel(self, event: MouseEvent, app) -> bool:
136
+ term_x = event.x - 1
137
+ term_y = event.y - 1
138
+
139
+ if self.x <= term_x < self.x + self.width and self.y <= term_y < self.y + self.height:
140
+ if event.button == 64:
141
+ self.scroll_y = max(0, self.scroll_y - 1)
142
+ if app._running: app._render_cycle()
143
+ return True
144
+ elif event.button == 65:
145
+ self.scroll_y = min(self.max_scroll, self.scroll_y + 1)
146
+ if app._running: app._render_cycle()
147
+ return True
148
+ return False
File without changes
@@ -0,0 +1,48 @@
1
+ from enum import Enum
2
+
3
+ class Color(Enum):
4
+ BLACK = 30
5
+ RED = 31
6
+ GREEN = 32
7
+ YELLOW = 33
8
+ BLUE = 34
9
+ MAGENTA = 35
10
+ CYAN = 36
11
+ WHITE = 37
12
+ DEFAULT = 39
13
+
14
+ class BackColor(Enum):
15
+ BLACK = 40
16
+ RED = 41
17
+ GREEN = 42
18
+ YELLOW = 43
19
+ BLUE = 44
20
+ MAGENTA = 45
21
+ CYAN = 46
22
+ WHITE = 47
23
+ DEFAULT = 49
24
+
25
+ ESC = '\x1b['
26
+ CLEAR_SCREEN = f"{ESC}2J"
27
+ ALT_SCREEN_ON = f"{ESC}?1049h"
28
+ ALT_SCREEN_OFF = f"{ESC}?1049l"
29
+ HIDE_CURSOR = f"{ESC}?25l"
30
+ SHOW_CURSOR = f"{ESC}?25h"
31
+ MOUSE_ON = f"{ESC}?1000h{ESC}?1006h"
32
+ MOUSE_OFF = f"{ESC}?1006l{ESC}?1000l"
33
+
34
+ # Text Styling
35
+ BOLD = f"{ESC}1m"
36
+ FAINT = f"{ESC}2m"
37
+ ITALIC = f"{ESC}3m"
38
+ UNDERLINE = f"{ESC}4m"
39
+
40
+ def move_cursor(x: int, y: int) -> str:
41
+ """Move cursor to 1-based x, y position."""
42
+ return f"{ESC}{y};{x}H"
43
+
44
+ def color(fg: Color = Color.DEFAULT, bg: BackColor = BackColor.DEFAULT) -> str:
45
+ """Set foreground and background color."""
46
+ return f"{ESC}{fg.value};{bg.value}m"
47
+
48
+ RESET = f"{ESC}0m"
@@ -0,0 +1,236 @@
1
+ import sys
2
+ import select
3
+ import signal
4
+ import re
5
+ import os
6
+
7
+ from .terminal import RawTerminal, get_terminal_size
8
+ from .ansi import CLEAR_SCREEN, move_cursor, RESET
9
+ from .events import KeyEvent, ResizeEvent, MouseEvent
10
+ from ..style.css import parse_css
11
+
12
+ class App:
13
+ """
14
+ The main application context and event loop.
15
+ Provides a base class for rendering and handling events.
16
+ """
17
+ css = ""
18
+
19
+ def __init__(self):
20
+ self._running = False
21
+ self._last_size = get_terminal_size()
22
+
23
+ # Buffer to accumulate output before flushing (double buffer concept)
24
+ self._output_buffer = []
25
+ self._input_buffer = ""
26
+
27
+ # DOM Root & Focus State
28
+ self.root = None
29
+ self.stylesheet = parse_css(self.css)
30
+ self.focused_widget = None
31
+ self.focused_id = None
32
+
33
+ def run(self):
34
+ """Starts the event loop."""
35
+ self._running = True
36
+
37
+ # Attach to SIGWINCH for resize events in Unix
38
+ try:
39
+ signal.signal(signal.SIGWINCH, self._handle_sigwinch)
40
+ except AttributeError:
41
+ # SIGWINCH is not available on all systems (e.g. Windows)
42
+ pass
43
+
44
+ with RawTerminal():
45
+ self._render_cycle()
46
+
47
+ while self._running:
48
+ # Wait for input (0.05s timeout for 20 FPS refresh rate max)
49
+ dr, _, _ = select.select([sys.stdin], [], [], 0.05)
50
+
51
+ if dr:
52
+ # Read all available bytes
53
+ try:
54
+ data = os.read(sys.stdin.fileno(), 1024).decode('utf-8', errors='replace')
55
+ except BlockingIOError:
56
+ continue
57
+
58
+ if not data:
59
+ continue
60
+
61
+ # Extract all SGR Mouse Events iteratively
62
+ mouse_pattern = r'\x1b\[<(\d+);(\d+);(\d+)([mM])'
63
+ for match in re.finditer(mouse_pattern, data):
64
+ btn, x, y, state = match.groups()
65
+ is_pressed = (state == 'M')
66
+ mouse_evt = MouseEvent(x=int(x), y=int(y), button=int(btn), pressed=is_pressed)
67
+ self.handle_mouse(mouse_evt)
68
+
69
+ # Remove parsed mouse events
70
+ data = re.sub(mouse_pattern, '', data)
71
+
72
+ # Quick checks for remaining inputs
73
+ if '\x03' in data or '\x04' in data:
74
+ self.exit()
75
+
76
+ # Split remaining ANSI escape codes or parse them.
77
+ # For simplicity, if we see an escape sequence, treat the block roughly.
78
+ # Otherwise handle pure characters
79
+ i = 0
80
+ while i < len(data):
81
+ if data[i] == '\x1b':
82
+ # Probably an escape sequence (like arrows)
83
+ # Just capture roughly until a letter
84
+ end = i + 1
85
+ while end < len(data) and not data[end].isalpha() and data[end] != '~':
86
+ end += 1
87
+ if end < len(data): end += 1
88
+
89
+ sub = data[i:end]
90
+ self.handle_input(KeyEvent(key=sub, is_printable=False))
91
+ i = end
92
+ else:
93
+ char = data[i]
94
+ # Only handle printable or basic backspaces
95
+ if char in ('\x08', '\x7f', '\r', '\n', '\t') or char.isprintable():
96
+ # In raw mode, we might receive \r for enter.
97
+ is_print = char.isprintable()
98
+ if char in ('\r', '\n', '\t'):
99
+ is_print = False
100
+ self.handle_input(KeyEvent(key=char, is_printable=is_print))
101
+ i += 1
102
+
103
+ # Request a re-render after an input if app is still running
104
+ if self._running:
105
+ self._render_cycle()
106
+
107
+ # Optionally check if size changed periodically if SIGWINCH failed
108
+ current_size = get_terminal_size()
109
+ if current_size != self._last_size:
110
+ self._last_size = current_size
111
+ self.handle_resize(ResizeEvent(*current_size))
112
+ self._render_cycle()
113
+
114
+ def exit(self):
115
+ """Signals the app to break out of the event loop."""
116
+ self._running = False
117
+
118
+ def write(self, text: str):
119
+ """Appends output to the render buffer."""
120
+ self._output_buffer.append(text)
121
+
122
+ def draw_text(self, x: int, y: int, text: str, apply_clip=True):
123
+ """Helper to draw text with optional vertical clipping."""
124
+ if apply_clip and hasattr(self, 'clip_rect') and self.clip_rect:
125
+ min_y, max_y, min_x, max_x = self.clip_rect
126
+ if y < min_y or y >= max_y:
127
+ return
128
+ # horizontal clipping
129
+ if x < min_x:
130
+ text = text[min_x - x:]
131
+ x = min_x
132
+ if x + len(text) > max_x:
133
+ text = text[:max_x - x]
134
+ if not text:
135
+ return
136
+ self.write(move_cursor(x, y))
137
+ self.write(text)
138
+
139
+ def flush(self):
140
+ """Writes the entire render buffer to the stdout in one go to prevent flicker."""
141
+ if self._output_buffer:
142
+ sys.stdout.write("".join(self._output_buffer))
143
+ sys.stdout.flush()
144
+ self._output_buffer.clear()
145
+
146
+ def _render_cycle(self):
147
+ """Internal render pass to orchestrate drawing."""
148
+ self.write(CLEAR_SCREEN)
149
+ self.write(move_cursor(1, 1))
150
+
151
+ # Call user's render method
152
+ self.render()
153
+
154
+ self.write(RESET)
155
+ self.flush()
156
+
157
+ def _handle_sigwinch(self, signum, frame):
158
+ """Unix signal handler for terminal resize."""
159
+ size = get_terminal_size()
160
+ self._last_size = size
161
+ self.handle_resize(ResizeEvent(width=size[0], height=size[1]))
162
+ if self._running:
163
+ self._render_cycle()
164
+
165
+ # --- Methods to be overridden by subclasses (User API) ---
166
+
167
+ def handle_input(self, event: KeyEvent):
168
+ """Triggered when a key is pressed."""
169
+ # By default, press 'q' to quit. Subclasses can override.
170
+ if event.key == 'q':
171
+ self.exit()
172
+
173
+ # Prioritize focused widget, else try routing through the DOM
174
+ consumed = False
175
+ if self.focused_widget:
176
+ consumed = self.focused_widget.handle_input(event)
177
+
178
+ if not consumed and self.root:
179
+ self.root.handle_input(event)
180
+
181
+ def handle_mouse(self, event: MouseEvent):
182
+ """Triggered on mouse interaction."""
183
+ if self.root:
184
+ self.root.handle_mouse(event, self)
185
+
186
+ def handle_resize(self, event: ResizeEvent):
187
+ """Triggered when the terminal window changes size."""
188
+ pass
189
+
190
+ def build_ui(self):
191
+ """Override this to return the root widget tree."""
192
+ return None
193
+
194
+ def render(self):
195
+ """
196
+ The main rendering function.
197
+ Rebuilds styling and layout, then renders the full Widget tree.
198
+ """
199
+ if not self.root:
200
+ self.root = self.build_ui()
201
+
202
+ # Restore focus after a UI rebuild if an element ID was previously focused
203
+ if self.focused_id:
204
+ def restore_focus(node):
205
+ if node.id == self.focused_id:
206
+ node.focused = True
207
+ self.focused_widget = node
208
+ for c in node.children:
209
+ restore_focus(c)
210
+ self.focused_widget = None
211
+ restore_focus(self.root)
212
+
213
+ if self.root:
214
+ # 1. Resolve CSS
215
+ self.root.resolve_styles(self.stylesheet)
216
+ # 2. Assign root X/Y
217
+ self.root.x = 0
218
+ self.root.y = 0
219
+ # 3. Calculate Layout
220
+ w, h = self._last_size
221
+ self.root.calculate_layout(w, h)
222
+ # 4. Render Layout
223
+ self.root.render(self)
224
+ else:
225
+ self.write(f"Casca UI - Press 'q' to exit.\\n Terminal size: {self._last_size}")
226
+
227
+ def run_app(widget, css=""):
228
+ """A convenience wrapper to run a single widget without subclassing App."""
229
+ class WrapperApp(App):
230
+ def __init__(self):
231
+ super().__init__()
232
+ self.css = css
233
+ def build_ui(self):
234
+ return widget
235
+
236
+ WrapperApp().run()
@@ -0,0 +1,18 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass
4
+ class KeyEvent:
5
+ key: str
6
+ is_printable: bool
7
+
8
+ @dataclass
9
+ class ResizeEvent:
10
+ width: int
11
+ height: int
12
+
13
+ @dataclass
14
+ class MouseEvent:
15
+ x: int
16
+ y: int
17
+ button: int
18
+ pressed: bool
@@ -0,0 +1,48 @@
1
+ import os
2
+ import sys
3
+ import tty
4
+ import termios
5
+
6
+ from .ansi import ALT_SCREEN_ON, ALT_SCREEN_OFF, HIDE_CURSOR, SHOW_CURSOR, MOUSE_ON, MOUSE_OFF
7
+
8
+
9
+ class RawTerminal:
10
+ """
11
+ Context manager to safely put the terminal into raw mode,
12
+ switch to the alternate screen buffer, and hide the cursor.
13
+ """
14
+ def __init__(self):
15
+ self.fd = sys.stdin.fileno()
16
+ self.old_settings = None
17
+
18
+ def __enter__(self):
19
+ # Save old terminal settings
20
+ self.old_settings = termios.tcgetattr(self.fd)
21
+
22
+ # Set raw mode to read characters immediately
23
+ tty.setraw(self.fd)
24
+
25
+ # Switch to alternate screen, hide cursor, enable mouse tracking
26
+ sys.stdout.write(ALT_SCREEN_ON)
27
+ sys.stdout.write(HIDE_CURSOR)
28
+ sys.stdout.write(MOUSE_ON)
29
+ sys.stdout.flush()
30
+ return self
31
+
32
+ def __exit__(self, exc_type, exc_val, exc_tb):
33
+ # Restore normal screen, show cursor, disable mouse
34
+ sys.stdout.write(MOUSE_OFF)
35
+ sys.stdout.write(ALT_SCREEN_OFF)
36
+ sys.stdout.write(SHOW_CURSOR)
37
+ sys.stdout.flush()
38
+
39
+ # Restore terminal settings
40
+ termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old_settings)
41
+
42
+ def get_terminal_size() -> tuple[int, int]:
43
+ """Returns (width, height) of the current terminal."""
44
+ try:
45
+ size = os.get_terminal_size()
46
+ return size.columns, size.lines
47
+ except OSError:
48
+ return 80, 24
File without changes
@@ -0,0 +1,184 @@
1
+ from typing import Optional, List
2
+ from ..core.ansi import Color, BackColor, color, move_cursor, BOLD, FAINT, ITALIC, UNDERLINE, RESET
3
+ from ..core.events import KeyEvent, MouseEvent
4
+
5
+ class Widget:
6
+ """The base class for all UI elements (DOM Node)."""
7
+
8
+ tag = 'widget'
9
+
10
+ def __init__(self, *children, id: str = "", classes: str = "", **props):
11
+ self.children: List['Widget'] = list(children)
12
+ self.id = id
13
+ self.classes = classes.split() if classes else []
14
+ self.props = props
15
+
16
+ # Layout properties (calculated)
17
+ self.x = 0
18
+ self.y = 0
19
+ self.width = 0
20
+ self.height = 0
21
+ self.content_width = 0
22
+ self.content_height = 0
23
+
24
+ # Resolved styles (from CSS)
25
+ self.style = {}
26
+
27
+ # State
28
+ self.focused = False
29
+
30
+ def resolve_styles(self, stylesheet: dict):
31
+ from ..style.css import get_style_for_node
32
+ self.style = get_style_for_node(self.id, self.classes, self.tag, stylesheet)
33
+
34
+ # Recursively resolve styles for children
35
+ for child in self.children:
36
+ child.resolve_styles(stylesheet)
37
+
38
+ def calculate_layout(self, available_width: int, available_height: int) -> tuple[int, int]:
39
+ """Base layout logic Flow (stacking). Returns tuple of (width_used, height_used)."""
40
+
41
+ # Apply padding and border calculations
42
+ padding = int(self.style.get('padding', 0))
43
+ margin = int(self.style.get('margin', 0))
44
+ border_width = 1 if 'border' in self.style else 0
45
+
46
+ # Width priority: Direct property -> Inline style -> Available parent width
47
+ self.width = self.props.get('width', int(self.style.get('width', available_width)))
48
+
49
+ # Content layout logic (stack children vertically by default)
50
+ current_y = padding + border_width
51
+ max_child_width = 0
52
+
53
+ for child in self.children:
54
+ child.x = self.x + padding + border_width
55
+ child.y = self.y + current_y
56
+
57
+ # The children can occupy the inner space
58
+ inner_avail_width = max(0, self.width - 2*(padding + border_width))
59
+
60
+ child_w, child_h = child.calculate_layout(inner_avail_width, available_height - current_y)
61
+ current_y += child_h
62
+ max_child_width = max(max_child_width, child_w)
63
+
64
+ self.content_width = max_child_width
65
+ self.content_height = current_y - (padding + border_width)
66
+
67
+ # Total height used by this widget including padding, border and margin
68
+ min_height = padding * 2 + border_width * 2
69
+
70
+ # If height is strictly set
71
+ set_height = self.props.get('height') or self.style.get('height')
72
+ if set_height:
73
+ self.height = int(set_height)
74
+ else:
75
+ self.height = self.content_height + min_height
76
+
77
+ return self.width + margin * 2, self.height + margin * 2
78
+
79
+ def apply_style(self, app):
80
+ """Applies the current widget's computed style and background to the terminal output."""
81
+ app.write(RESET)
82
+ border_style = self.style.get('border', 'none')
83
+ fg_color = self.style.get('color', 'DEFAULT').upper()
84
+ bg_color = self.style.get('background', 'DEFAULT').upper()
85
+
86
+ fg = getattr(Color, fg_color, Color.DEFAULT)
87
+ bg = getattr(BackColor, bg_color, BackColor.DEFAULT)
88
+ app.write(color(fg, bg))
89
+
90
+ # Apply text styling
91
+ if self.style.get('font-weight') == 'bold': app.write(BOLD)
92
+ if self.style.get('font-style') == 'italic': app.write(ITALIC)
93
+ if self.style.get('text-decoration') == 'underline': app.write(UNDERLINE)
94
+ if 'opacity' in self.style and float(self.style['opacity']) < 1.0: app.write(FAINT)
95
+
96
+ def render(self, app):
97
+ """Draws the widget to the app render buffer."""
98
+ self.apply_style(app)
99
+
100
+ # Draw background fill if applicable
101
+ if 'background' in self.style:
102
+ for r in range(self.height):
103
+ app.draw_text(self.x + 1, self.y + r + 1, " " * self.width)
104
+ self.apply_style(app)
105
+
106
+ # Draw border
107
+ if self.style.get('border', 'none') != 'none':
108
+ self._draw_border(app)
109
+
110
+ # Draw own content
111
+ self.render_content(app)
112
+
113
+ # Draw children
114
+ for child in(self.children):
115
+ child.render(app)
116
+ # Reattach own style so subsequent children/parents don't get reset
117
+ self.apply_style(app)
118
+
119
+ app.write(RESET)
120
+
121
+ def render_content(self, app):
122
+ """Override to implement specific widget text / drawing logic."""
123
+ pass
124
+
125
+ def _draw_border(self, app):
126
+ border_char = self.style.get('border-char', '+') if self.style.get('border') == 'ascii' else '█'
127
+ # Top and bottom
128
+ for r in (0, self.height - 1):
129
+ if r >= 0 and self.width > 0:
130
+ app.draw_text(self.x + 1, self.y + r + 1, border_char * self.width)
131
+
132
+ # Sides
133
+ for r in range(1, self.height - 1):
134
+ if self.width > 0:
135
+ app.draw_text(self.x + 1, self.y + r + 1, border_char)
136
+ if self.width > 1:
137
+ app.draw_text(self.x + self.width, self.y + r + 1, border_char)
138
+
139
+ def handle_input(self, event: KeyEvent) -> bool:
140
+ """
141
+ Returns True if the event was consumed, preventing bubbling.
142
+ Passes input down the tree.
143
+ """
144
+ for child in reversed(self.children):
145
+ if child.handle_input(event):
146
+ return True
147
+ return False
148
+
149
+ def handle_mouse(self, event: MouseEvent, app) -> bool:
150
+ """Passes mouse events down if they fall within bounds."""
151
+ # Terminal coordinates are typically 1-based, and our widget coordinates (x,y)
152
+ # may be 0-based logically, but rendered 1-based.
153
+ # Actually our calculate_layout produces 0-based x, y. And move_cursor adds 1.
154
+ # Let's align on 0-based checking. The event has 1-based x,y.
155
+ term_x = event.x - 1
156
+ term_y = event.y - 1
157
+
158
+ if self.x <= term_x < self.x + self.width and self.y <= term_y < self.y + self.height:
159
+ # Propagate to children first (highest z-index effectively)
160
+ for child in reversed(self.children):
161
+ if child.handle_mouse(event, app):
162
+ return True
163
+
164
+ if event.pressed and self.tag in ('input', 'button', 'checkbox'):
165
+ if app.focused_widget and app.focused_widget != self:
166
+ app.focused_widget.focused = False
167
+ app.focused_widget = self
168
+ self.focused = True
169
+ if self.id:
170
+ app.focused_id = self.id
171
+
172
+ # If kids didn't handle it, try handling it ourselves
173
+ handled = self.on_mouse(event) or (event.pressed and self.tag in ('input', 'button', 'checkbox'))
174
+
175
+ # Special case for scroll wheel in bounds but not handled
176
+ if not handled and event.button in (64, 65):
177
+ return getattr(self, 'handle_wheel', lambda e, a: False)(event, app)
178
+
179
+ return handled
180
+ return False
181
+
182
+ def on_mouse(self, event: MouseEvent) -> bool:
183
+ """Override to implement specific widget mouse logic."""
184
+ return False
File without changes
@@ -0,0 +1,57 @@
1
+ import re
2
+ from typing import Dict, Any
3
+
4
+ def parse_css(css_string: str) -> Dict[str, Dict[str, str]]:
5
+ """
6
+ Extremely lightweight CSS parser.
7
+ Supports basic classes (.cls), IDs (#id), and Element (tag) selectors.
8
+ Returns: { '.btn': {'color': 'red', 'padding': '1'}, ... }
9
+ """
10
+ stylesheet = {}
11
+
12
+ # Remove comments
13
+ css_string = re.sub(r'/\*.*?\*/', '', css_string, flags=re.DOTALL)
14
+
15
+ # Find all blocks: selector { rules }
16
+ blocks = re.findall(r'([^{]+)\{([^}]+)\}', css_string)
17
+
18
+ for selector_group, rules_text in blocks:
19
+ selectors = [s.strip() for s in selector_group.split(',')]
20
+
21
+ rules = {}
22
+ for rule in rules_text.split(';'):
23
+ rule = rule.strip()
24
+ if not rule:
25
+ continue
26
+ if ':' in rule:
27
+ key, value = rule.split(':', 1)
28
+ rules[key.strip()] = value.strip()
29
+
30
+ for selector in selectors:
31
+ if selector not in stylesheet:
32
+ stylesheet[selector] = {}
33
+ stylesheet[selector].update(rules)
34
+
35
+ return stylesheet
36
+
37
+ def get_style_for_node(node_id: str, classes: list[str], tag: str, stylesheet: dict) -> dict:
38
+ """Resolve styles for a given node in order of specificity: tag -> class -> id"""
39
+ style = {}
40
+
41
+ # 1. Tag
42
+ if tag in stylesheet:
43
+ style.update(stylesheet[tag])
44
+
45
+ # 2. Classes
46
+ for cls in classes:
47
+ cls_sel = f".{cls}"
48
+ if cls_sel in stylesheet:
49
+ style.update(stylesheet[cls_sel])
50
+
51
+ # 3. ID
52
+ if node_id:
53
+ id_sel = f"#{node_id}"
54
+ if id_sel in stylesheet:
55
+ style.update(stylesheet[id_sel])
56
+
57
+ return style
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: casca
3
+ Version: 0.1.0
4
+ Summary: Native Python CLI UI library with CSS-like styling
5
+ Author-email: Abdallah <abdalla.zain2004@gmail.com>
6
+ License: MIT
7
+ Keywords: tui,cli,terminal,ui,css,python
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Topic :: Software Development :: User Interfaces
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: chatbot
18
+ Requires-Dist: groq>=0.20.0; extra == "chatbot"
19
+ Provides-Extra: docs
20
+ Requires-Dist: mkdocs>=1.6.0; extra == "docs"
21
+ Requires-Dist: mkdocs-material>=9.5.0; extra == "docs"
22
+ Requires-Dist: pymdown-extensions>=10.8.0; extra == "docs"
23
+ Dynamic: license-file
24
+
25
+ # Casca
26
+
27
+ Casca is a lightweight, native Python TUI library with a DOM-like widget tree and CSS-style theming.
28
+
29
+ ## Features
30
+
31
+ - Zero core dependencies (standard library runtime)
32
+ - Declarative widgets (`Container`, `Label`, `Input`, `Button`, `Card`, `Header`, `ScrollView`)
33
+ - CSS-like style parsing and cascade
34
+ - Keyboard and mouse event handling in raw terminal mode
35
+ - Real examples including login screen, dashboard, and Groq chatbot
36
+
37
+ ## Quick Start
38
+
39
+ ```python
40
+ from casca import Container, Label, run_app
41
+
42
+ CSS = """
43
+ #root {
44
+ padding: 1;
45
+ border: ascii;
46
+ }
47
+ """
48
+
49
+
50
+
51
+ ui_tree = Container(
52
+ Label("Hello from Casca"),
53
+ id="root",
54
+ )
55
+
56
+ if __name__ == "__main__":
57
+ run_app(ui_tree, css=CSS)
58
+ ```
59
+
60
+ ## Run Use Cases
61
+
62
+ ```bash
63
+ python3 -m use_cases.login_screen
64
+ python3 -m use_cases.dashboard
65
+ python3 -m use_cases.chatbot
66
+ ```
67
+
68
+ `use_cases.chatbot` requires `groq` and a `GROQ_API_KEY`.
69
+
70
+
71
+ ## Project Layout
72
+
73
+ - `casca/`: core library
74
+ - `use_cases/`: real apps built with Casca
75
+ - `examples/`: smaller demos
76
+ - `docs/`: MkDocs content
77
+
78
+ ## License
79
+
80
+ MIT
@@ -0,0 +1,24 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ casca/__init__.py
5
+ casca.egg-info/PKG-INFO
6
+ casca.egg-info/SOURCES.txt
7
+ casca.egg-info/dependency_links.txt
8
+ casca.egg-info/requires.txt
9
+ casca.egg-info/top_level.txt
10
+ casca/components/__init__.py
11
+ casca/components/base.py
12
+ casca/components/forms.py
13
+ casca/components/panels.py
14
+ casca/components/scroll.py
15
+ casca/core/__init__.py
16
+ casca/core/ansi.py
17
+ casca/core/app.py
18
+ casca/core/events.py
19
+ casca/core/terminal.py
20
+ casca/dom/__init__.py
21
+ casca/dom/node.py
22
+ casca/style/__init__.py
23
+ casca/style/css.py
24
+ tests/test_imports.py
@@ -0,0 +1,8 @@
1
+
2
+ [chatbot]
3
+ groq>=0.20.0
4
+
5
+ [docs]
6
+ mkdocs>=1.6.0
7
+ mkdocs-material>=9.5.0
8
+ pymdown-extensions>=10.8.0
@@ -0,0 +1 @@
1
+ casca
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "casca"
7
+ version = "0.1.0"
8
+ description = "Native Python CLI UI library with CSS-like styling"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Abdallah", email = "abdalla.zain2004@gmail.com" }
14
+ ]
15
+ keywords = ["tui", "cli", "terminal", "ui", "css", "python"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Environment :: Console",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Topic :: Software Development :: User Interfaces"
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ chatbot = ["groq>=0.20.0"]
27
+ docs = [
28
+ "mkdocs>=1.6.0",
29
+ "mkdocs-material>=9.5.0",
30
+ "pymdown-extensions>=10.8.0"
31
+ ]
32
+
33
+ [tool.setuptools]
34
+ packages = ["casca", "casca.core", "casca.dom", "casca.style", "casca.components"]
casca-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ from casca import App, Container, Label, Button, Input, Checkbox, Card, Header
2
+
3
+
4
+ def test_core_imports_exist():
5
+ assert App is not None
6
+ assert Container is not None
7
+ assert Label is not None
8
+ assert Button is not None
9
+ assert Input is not None
10
+ assert Checkbox is not None
11
+ assert Card is not None
12
+ assert Header is not None