Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,46 @@ jobs:
- uses: actions/checkout@v4
- name: Build (universal)
run: swift build -c release --arch arm64 --arch x86_64
- name: Shape recognizer tests
run: ./scripts/test-shapes.sh

windows:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install mingw-w64
run: sudo apt-get update && sudo apt-get install -y mingw-w64 zip
- name: Build TinyHand.exe and the shape tests
run: make -C windows -j4 dist test
- uses: actions/upload-artifact@v4
with:
name: TinyHand-windows
path: |
windows/build/TinyHand.exe
windows/build/shape_test.exe
windows/dist/*.zip

windows-check:
needs: windows
runs-on: windows-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: TinyHand-windows
path: windows
- name: Shape recognizer tests
run: windows/build/shape_test.exe
- name: Render every shape and pointer
shell: pwsh
run: |
$p = Start-Process windows/build/TinyHand.exe -ArgumentList '--render-test', "$env:RUNNER_TEMP\render.png" -Wait -PassThru
if ($p.ExitCode -ne 0) { Get-Content "$env:LOCALAPPDATA\TinyHand\TinyHand.log"; exit $p.ExitCode }
- name: Click through Settings
shell: pwsh
run: |
$p = Start-Process windows/build/TinyHand.exe -ArgumentList '--settings-ui-test' -Wait -PassThru
Get-Content "$env:LOCALAPPDATA\TinyHand\TinyHand.log" | Select-String 'selftest'
exit $p.ExitCode
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ dist/
*.dmg
*.zip
assets/unused/
windows/build/
6 changes: 5 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ let package = Package(
.package(url: "https://github.com/sparkle-project/Sparkle", exact: "2.10.0"),
],
targets: [
.target(
name: "ShapeKit",
path: "Sources/ShapeKit"
),
.executableTarget(
name: "TinyHand",
dependencies: [.product(name: "Sparkle", package: "Sparkle")],
dependencies: ["ShapeKit", .product(name: "Sparkle", package: "Sparkle")],
path: "Sources/TinyHand",
linkerSettings: [.unsafeFlags(["-Xlinker", "-rpath", "-Xlinker", "@executable_path/../Frameworks"])]
)
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# TinyHand

[![Latest release](https://img.shields.io/github/v/release/nilbuild/tinyhand?display_name=tag&sort=semver&cacheSeconds=3600)](https://github.com/nilbuild/tinyhand/releases/latest)
[![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey)](https://tinyhand.app)
[![Platform: macOS | Windows](https://img.shields.io/badge/platform-macOS%20%7C%20Windows-lightgrey)](https://tinyhand.app)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)

A tiny hand on a stick that points at things on your Mac screen.
Expand All @@ -27,6 +27,8 @@ Open `TinyHand.dmg`, drag TinyHand to Applications and allow **Accessibility** w
| Right-click the menu bar icon | Menu and Settings |
| Click | Tap (nothing underneath gets clicked) |
| Hold and drag | Draw on the screen |
| Draw a rough shape | It snaps into a clean line, arrow, circle, box, triangle, diamond, star or heart (turn it off in Settings, Drawing) |
| Draw a small V at the end of a line | The line becomes an arrow |
| Hold **⌘** while drawing | Add an arrowhead to the end of the line |
| **⌃⌥X** | Clear the drawing |
| **Esc** | Clear everything and turn the pointer off |
Expand Down Expand Up @@ -58,6 +60,12 @@ records the app's `pid`, so a file left behind by a crash can be detected.

If TinyHand isn't running, opening any link launches it and then runs the command.

## Windows

TinyHand also runs on Windows 10 and 11 (x64): the same hands, drawing, shape snapping, tap sounds and
Settings, from the tray instead of the menu bar. Build it with `make -C windows` (mingw-w64) and see
[windows/README.md](windows/README.md) for details.

## License

MIT. See [LICENSE](LICENSE).
93 changes: 93 additions & 0 deletions Sources/ShapeKit/include/shapekit.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#ifndef TINYHAND_SHAPEKIT_H
#define TINYHAND_SHAPEKIT_H

/*
* ShapeKit: turns a rough freehand stroke into a clean shape.
*
* Plain C99 with no dependencies, so the macOS app (through SwiftPM) and the
* Windows port (through the Makefile) share one recognizer and behave the same.
*
* Coordinates are in whatever unit the caller draws in (points on macOS,
* pixels on Windows). Pass y_down = 1 when +y points down the screen (Windows),
* 0 when +y points up (macOS). It only matters for shapes that have an
* "upright" pose: a triangle, pentagon or star pointing up, a heart with its
* point at the bottom.
*/

#ifdef __cplusplus
extern "C" {
#endif

typedef struct {
float x;
float y;
} th_point;

typedef enum {
TH_SHAPE_NONE = 0,
TH_SHAPE_LINE,
TH_SHAPE_ARROW,
TH_SHAPE_CIRCLE,
TH_SHAPE_ELLIPSE,
TH_SHAPE_TRIANGLE,
TH_SHAPE_SQUARE,
TH_SHAPE_RECTANGLE,
TH_SHAPE_DIAMOND,
TH_SHAPE_PENTAGON,
TH_SHAPE_HEXAGON,
TH_SHAPE_STAR,
TH_SHAPE_HEART,
TH_SHAPE_KIND_COUNT
} th_shape_kind;

#define TH_SHAPE_MAX_POINTS 256
#define TH_SHAPE_MAX_PATHS 4

/*
* The clean shape as one or more polylines. Path i uses
* points[path_start[i] .. path_start[i] + path_length[i] - 1] and is closed
* when path_closed[i] is non-zero. Curves (circle, ellipse, heart) come back
* finely sampled, so callers only ever draw straight segments.
*/
typedef struct {
th_shape_kind kind;
int path_count;
int path_start[TH_SHAPE_MAX_PATHS];
int path_length[TH_SHAPE_MAX_PATHS];
int path_closed[TH_SHAPE_MAX_PATHS];
int point_count;
th_point points[TH_SHAPE_MAX_POINTS];
} th_shape;

/*
* Recognise one stroke, points in drawing order. Returns the kind found and
* fills *out, or returns TH_SHAPE_NONE (and sets out->kind to it) when the
* stroke should stay freehand: scribbles, handwriting, tiny marks.
*/
th_shape_kind th_recognize(const th_point *points, int count, int y_down, th_shape *out);

/*
* Two-stroke arrows: call this for a new stroke when the previous stroke was
* just snapped to a line. If the new stroke is a small "V" or "<" drawn at
* either end of that line, *out becomes the finished arrow (pointing at that
* end) and TH_SHAPE_ARROW is returned; the caller replaces the line with it
* and drops the new stroke. Otherwise returns TH_SHAPE_NONE.
*/
th_shape_kind th_recognize_arrowhead(const th_shape *line, const th_point *points, int count,
int y_down, th_shape *out);

/* "Line", "Arrow", "Circle", ... ; "None" for TH_SHAPE_NONE. */
const char *th_shape_name(th_shape_kind kind);

/* Small accessors so Swift does not have to walk C fixed-size arrays. */
int th_shape_path_count(const th_shape *shape);
int th_shape_path_start(const th_shape *shape, int path);
int th_shape_path_length(const th_shape *shape, int path);
int th_shape_path_closed(const th_shape *shape, int path);
th_point th_shape_point(const th_shape *shape, int index);

#ifdef __cplusplus
}
#endif

#endif
Loading