Skip to content

Commit df182d7

Browse files
committed
fix: add textual samples
1 parent 16af2fd commit df182d7

6 files changed

Lines changed: 1165 additions & 0 deletions

File tree

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
# Simple Counter - Textual + Aptabase
2+
3+
A minimal example demonstrating how to integrate [Aptabase](https://aptabase.com/) analytics into a [Textual](https://textual.textualize.io/) TUI application.
4+
5+
Perfect for learning the basics of adding privacy-first analytics to your terminal apps!
6+
7+
## 🎯 What It Does
8+
9+
This is a simple counter app that:
10+
- Increments a counter when you click a button
11+
- Resets the counter to zero
12+
- Tracks all interactions with Aptabase analytics
13+
- Shows notifications for user feedback
14+
15+
## 📸 Features
16+
17+
- **Clean UI**: Centered layout with large counter display
18+
- **Two Buttons**:
19+
- "Click Me!" - Increments the counter
20+
- "Reset" - Resets to zero
21+
- **Analytics Tracking**:
22+
- App start/stop events
23+
- Button clicks with counter values
24+
- Reset actions with previous count
25+
- Session duration
26+
27+
## 🚀 Quick Start
28+
29+
### Installation
30+
31+
#### Using uv (recommended)
32+
33+
```bash
34+
# Install dependencies
35+
uv pip install textual aptabase
36+
37+
# Run the app
38+
uv run main.py
39+
```
40+
41+
#### Using pip
42+
43+
```bash
44+
# Create and activate virtual environment
45+
python -m venv venv
46+
source venv/bin/activate # On Windows: venv\Scripts\activate
47+
48+
# Install dependencies
49+
pip install textual aptabase
50+
51+
# Run the app
52+
python main.py
53+
```
54+
55+
#### Using pyproject.toml
56+
57+
```bash
58+
# If you have the pyproject.toml file
59+
uv sync
60+
uv run counter
61+
```
62+
63+
## ⚙️ Configuration
64+
65+
Before running, you need to set your Aptabase app key:
66+
67+
1. Sign up at [aptabase.com](https://aptabase.com/)
68+
2. Create a new app
69+
3. Copy your app key (format: `A-EU-XXXXXXXXXX` or `A-US-XXXXXXXXXX`)
70+
4. Update the key in `simple_counter.py`:
71+
72+
```python
73+
# Replace with your actual Aptabase app key
74+
app = CounterApp(app_key="A-EU-XXXXXXXXXX")
75+
```
76+
77+
## 📊 Tracked Events
78+
79+
The app automatically tracks:
80+
81+
### 1. **app_started**
82+
Sent when the app launches.
83+
84+
```python
85+
{
86+
"event": "app_started"
87+
}
88+
```
89+
90+
### 2. **button_clicked**
91+
Sent when "Click Me!" is pressed.
92+
93+
```python
94+
{
95+
"event": "button_clicked",
96+
"action": "increment",
97+
"count": 5 # Current counter value
98+
}
99+
```
100+
101+
### 3. **counter_reset**
102+
Sent when "Reset" is pressed.
103+
104+
```python
105+
{
106+
"event": "counter_reset",
107+
"previous_count": 10 # Value before reset
108+
}
109+
```
110+
111+
### 4. **app_closed**
112+
Sent when the app exits.
113+
114+
```python
115+
{
116+
"event": "app_closed",
117+
"final_count": 7 # Final counter value
118+
}
119+
```
120+
121+
## 🎮 Usage
122+
123+
1. **Start the app**: Run `python simple_counter.py`
124+
2. **Click the button**: Press "Click Me!" to increment (or press `Enter` when focused)
125+
3. **Reset**: Click "Reset" to set counter back to zero
126+
4. **Quit**: Press `q` or `Ctrl+C`
127+
128+
## 💻 Code Structure
129+
130+
```python
131+
class CounterApp(App):
132+
def __init__(self, app_key: str):
133+
# Initialize with your Aptabase key
134+
135+
async def on_mount(self):
136+
# Start Aptabase when app starts
137+
138+
async def on_unmount(self):
139+
# Stop Aptabase and send final events
140+
141+
async def on_button_pressed(self, event):
142+
# Handle button clicks and track events
143+
```
144+
145+
### Key Components
146+
147+
**Aptabase Initialization:**
148+
```python
149+
self.aptabase = Aptabase(
150+
app_key=self.app_key,
151+
app_version="1.0.0",
152+
is_debug=True # Shows debug info
153+
)
154+
await self.aptabase.start()
155+
```
156+
157+
**Tracking Events:**
158+
```python
159+
await self.aptabase.track("button_clicked", {
160+
"action": "increment",
161+
"count": self.counter
162+
})
163+
```
164+
165+
**Cleanup:**
166+
```python
167+
await self.aptabase.stop() # Flushes pending events
168+
```
169+
170+
## 🔧 Customization Ideas
171+
172+
Here are some ways to extend this app:
173+
174+
### Add More Buttons
175+
```python
176+
yield Button("Increment by 5", id="btn-plus5")
177+
yield Button("Decrement", id="btn-decrement")
178+
```
179+
180+
### Track Time Between Clicks
181+
```python
182+
import time
183+
184+
self.last_click = time.time()
185+
186+
# In button handler
187+
time_since_last = time.time() - self.last_click
188+
await self.aptabase.track("button_clicked", {
189+
"count": self.counter,
190+
"time_since_last_click": round(time_since_last, 2)
191+
})
192+
```
193+
194+
### Add Keyboard Shortcuts
195+
```python
196+
BINDINGS = [
197+
Binding("space", "increment", "Increment"),
198+
Binding("r", "reset", "Reset"),
199+
]
200+
201+
async def action_increment(self):
202+
self.counter += 1
203+
await self.aptabase.track("keyboard_increment")
204+
```
205+
206+
### Save High Score
207+
```python
208+
self.high_score = 0
209+
210+
if self.counter > self.high_score:
211+
self.high_score = self.counter
212+
await self.aptabase.track("new_high_score", {
213+
"score": self.high_score
214+
})
215+
```
216+
217+
## 🐛 Troubleshooting
218+
219+
### "Analytics unavailable" message
220+
221+
**Cause**: Invalid app key or network issues.
222+
223+
**Solutions**:
224+
1. Check your app key format: Must be `A-EU-*` or `A-US-*`
225+
2. Verify network connectivity
226+
3. Check Aptabase dashboard status
227+
4. Look for error details in the console
228+
229+
### Import errors
230+
231+
```bash
232+
ModuleNotFoundError: No module named 'textual'
233+
```
234+
235+
**Solution**: Install dependencies
236+
```bash
237+
pip install textual aptabase
238+
```
239+
240+
### App won't start
241+
242+
**Check Python version**:
243+
```bash
244+
python --version # Must be 3.11+
245+
```
246+
247+
## 📚 Learn More
248+
249+
### Next Steps
250+
251+
Once you're comfortable with this simple example, check out:
252+
253+
1. **textual_aptabase_demo.py** - Full dashboard with tabs, forms, and real-time stats
254+
2. **advanced_patterns.py** - User identification, error tracking, performance monitoring
255+
256+
### Resources
257+
258+
- **Aptabase**: [https://aptabase.com/docs](https://aptabase.com/docs)
259+
- **Aptabase Python SDK**: [https://github.com/aptabase/aptabase-py](https://github.com/aptabase/aptabase-py)
260+
- **Textual**: [https://textual.textualize.io/](https://textual.textualize.io/)
261+
- **Textual Tutorial**: [https://textual.textualize.io/tutorial/](https://textual.textualize.io/tutorial/)
262+
263+
## 🔐 Privacy
264+
265+
Aptabase is privacy-first analytics:
266+
- ✅ No personal data collected
267+
- ✅ No IP addresses stored
268+
- ✅ No cookies or tracking
269+
- ✅ GDPR compliant
270+
- ✅ Open source
271+
272+
This example only tracks:
273+
- Counter values (anonymous)
274+
- Button click events
275+
- Session duration
276+
- App lifecycle events
277+
278+
## 📄 License
279+
280+
MIT License - feel free to use this as a starting point for your own projects!
281+
282+
## 🤝 Contributing
283+
284+
This is a simple example/demo. Feel free to:
285+
- Fork and modify
286+
- Use in your own projects
287+
- Share improvements
288+
289+
## ❓ Questions?
290+
291+
- **Aptabase Support**: [https://aptabase.com/](https://aptabase.com/)
292+
- **Textual Discord**: [https://discord.gg/Enf6Z3qhVr](https://discord.gg/Enf6Z3qhVr)
293+
294+
---
295+
296+
**Happy coding!** 🚀
297+
298+
Built with ❤️ using [Textual](https://textual.textualize.io/) and [Aptabase](https://aptabase.com/)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""
2+
Minimal Textual + Aptabase Example
3+
4+
A simple counter app that tracks button clicks.
5+
"""
6+
7+
from textual.app import App, ComposeResult
8+
from textual.containers import Center
9+
from textual.widgets import Button, Header, Footer, Static
10+
from aptabase import Aptabase
11+
12+
13+
class CounterApp(App):
14+
"""A simple counter app with analytics"""
15+
16+
CSS = """
17+
Screen {
18+
align: center middle;
19+
}
20+
21+
#counter {
22+
width: 40;
23+
height: 10;
24+
content-align: center middle;
25+
border: solid green;
26+
margin: 1;
27+
}
28+
29+
Button {
30+
margin: 1 2;
31+
}
32+
"""
33+
34+
def __init__(self, app_key: str = "A-EU-0000000000"):
35+
super().__init__()
36+
self.app_key = app_key
37+
self.aptabase: Aptabase | None = None
38+
self.counter = 0
39+
40+
async def on_mount(self) -> None:
41+
"""Initialize Aptabase"""
42+
try:
43+
self.aptabase = Aptabase(
44+
app_key=self.app_key,
45+
app_version="1.0.0",
46+
is_debug=True
47+
)
48+
await self.aptabase.start()
49+
await self.aptabase.track("app_started")
50+
self.notify("Analytics connected! ✅")
51+
except Exception as e:
52+
self.notify(f"Analytics unavailable: {e}", severity="warning")
53+
54+
async def on_unmount(self) -> None:
55+
"""Cleanup Aptabase"""
56+
if self.aptabase:
57+
await self.aptabase.track("app_closed", {"final_count": self.counter})
58+
await self.aptabase.stop()
59+
60+
def compose(self) -> ComposeResult:
61+
"""Create the UI"""
62+
yield Header()
63+
with Center():
64+
yield Static(f"[bold cyan]Count: {self.counter}[/bold cyan]", id="counter")
65+
yield Button("Click Me!", id="btn-increment", variant="primary")
66+
yield Button("Reset", id="btn-reset", variant="warning")
67+
yield Footer()
68+
69+
async def on_button_pressed(self, event: Button.Pressed) -> None:
70+
"""Handle button clicks"""
71+
if event.button.id == "btn-increment":
72+
self.counter += 1
73+
if self.aptabase:
74+
await self.aptabase.track("button_clicked", {
75+
"action": "increment",
76+
"count": self.counter
77+
})
78+
elif event.button.id == "btn-reset":
79+
old_count = self.counter
80+
self.counter = 0
81+
if self.aptabase:
82+
await self.aptabase.track("counter_reset", {
83+
"previous_count": old_count
84+
})
85+
86+
# Update the counter display
87+
counter_widget = self.query_one("#counter", Static)
88+
counter_widget.update(f"[bold cyan]Count: {self.counter}[/bold cyan]")
89+
90+
91+
if __name__ == "__main__":
92+
# Replace with your Aptabase app key
93+
app = CounterApp(app_key="A-EU-0000000000")
94+
app.run()

0 commit comments

Comments
 (0)