hair-trigger 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hair_trigger-0.2.1/LICENSE +21 -0
- hair_trigger-0.2.1/PKG-INFO +459 -0
- hair_trigger-0.2.1/README.md +442 -0
- hair_trigger-0.2.1/pyproject.toml +25 -0
- hair_trigger-0.2.1/setup.cfg +4 -0
- hair_trigger-0.2.1/src/hair_trigger/__init__.py +3 -0
- hair_trigger-0.2.1/src/hair_trigger/configure.py +31 -0
- hair_trigger-0.2.1/src/hair_trigger/event.py +89 -0
- hair_trigger-0.2.1/src/hair_trigger/py.typed +0 -0
- hair_trigger-0.2.1/src/hair_trigger/runner.py +47 -0
- hair_trigger-0.2.1/src/hair_trigger/scheduler.py +81 -0
- hair_trigger-0.2.1/src/hair_trigger/typing/__init__.py +2 -0
- hair_trigger-0.2.1/src/hair_trigger/typing/runner.py +24 -0
- hair_trigger-0.2.1/src/hair_trigger/typing/scheduler.py +29 -0
- hair_trigger-0.2.1/src/hair_trigger.egg-info/PKG-INFO +459 -0
- hair_trigger-0.2.1/src/hair_trigger.egg-info/SOURCES.txt +19 -0
- hair_trigger-0.2.1/src/hair_trigger.egg-info/dependency_links.txt +1 -0
- hair_trigger-0.2.1/src/hair_trigger.egg-info/top_level.txt +1 -0
- hair_trigger-0.2.1/tests/test_config.py +43 -0
- hair_trigger-0.2.1/tests/test_event.py +212 -0
- hair_trigger-0.2.1/tests/test_scheduler.py +151 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BetterBuiltFool
|
|
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,459 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hair_trigger
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Simple, Subscribable, Custom Events.
|
|
5
|
+
Author-email: BetterBuiltFool <betterbuiltfool@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/BetterBuiltFool/hair_trigger
|
|
8
|
+
Project-URL: Issues, https://github.com/BetterBuiltFool/hair_trigger/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Utilities
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.12.4
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
<!-- Improved compatibility of back to top link: See: https://github.com/othneildrew/Best-README-Template/pull/73 -->
|
|
19
|
+
<a id="readme-top"></a>
|
|
20
|
+
<!--
|
|
21
|
+
*** Thanks for checking out the Best-README-Template. If you have a suggestion
|
|
22
|
+
*** that would make this better, please fork the repo and create a pull request
|
|
23
|
+
*** or simply open an issue with the tag "enhancement".
|
|
24
|
+
*** Don't forget to give the project a star!
|
|
25
|
+
*** Thanks again! Now go create something AMAZING! :D
|
|
26
|
+
-->
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
<!-- PROJECT SHIELDS -->
|
|
31
|
+
<!--
|
|
32
|
+
*** I'm using markdown "reference style" links for readability.
|
|
33
|
+
*** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
|
|
34
|
+
*** See the bottom of this document for the declaration of the reference variables
|
|
35
|
+
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
|
|
36
|
+
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
|
|
37
|
+
-->
|
|
38
|
+
[![Contributors][contributors-shield]][contributors-url]
|
|
39
|
+
[![Forks][forks-shield]][forks-url]
|
|
40
|
+
[![Stargazers][stars-shield]][stars-url]
|
|
41
|
+
[![Issues][issues-shield]][issues-url]
|
|
42
|
+
[![MIT License][license-shield]][license-url]
|
|
43
|
+
<!--
|
|
44
|
+
[![LinkedIn][linkedin-shield]][linkedin-url]
|
|
45
|
+
-->
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
<!-- PROJECT LOGO -->
|
|
50
|
+
<br />
|
|
51
|
+
<!--
|
|
52
|
+
<div align="center">
|
|
53
|
+
<a href="https://github.com/BetterBuiltFool/hair_trigger">
|
|
54
|
+
<img src="images/logo.png" alt="Logo" width="80" height="80">
|
|
55
|
+
</a>
|
|
56
|
+
-->
|
|
57
|
+
|
|
58
|
+
<h3 align="center">Hair Trigger</h3>
|
|
59
|
+
|
|
60
|
+
<p align="center">
|
|
61
|
+
Simple, Subscribable, Custom Events
|
|
62
|
+
<br />
|
|
63
|
+
<a href="https://github.com/BetterBuiltFool/hair_trigger"><strong>Explore the docs »</strong></a>
|
|
64
|
+
<br />
|
|
65
|
+
<br />
|
|
66
|
+
<!--
|
|
67
|
+
<a href="https://github.com/BetterBuiltFool/hair_trigger">View Demo</a>
|
|
68
|
+
·
|
|
69
|
+
-->
|
|
70
|
+
<a href="https://github.com/BetterBuiltFool/hair_trigger/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
|
|
71
|
+
·
|
|
72
|
+
<a href="https://github.com/BetterBuiltFool/hair_trigger/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
<!-- TABLE OF CONTENTS -->
|
|
79
|
+
<details>
|
|
80
|
+
<summary>Table of Contents</summary>
|
|
81
|
+
<ol>
|
|
82
|
+
<li>
|
|
83
|
+
<a href="#about-the-project">About The Project</a>
|
|
84
|
+
</li>
|
|
85
|
+
<li>
|
|
86
|
+
<a href="#getting-started">Getting Started</a>
|
|
87
|
+
<ul>
|
|
88
|
+
<li><a href="#installation">Installation</a></li>
|
|
89
|
+
</ul>
|
|
90
|
+
</li>
|
|
91
|
+
<li><a href="#usage">Usage</a></li>
|
|
92
|
+
<ul>
|
|
93
|
+
<li><a href="#defining-events">Defining Events</a></li>
|
|
94
|
+
<li><a href="#assigning-instances">Assigning Instances</a></li>
|
|
95
|
+
<li><a href="#subscribing-to-events">Subscribing to Events</a></li>
|
|
96
|
+
<li><a href="#triggering-events">Triggering Events</a></li>
|
|
97
|
+
<li><a href="#configuration">Configuration</a></li>
|
|
98
|
+
</ul>
|
|
99
|
+
<!-- <li><a href="#roadmap">Roadmap</a></li> -->
|
|
100
|
+
<!--<li><a href="#contributing">Contributing</a></li>-->
|
|
101
|
+
<li><a href="#license">License</a></li>
|
|
102
|
+
<li><a href="#contact">Contact</a></li>
|
|
103
|
+
<li><a href="#acknowledgments">Acknowledgments</a></li>
|
|
104
|
+
</ol>
|
|
105
|
+
</details>
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
<!-- ABOUT THE PROJECT -->
|
|
110
|
+
## About The Project
|
|
111
|
+
|
|
112
|
+
<!--
|
|
113
|
+
[![Product Name Screen Shot][product-screenshot]](https://example.com)
|
|
114
|
+
-->
|
|
115
|
+
|
|
116
|
+
Hair Trigger offers custom, subscribable events in the style of Luau events that allow for decoupled access between objects.
|
|
117
|
+
|
|
118
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
<!-- GETTING STARTED -->
|
|
122
|
+
## Getting Started
|
|
123
|
+
|
|
124
|
+
Hair Trigger is written in pure python, with no system dependencies, and should be OS-agnostic.
|
|
125
|
+
|
|
126
|
+
### Installation
|
|
127
|
+
|
|
128
|
+
Hair Trigger can be installed from the [PyPI][pypi-url] using [pip][pip-url]:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
pip install hair_trigger
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
and can be imported for use with:
|
|
135
|
+
```python
|
|
136
|
+
import hair_trigger
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Hair Trigger has no dependencies beyond python itself.
|
|
140
|
+
|
|
141
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
<!-- USAGE EXAMPLES -->
|
|
146
|
+
## Usage
|
|
147
|
+
|
|
148
|
+
Hair Trigger supplies no events by default, they must be custom made.
|
|
149
|
+
|
|
150
|
+
As an example, we'll make a simple event for detecting when an object is enabled.
|
|
151
|
+
|
|
152
|
+
### Defining Events
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from typing import Any
|
|
157
|
+
import hair_trigger
|
|
158
|
+
|
|
159
|
+
class OnEnable(hair_trigger.Event):
|
|
160
|
+
"""
|
|
161
|
+
Called whenever the owner becomes enabled.
|
|
162
|
+
|
|
163
|
+
:param this: The object being enabled.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def trigger(self, this: Any) -> None:
|
|
167
|
+
return super().trigger(this)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Naming convention is suggested as `On[Event name]`. The `trigger` method must be defined, and must, at a minimum, call the super method. `trigger`'s signature will also define the required signature of subscribing callbacks.
|
|
171
|
+
It is recommended to put the docstring describing `trigger`'s parameters in the class docstring, so that it is visible to users.
|
|
172
|
+
|
|
173
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
174
|
+
|
|
175
|
+
### Assigning Instances
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
Now we'll need an object to have the event.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
|
|
182
|
+
class Foo:
|
|
183
|
+
def __init__(self, enabled: bool = False) -> None:
|
|
184
|
+
self.OnEnable = OnEnable()
|
|
185
|
+
self._enabled = enabled
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
When a `Foo` is created, a new instance of `OnEnable` is created for it, as well. It is recommended that the event attribute breaks normal snake_case style and uses PascalCase to make it clear that this is an event object rather than a method or a typical attribute.
|
|
190
|
+
|
|
191
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
192
|
+
|
|
193
|
+
### Subscribing to Events
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
Let's say we want to print something when a `Foo` is enabled. Subscribing is done primarily by using the event instance as a decorator.
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
foo = Foo()
|
|
201
|
+
|
|
202
|
+
@foo.OnEnable
|
|
203
|
+
def do_the_thing(this: Foo) -> None:
|
|
204
|
+
print(f"{this} has been enabled")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# For simple expressions, a lambda is also okay
|
|
208
|
+
# For this though, we do not use it as a decorator.
|
|
209
|
+
|
|
210
|
+
foo.OnEnable(lambda this: print(f"{this} has been enabled"))
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Additionally, objects can subscribe to an event as well. It uses the event as a decorator, too, but requires an additional parameter, the subscribing object. Subscribers need an owner so they don't tie up garbage collection.
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
|
|
218
|
+
class FooListener:
|
|
219
|
+
|
|
220
|
+
def __init__(self, foo: Foo) -> None:
|
|
221
|
+
|
|
222
|
+
@foo.OnEnable(self)
|
|
223
|
+
def _(self, this: Foo) -> None:
|
|
224
|
+
# Note: `self` here will shadow the `self` of init. This is important!
|
|
225
|
+
print(f"{self} noticed {this} is now enabled")
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Alternatively, we can subscribe to a bound method directly, by using the event as a regular function. This doesn't require the subscribing object to be passed, it is extracted from the bound method.
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
|
|
232
|
+
class FooListener:
|
|
233
|
+
|
|
234
|
+
def __init__(self, foo:Foo) -> None:
|
|
235
|
+
|
|
236
|
+
foo.OnEnable(self.listen_in)
|
|
237
|
+
|
|
238
|
+
def listen_in(self, this: Foo) -> None:
|
|
239
|
+
print(f"{self} noticed {this} is now enabled")
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Both versions have the same behavior, and if we have multiple of `FooListener` with the same `Foo`, the message will be printed once each.
|
|
243
|
+
|
|
244
|
+
Important notes:
|
|
245
|
+
- The callback must be subscribed in a method, not the class definition.
|
|
246
|
+
- The init method is a great candidate for callback subscription, but it can be done elsewhere if needed. Get creative!
|
|
247
|
+
- For new callbacks created inside the init/equivalent:
|
|
248
|
+
- The callback does not need a name, "_" is fine.
|
|
249
|
+
- The `self` inside the callback must shadow the `self` of the init. This allows the callback to use the object, but won't prevent garbage collection due to a closure.
|
|
250
|
+
- Other than the `self`, the signature take all parameters as the trigger method of the event. Unused parameters can be caught with *args.
|
|
251
|
+
|
|
252
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
253
|
+
|
|
254
|
+
### Triggering Events
|
|
255
|
+
|
|
256
|
+
Now that we have a listener, we'll need to actually to something to trigger the event. To do this, we'll simply need to call the `trigger` method of the event.
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
|
|
260
|
+
class Foo:
|
|
261
|
+
# init definition as above
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def enabled(self) -> bool:
|
|
265
|
+
return self._enabled
|
|
266
|
+
|
|
267
|
+
@enabled.setter
|
|
268
|
+
def enabled(self, enabled: bool) ->:
|
|
269
|
+
self._enabled = enabled
|
|
270
|
+
if enabled:
|
|
271
|
+
self.OnEnable.trigger(self)
|
|
272
|
+
```
|
|
273
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
### Configuration
|
|
277
|
+
|
|
278
|
+
By default, Hair Trigger will attempt to run callbacks immediately, in syncronous mode. If asynchronous behavior is needed, or events need to be run manually or in a particular order, this can be changed using the `config` function.
|
|
279
|
+
|
|
280
|
+
#### Synchronous vs Asynchronous
|
|
281
|
+
|
|
282
|
+
The default system will run callback synchronously, so any blocking that occurs will block the entire thread. If that's undesireable, you can also use:
|
|
283
|
+
|
|
284
|
+
- ThreadRunner: Uses the Python threading module to run callbacks in new threads, good for general purpose multithreading.
|
|
285
|
+
- AsyncioRunner: Uses Python's asyncio module, useful for when threading must be async-aware, such as in WASM deployments.
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
import hair_trigger
|
|
289
|
+
from hair_trigger.runner import AsyncioRunner, ThreadRunner
|
|
290
|
+
|
|
291
|
+
# Run standard threads
|
|
292
|
+
hair_trigger.config(runner=ThreadRunner())
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# Run async-aware
|
|
296
|
+
hair_trigger.config(runner=AsyncioRunner())
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
#### Scheduling Modes
|
|
300
|
+
|
|
301
|
+
Without config, triggering an event instantly begins notifying the event's subscribers, and if those trigger additional events, they'll take over mid-call. Instead, you can use a deferred scheduler.
|
|
302
|
+
|
|
303
|
+
Included are:
|
|
304
|
+
|
|
305
|
+
- StackScheduler: New events are put onto a stack, so that the newest event resolve before olderone resolve.
|
|
306
|
+
- QueueScheduler: New events are put into a queue, so events resolve in the order they are triggered.
|
|
307
|
+
|
|
308
|
+
The deferred schedulers must be triggered manually, using `hair_trigger.scheduler.pump_events()`.
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
import hair_trigger
|
|
312
|
+
import hair_trigger.scheduler
|
|
313
|
+
from hair_trigger.scheduler import QueueScheduler
|
|
314
|
+
|
|
315
|
+
hair_trigger.config(scheduler=QueueScheduler())
|
|
316
|
+
|
|
317
|
+
# Do things to trigger events
|
|
318
|
+
|
|
319
|
+
hair_trigger.scheduler.pump_events()
|
|
320
|
+
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
#### Custom Runners and Schedulers
|
|
324
|
+
|
|
325
|
+
Runners and schedulers are protocols, so custom one can be created to get specific behaviors.
|
|
326
|
+
|
|
327
|
+
For example:
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
|
|
331
|
+
class LoggingThreadRunner:
|
|
332
|
+
|
|
333
|
+
def schedule(self, func: Callable[..., Any], *args, **kwds) -> None:
|
|
334
|
+
print(f"Calling function {func}")
|
|
335
|
+
threading.Thread(target=func, args=args, kwargs=kwds).start()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
hair_trigger.config(LoggingThreadRunner())
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
This will log the function before starting the thread.
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
345
|
+
|
|
346
|
+
<!-- ROADMAP -->
|
|
347
|
+
<!-- ## Roadmap
|
|
348
|
+
|
|
349
|
+
- [ ] (Eternal) Improve the renderer. Faster rendering means more renderables! -->
|
|
350
|
+
|
|
351
|
+
<!--
|
|
352
|
+
- [ ] Feature 2
|
|
353
|
+
- [ ] Feature 3
|
|
354
|
+
- [ ] Nested Feature
|
|
355
|
+
-->
|
|
356
|
+
|
|
357
|
+
<!-- See the [open issues](https://github.com/BetterBuiltFool/hair_trigger/issues) for a full list of proposed features (and known issues). -->
|
|
358
|
+
|
|
359
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
<!-- CONTRIBUTING -->
|
|
364
|
+
<!--
|
|
365
|
+
## Contributing
|
|
366
|
+
|
|
367
|
+
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
|
368
|
+
|
|
369
|
+
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
|
|
370
|
+
Don't forget to give the project a star! Thanks again!
|
|
371
|
+
|
|
372
|
+
1. Fork the Project
|
|
373
|
+
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
|
|
374
|
+
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
|
|
375
|
+
4. Push to the Branch (`git push origin feature/AmazingFeature`)
|
|
376
|
+
5. Open a Pull Request
|
|
377
|
+
|
|
378
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
379
|
+
|
|
380
|
+
### Top contributors:
|
|
381
|
+
|
|
382
|
+
<a href="https://github.com/BetterBuiltFool/hair_trigger/graphs/contributors">
|
|
383
|
+
<img src="https://contrib.rocks/image?repo=BetterBuiltFool/hair_trigger" alt="contrib.rocks image" />
|
|
384
|
+
</a>
|
|
385
|
+
-->
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
<!-- LICENSE -->
|
|
390
|
+
## License
|
|
391
|
+
|
|
392
|
+
Distributed under the MIT License. See `LICENSE.txt` for more information.
|
|
393
|
+
|
|
394
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
<!-- CONTACT -->
|
|
399
|
+
## Contact
|
|
400
|
+
|
|
401
|
+
Better Built Fool - betterbuiltfool@gmail.com
|
|
402
|
+
|
|
403
|
+
Bluesky - [@betterbuiltfool.bsky.social](https://bsky.app/profile/betterbuiltfool.bsky.social)
|
|
404
|
+
<!--
|
|
405
|
+
- [@twitter_handle](https://twitter.com/twitter_handle)
|
|
406
|
+
-->
|
|
407
|
+
|
|
408
|
+
Project Link: [https://github.com/BetterBuiltFool/hair_trigger](https://github.com/BetterBuiltFool/hair_trigger)
|
|
409
|
+
|
|
410
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
<!-- ACKNOWLEDGMENTS -->
|
|
415
|
+
<!--## Acknowledgments
|
|
416
|
+
|
|
417
|
+
* []()
|
|
418
|
+
* []()
|
|
419
|
+
* []()
|
|
420
|
+
|
|
421
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
422
|
+
-->
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
<!-- MARKDOWN LINKS & IMAGES -->
|
|
426
|
+
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
|
427
|
+
[contributors-shield]: https://img.shields.io/github/contributors/BetterBuiltFool/hair_trigger.svg?style=for-the-badge
|
|
428
|
+
[contributors-url]: https://github.com/BetterBuiltFool/hair_trigger/graphs/contributors
|
|
429
|
+
[forks-shield]: https://img.shields.io/github/forks/BetterBuiltFool/hair_trigger.svg?style=for-the-badge
|
|
430
|
+
[forks-url]: https://github.com/BetterBuiltFool/hair_trigger/network/members
|
|
431
|
+
[stars-shield]: https://img.shields.io/github/stars/BetterBuiltFool/hair_trigger.svg?style=for-the-badge
|
|
432
|
+
[stars-url]: https://github.com/BetterBuiltFool/hair_trigger/stargazers
|
|
433
|
+
[issues-shield]: https://img.shields.io/github/issues/BetterBuiltFool/hair_trigger.svg?style=for-the-badge
|
|
434
|
+
[issues-url]: https://github.com/BetterBuiltFool/hair_trigger/issues
|
|
435
|
+
[license-shield]: https://img.shields.io/github/license/BetterBuiltFool/hair_trigger.svg?style=for-the-badge
|
|
436
|
+
[license-url]: https://github.com/BetterBuiltFool/hair_trigger/blob/main/LICENSE
|
|
437
|
+
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
|
|
438
|
+
[linkedin-url]: https://linkedin.com/in/linkedin_username
|
|
439
|
+
[product-screenshot]: images/screenshot.png
|
|
440
|
+
[Next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
|
|
441
|
+
[Next-url]: https://nextjs.org/
|
|
442
|
+
[python.org]: https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54
|
|
443
|
+
[python-url]: https://www.python.org/
|
|
444
|
+
[React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB
|
|
445
|
+
[React-url]: https://reactjs.org/
|
|
446
|
+
[Vue.js]: https://img.shields.io/badge/Vue.js-35495E?style=for-the-badge&logo=vuedotjs&logoColor=4FC08D
|
|
447
|
+
[Vue-url]: https://vuejs.org/
|
|
448
|
+
[Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white
|
|
449
|
+
[Angular-url]: https://angular.io/
|
|
450
|
+
[Svelte.dev]: https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte&logoColor=FF3E00
|
|
451
|
+
[Svelte-url]: https://svelte.dev/
|
|
452
|
+
[Laravel.com]: https://img.shields.io/badge/Laravel-FF2D20?style=for-the-badge&logo=laravel&logoColor=white
|
|
453
|
+
[Laravel-url]: https://laravel.com
|
|
454
|
+
[Bootstrap.com]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white
|
|
455
|
+
[Bootstrap-url]: https://getbootstrap.com
|
|
456
|
+
[JQuery.com]: https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery&logoColor=white
|
|
457
|
+
[JQuery-url]: https://jquery.com
|
|
458
|
+
[pypi-url]: https://pypi.org/project/hair_trigger
|
|
459
|
+
[pip-url]: https://pip.pypa.io/en/stable/
|