Skip to content
Draft
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
61 changes: 60 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,58 @@ Frame-precise timing tool for speedrunning and competitive gaming.

[![Video Tutorial](http://img.youtube.com/vi/hF1zg31TAxk/0.jpg)](http://www.youtube.com/watch?v=hF1zg31TAxk)

## Installation

### macOS (Intel & Apple Silicon)

1. **Build the application:**
```bash
./build.sh gradle
```

2. **Create macOS app bundle:**
```bash
./create-app.sh
```

3. **Install to Applications folder:**
```bash
cp -r FlowTimer.app /Applications/
```

4. **Grant accessibility permissions:**
- Open System Settings → Privacy & Security → Accessibility
- Click the "+" button and add FlowTimer
- This is required for global keyboard shortcuts

5. **Launch from Spotlight:**
- Press Cmd+Space and search "FlowTimer"

### Validation

Run the validation script to ensure the app bundle is correctly configured:
```bash
./test-app-bundle.sh
```

## Build

Requires Java 17+.

### Using Gradle
```bash
./build.sh gradle
# or
gradle clean build
java -XstartOnFirstThread -jar build/libs/FlowTimer.jar
java -jar build/libs/FlowTimer.jar
```

### Using Maven
```bash
./build.sh maven
# or
mvn clean package
java -jar target/FlowTimer.jar
```

## Changes in 1.8.1
Expand All @@ -21,6 +66,8 @@ java -XstartOnFirstThread -jar build/libs/FlowTimer.jar
- Target time calculation for Variable Offset timer shows when specified frame occurs
- Native ARM64 support for Apple Silicon
- Updated dependencies: LWJGL 3.3.3, JNativeHook 2.2.2
- Improved macOS app bundle with robust launcher script
- Fixed Apple Silicon compatibility issues

## Usage

Expand All @@ -31,6 +78,18 @@ Three timer modes:

Global key hooks require accessibility permissions on macOS.

## Troubleshooting

### App won't start from Applications folder
- Check the debug log: `tail -f /tmp/flowtimer_debug.log`
- Ensure Java 17+ is installed: `java -version`
- Verify accessibility permissions are granted

### Architecture issues on Apple Silicon
- The app includes native ARM64 libraries for both JNativeHook and LWJGL
- Launcher automatically detects architecture and applies optimizations
- No need for Rosetta 2 compatibility mode

## License

Original FlowTimer project, modernized for current hardware.
2 changes: 1 addition & 1 deletion build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then

# Run with appropriate JVM arguments for macOS
if [[ "$OSTYPE" == "darwin"* ]]; then
java -XstartOnFirstThread -jar "$JAR_FILE"
java -Djava.awt.headless=false -jar "$JAR_FILE"
else
java -jar "$JAR_FILE"
fi
Expand Down
110 changes: 79 additions & 31 deletions create-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ cat > "${APP_BUNDLE}/Contents/Info.plist" << EOF
<false/>
<key>NSHighResolutionCapable</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>10.15</string>
<key>NSAppleEventsUsageDescription</key>
<string>FlowTimer requires access to system events for global keyboard shortcuts.</string>
<key>NSMicrophoneUsageDescription</key>
<string>FlowTimer may use the microphone for audio feedback features.</string>
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
<key>NSRequiresAquaSystemAppearance</key>
<false/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
</dict>
</plist>
EOF
Expand All @@ -55,22 +67,28 @@ cp build/libs/FlowTimer.jar "${APP_BUNDLE}/Contents/Resources/"
cp -r res/ "${APP_BUNDLE}/Contents/Resources/"
cp -r lib/ "${APP_BUNDLE}/Contents/Resources/"

# Copy the ARM64 JNativeHook library (LWJGL will use its own built-in ARM64 libraries)
cp build/libs/libJNativeHook.arm64.dylib "${APP_BUNDLE}/Contents/Resources/"

# Copy the icon
cp res/image/icon.png "${APP_BUNDLE}/Contents/Resources/icon.icns"
# Copy the icon if it exists, otherwise use a default
if [ -f "res/image/icon.png" ]; then
cp res/image/icon.png "${APP_BUNDLE}/Contents/Resources/icon.icns"
else
echo "Warning: Icon file not found, app will use default icon"
fi

# Create launcher script
cat > "${APP_BUNDLE}/Contents/MacOS/FlowTimer" << 'EOF'
#!/bin/bash

# Debug log file
# FlowTimer macOS App Bundle Launcher
# Supports both Intel and Apple Silicon Macs

# Debug log file for troubleshooting
LOG_FILE="/tmp/flowtimer_debug.log"
exec > >(tee -a "$LOG_FILE") 2>&1

echo "$(date): FlowTimer launcher started"
echo "Arguments: $@"
echo "Architecture: $(uname -m)"
echo "macOS Version: $(sw_vers -productVersion)"

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
echo "Launcher directory: $DIR"
Expand All @@ -80,50 +98,80 @@ echo "Resources directory: $RESOURCES_DIR"

if [ ! -d "$RESOURCES_DIR" ]; then
echo "ERROR: Resources directory not found at $RESOURCES_DIR"
osascript -e 'display dialog "FlowTimer resources not found!" buttons {"OK"} default button "OK"'
osascript -e 'display alert "FlowTimer Error" message "Application resources not found. Please reinstall FlowTimer." as critical'
exit 1
fi

# Find Java - try common locations
# Find Java - try multiple locations and validate version
JAVA_CMD=""
if command -v java &> /dev/null; then
JAVA_CMD="java"
echo "Found Java via command: $(which java)"
elif [ -f "/usr/bin/java" ]; then
JAVA_CMD="/usr/bin/java"
echo "Found Java at: /usr/bin/java"
elif [ -f "/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java" ]; then
JAVA_CMD="/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"
echo "Found Java at plugin location"
else
echo "ERROR: Java not found"
osascript -e 'display dialog "Java 17+ is required but not found. Please install Java." buttons {"OK"} default button "OK"'
MIN_JAVA_VERSION=17

find_java() {
local java_paths=(
"$(command -v java 2>/dev/null)"
"/usr/bin/java"
"/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"
"/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java"
"$(/usr/libexec/java_home -v $MIN_JAVA_VERSION 2>/dev/null)/bin/java"
)

for java_path in "${java_paths[@]}"; do
if [ -n "$java_path" ] && [ -x "$java_path" ]; then
# Check Java version
local version_output=$("$java_path" -version 2>&1 | head -n 1)
local version_num=$("$java_path" -version 2>&1 | awk -F '"' '/version/ {print $2}' | cut -d'.' -f1)

if [ -n "$version_num" ] && [ "$version_num" -ge "$MIN_JAVA_VERSION" ]; then
echo "Found compatible Java at: $java_path"
echo "Java version: $version_output"
JAVA_CMD="$java_path"
return 0
else
echo "Java at $java_path is too old (version $version_num, need $MIN_JAVA_VERSION+)"
fi
fi
done
return 1
}

if ! find_java; then
echo "ERROR: Java $MIN_JAVA_VERSION+ not found"
osascript -e 'display alert "Java Required" message "FlowTimer requires Java 17 or later. Please install Java and try again.\n\nRecommended: Download from https://adoptium.net/" as critical buttons {"OK"}'
exit 1
fi

# Check Java version
JAVA_VERSION=$("$JAVA_CMD" -version 2>&1 | head -n 1)
echo "Java version: $JAVA_VERSION"

# Change to the Resources directory where all the FlowTimer files are located
cd "$RESOURCES_DIR"
echo "Changed to directory: $(pwd)"

# Check if JAR file exists
if [ ! -f "FlowTimer.jar" ]; then
echo "ERROR: FlowTimer.jar not found in $(pwd)"
osascript -e 'display dialog "FlowTimer.jar not found!" buttons {"OK"} default button "OK"'
osascript -e 'display alert "FlowTimer Error" message "FlowTimer.jar not found. Please reinstall FlowTimer." as critical'
exit 1
fi

echo "Starting FlowTimer..."
# Set the native library path for JNativeHook ARM64 library
# LWJGL will use its own built-in ARM64 libraries automatically
NATIVE_LIB_PATH="$(pwd)"
echo "Native library path: $NATIVE_LIB_PATH"
echo "Working directory: $(pwd)"

# macOS-specific JVM arguments
JVM_ARGS=(
"-Djava.awt.headless=false"
"-Dapple.laf.useScreenMenuBar=true"
"-Dcom.apple.mrj.application.apple.menu.about.name=FlowTimer"
"-Dapple.awt.application.name=FlowTimer"
)

# Apple Silicon specific optimizations
if [[ "$(uname -m)" == "arm64" ]]; then
echo "Detected Apple Silicon, using ARM64 optimizations"
JVM_ARGS+=("-XX:+UnlockExperimentalVMOptions" "-XX:+UseZGC")
fi

echo "JVM arguments: ${JVM_ARGS[*]}"

# Run FlowTimer with proper working directory and native library path
exec "$JAVA_CMD" -Djava.awt.headless=false -Djava.library.path="$NATIVE_LIB_PATH" -jar FlowTimer.jar "$@"
# Run FlowTimer
exec "$JAVA_CMD" "${JVM_ARGS[@]}" -jar FlowTimer.jar "$@"
EOF

# Make launcher executable
Expand Down
110 changes: 110 additions & 0 deletions test-app-bundle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/bin/bash

# Test script for FlowTimer app bundle validation
# This script validates the app bundle structure and basic functionality

set -e

APP_BUNDLE="FlowTimer.app"
SUCCESS_COLOR='\033[0;32m'
ERROR_COLOR='\033[0;31m'
INFO_COLOR='\033[0;34m'
NC='\033[0m' # No Color

echo -e "${INFO_COLOR}FlowTimer App Bundle Validation${NC}"
echo "=================================="

# Check if app bundle exists
if [ ! -d "$APP_BUNDLE" ]; then
echo -e "${ERROR_COLOR}ERROR: $APP_BUNDLE not found${NC}"
echo "Please run ./create-app.sh first"
exit 1
fi

echo -e "${SUCCESS_COLOR}✓ App bundle exists${NC}"

# Check bundle structure
required_paths=(
"$APP_BUNDLE/Contents"
"$APP_BUNDLE/Contents/Info.plist"
"$APP_BUNDLE/Contents/MacOS"
"$APP_BUNDLE/Contents/MacOS/FlowTimer"
"$APP_BUNDLE/Contents/Resources"
"$APP_BUNDLE/Contents/Resources/FlowTimer.jar"
)

for path in "${required_paths[@]}"; do
if [ -e "$path" ]; then
echo -e "${SUCCESS_COLOR}✓ $path exists${NC}"
else
echo -e "${ERROR_COLOR}✗ $path missing${NC}"
exit 1
fi
done

# Check launcher script is executable
if [ -x "$APP_BUNDLE/Contents/MacOS/FlowTimer" ]; then
echo -e "${SUCCESS_COLOR}✓ Launcher script is executable${NC}"
else
echo -e "${ERROR_COLOR}✗ Launcher script is not executable${NC}"
exit 1
fi

# Validate Info.plist structure (basic XML check)
if grep -q "<?xml version" "$APP_BUNDLE/Contents/Info.plist" && grep -q "</plist>" "$APP_BUNDLE/Contents/Info.plist"; then
echo -e "${SUCCESS_COLOR}✓ Info.plist has valid XML structure${NC}"
else
echo -e "${ERROR_COLOR}✗ Info.plist has invalid XML structure${NC}"
exit 1
fi

# Check bundle identifier
if grep -q "com.github.stringflow.flowtimer" "$APP_BUNDLE/Contents/Info.plist"; then
echo -e "${SUCCESS_COLOR}✓ Bundle identifier is correct${NC}"
else
echo -e "${ERROR_COLOR}✗ Bundle identifier is incorrect or missing${NC}"
exit 1
fi

# Check JAR file integrity
if unzip -t "$APP_BUNDLE/Contents/Resources/FlowTimer.jar" &>/dev/null; then
echo -e "${SUCCESS_COLOR}✓ JAR file is valid${NC}"
else
echo -e "${ERROR_COLOR}✗ JAR file is corrupted${NC}"
exit 1
fi

# Check for required native libraries in JAR
required_natives=(
"com/github/kwhat/jnativehook/lib/darwin/arm64/libJNativeHook.dylib"
"com/github/kwhat/jnativehook/lib/darwin/x86_64/libJNativeHook.dylib"
)

for native in "${required_natives[@]}"; do
if unzip -l "$APP_BUNDLE/Contents/Resources/FlowTimer.jar" | grep -q "$native"; then
echo -e "${SUCCESS_COLOR}✓ Found $native${NC}"
else
echo -e "${ERROR_COLOR}✗ Missing $native${NC}"
exit 1
fi
done

# Test launcher script syntax
if bash -n "$APP_BUNDLE/Contents/MacOS/FlowTimer"; then
echo -e "${SUCCESS_COLOR}✓ Launcher script syntax is valid${NC}"
else
echo -e "${ERROR_COLOR}✗ Launcher script has syntax errors${NC}"
exit 1
fi

echo ""
echo -e "${SUCCESS_COLOR}🎉 All tests passed! App bundle is ready for deployment.${NC}"
echo ""
echo "Installation instructions:"
echo "1. Copy the app bundle to Applications:"
echo " cp -r FlowTimer.app /Applications/"
echo ""
echo "2. Grant accessibility permissions in System Settings:"
echo " System Settings > Privacy & Security > Accessibility > Add FlowTimer"
echo ""
echo "3. Launch from Spotlight by searching 'FlowTimer'"