Dateien nach "/" hochladen
This commit is contained in:
@@ -0,0 +1,833 @@
|
|||||||
|
#!/bin/zsh
|
||||||
|
# =============================================================================
|
||||||
|
# GeoTurtl_install_v0.9.7-beta.sh
|
||||||
|
# Selbstentpackendes Installationsscript für GeoTurtl v0.9.7-beta
|
||||||
|
#
|
||||||
|
# VERWENDUNG:
|
||||||
|
# chmod +x ~/Downloads/GeoTurtl_v0.9.7-beta/GeoTurtl_install_v0.9.7-beta.sh
|
||||||
|
# ~/Downloads/GeoTurtl_v0.9.7-beta/GeoTurtl_install_v0.9.7-beta.sh
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
VERSION="0.9.7-beta"
|
||||||
|
TARGET="/Applications/Scripte/python/GeoTurtl"
|
||||||
|
SCRIPT="$TARGET/GeoTurtl.py"
|
||||||
|
README_HTML="$TARGET/README.html"
|
||||||
|
README_MD="$TARGET/README.md"
|
||||||
|
STARTER="$TARGET/geoturtl_start.sh"
|
||||||
|
STOPPER="$TARGET/geoturtl_stop.sh"
|
||||||
|
PLIST_DIR="$HOME/Library/LaunchAgents"
|
||||||
|
PLIST="$PLIST_DIR/com.user.geoturtl.plist"
|
||||||
|
LOGFILE="$HOME/Library/Logs/GeoTurtl.log"
|
||||||
|
PIDFILE="$HOME/Library/Application Support/GeoTurtl.pid"
|
||||||
|
|
||||||
|
# Ordner des Installationsscripts (hier liegen die README-Dateien)
|
||||||
|
INSTALL_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "╔══════════════════════════════════════════════════╗"
|
||||||
|
echo "║ 🐢 GeoTurtl v$VERSION – Installation ║"
|
||||||
|
echo "╚══════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Haftungsausschluss ────────────────────────────────────────────────────────
|
||||||
|
echo "┌─────────────────────────────────────────────────────┐"
|
||||||
|
echo "│ ⚠️ HAFTUNGSAUSSCHLUSS │"
|
||||||
|
echo "├─────────────────────────────────────────────────────┤"
|
||||||
|
echo "│ GeoTurtl wurde nach bestem Wissen und Gewissen │"
|
||||||
|
echo "│ entwickelt und kostenlos zur Verfügung gestellt. │"
|
||||||
|
echo "│ │"
|
||||||
|
echo "│ • Die Software wird 'so wie sie ist' (as is) │"
|
||||||
|
echo "│ bereitgestellt – ohne jegliche Garantie. │"
|
||||||
|
echo "│ • H. Linz übernimmt keine Haftung für Schäden, │"
|
||||||
|
echo "│ Datenverluste oder Beeinträchtigungen jeglicher │"
|
||||||
|
echo "│ Art durch Nutzung dieser Software. │"
|
||||||
|
echo "│ • Drittdienste (ipinfo.io, ipify.org) liegen │"
|
||||||
|
echo "│ außerhalb der Verantwortung des Autors. │"
|
||||||
|
echo "│ • Jede Person nutzt diese Software auf eigene │"
|
||||||
|
echo "│ Gefahr und eigene Verantwortung. │"
|
||||||
|
echo "│ │"
|
||||||
|
echo "│ Durch Fortfahren erkennst du diese Bedingungen an. │"
|
||||||
|
echo "└─────────────────────────────────────────────────────┘"
|
||||||
|
echo ""
|
||||||
|
echo -n " Installation fortsetzen? [j/N] "
|
||||||
|
read CONFIRM
|
||||||
|
if [[ "$CONFIRM" != "j" && "$CONFIRM" != "J" ]]; then
|
||||||
|
echo " Installation abgebrochen."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Alte Versionen entfernen ──────────────────────────────────────────────────
|
||||||
|
echo "→ Prüfe auf alte Versionen …"
|
||||||
|
|
||||||
|
# Alle laufenden ddIP/GeoTurtl-Prozesse beenden
|
||||||
|
for PROCNAME in "ddIP.py" "GeoTurtl.py"; do
|
||||||
|
PIDS=$(pgrep -f "$PROCNAME" 2>/dev/null || true)
|
||||||
|
if [ -n "$PIDS" ]; then
|
||||||
|
echo " ✓ Beende laufende Instanz: $PROCNAME (PID $PIDS)"
|
||||||
|
pkill -f "$PROCNAME" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Alte PID-Files entfernen
|
||||||
|
for PIDFILE in "$HOME/Library/Application Support/ddIP.pid" "$HOME/Library/Application Support/GeoTurtl.pid"; do
|
||||||
|
[ -f "$PIDFILE" ] && rm -f "$PIDFILE" && echo " ✓ PID-File entfernt: $PIDFILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Alte Lock-Files entfernen
|
||||||
|
for LOCKFILE in "$HOME/Library/Application Support/GeoTurtl.lock"; do
|
||||||
|
[ -f "$LOCKFILE" ] && rm -f "$LOCKFILE" && echo " ✓ Lock-File entfernt: $LOCKFILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Alte launchd-Agenten entfernen
|
||||||
|
for PLIST in "$HOME/Library/LaunchAgents/com.user.ddip.plist" "$HOME/Library/LaunchAgents/com.user.geoturtl.plist"; do
|
||||||
|
if [ -f "$PLIST" ]; then
|
||||||
|
launchctl unload "$PLIST" 2>/dev/null || true
|
||||||
|
rm -f "$PLIST"
|
||||||
|
echo " ✓ launchd-Agent entfernt: $(basename $PLIST)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Alte Programmordner entfernen
|
||||||
|
for DIR in "/Applications/Scripte/python/ddIP" "/Applications/Scripte/python/GeoTurtl"; do
|
||||||
|
if [ -d "$DIR" ] && [ "$DIR" != "$TARGET" ]; then
|
||||||
|
rm -rf "$DIR"
|
||||||
|
echo " ✓ Alter Programmordner entfernt: $DIR"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Alte Einzeldateien entfernen (ddIP ohne Unterordner)
|
||||||
|
for FILE in "/Applications/Scripte/python/ddIP.py" "/Applications/Scripte/python/ddip_start.sh" "/Applications/Scripte/python/ddip_stop.sh" "/Applications/Scripte/python/geoturtl_start.sh" "/Applications/Scripte/python/geoturtl_stop.sh"; do
|
||||||
|
if [ -f "$FILE" ]; then
|
||||||
|
rm -f "$FILE"
|
||||||
|
echo " ✓ Alte Datei entfernt: $FILE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Alte Aliases aus ~/.zshrc entfernen
|
||||||
|
if grep -qE "alias (ddip|geoturtl)" ~/.zshrc 2>/dev/null; then
|
||||||
|
sed -i '' '/alias ddip/d' ~/.zshrc
|
||||||
|
sed -i '' '/alias geoturtl/d' ~/.zshrc
|
||||||
|
echo " ✓ Alte Aliases aus ~/.zshrc entfernt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " ✓ Aufräumen abgeschlossen"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Python-Pakete ─────────────────────────────────────────────────────────────
|
||||||
|
echo "→ Installiere Python-Pakete …"
|
||||||
|
pip3 install --quiet rumps pyobjc-framework-CoreLocation
|
||||||
|
echo " ✓ Pakete installiert"
|
||||||
|
|
||||||
|
# ── Zielordner ────────────────────────────────────────────────────────────────
|
||||||
|
if [ ! -d "$TARGET" ]; then
|
||||||
|
echo "→ Lege Ordner an: $TARGET"
|
||||||
|
mkdir -p "$TARGET"
|
||||||
|
else
|
||||||
|
echo "→ Ordner existiert bereits: $TARGET"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── README-Dateien kopieren (liegen neben dem Installationsscript) ────────────
|
||||||
|
echo "→ Kopiere Dokumentation …"
|
||||||
|
if [ -f "$INSTALL_DIR/README.html" ]; then
|
||||||
|
cp "$INSTALL_DIR/README.html" "$README_HTML"
|
||||||
|
echo " ✓ README.html kopiert"
|
||||||
|
else
|
||||||
|
echo " ⚠️ README.html nicht gefunden – wird übersprungen"
|
||||||
|
fi
|
||||||
|
if [ -f "$INSTALL_DIR/README.md" ]; then
|
||||||
|
cp "$INSTALL_DIR/README.md" "$README_MD"
|
||||||
|
echo " ✓ README.md kopiert"
|
||||||
|
else
|
||||||
|
echo " ⚠️ README.md nicht gefunden – wird übersprungen"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── GeoTurtl.py erzeugen ─────────────────────────────────────────────────────
|
||||||
|
echo "→ Erzeuge GeoTurtl.py …"
|
||||||
|
cat > "$SCRIPT" << 'PYEOF'
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# =============================================================================
|
||||||
|
# yaml
|
||||||
|
# name: GeoTurtl
|
||||||
|
# version: 0.9.7-beta
|
||||||
|
# description: macOS Menüleisten-App – Standort + öffentliche IP-Adressen
|
||||||
|
# author: H. Linz
|
||||||
|
# contact: linz.on.tour@gmail.com
|
||||||
|
# license: MIT
|
||||||
|
# requires_python: ">=3.6"
|
||||||
|
# dependencies:
|
||||||
|
# - rumps
|
||||||
|
# - pyobjc-framework-CoreLocation
|
||||||
|
# platform: macOS (Apple Silicon & Intel)
|
||||||
|
# created: 2026-06-07
|
||||||
|
# disclaimer: >
|
||||||
|
# Diese Software wird ohne jegliche Garantie bereitgestellt.
|
||||||
|
# Nutzung auf eigene Gefahr. Keine Haftung für Schäden.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import objc
|
||||||
|
import rumps
|
||||||
|
from CoreLocation import CLGeocoder, CLLocation, CLLocationManager
|
||||||
|
from Foundation import NSBundle, NSDate, NSRunLoop
|
||||||
|
|
||||||
|
APP_TITLE = "🐢"
|
||||||
|
APP_VERSION = "0.9.7-beta"
|
||||||
|
AUTHOR = "H. Linz"
|
||||||
|
CONTACT = "linz.on.tour@gmail.com"
|
||||||
|
UPDATE_INTERVALS = {"Aus": 0, "1 Minute": 60, "5 Minuten": 300, "15 Minuten": 900}
|
||||||
|
DEFAULT_INTERVAL = "5 Minuten"
|
||||||
|
TIMEOUT = 8
|
||||||
|
LOC_TIMEOUT = 15
|
||||||
|
README_HTML = os.path.join(os.path.dirname(os.path.abspath(__file__)), "README.html")
|
||||||
|
|
||||||
|
|
||||||
|
# ── CoreLocation Delegate ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class LocationDelegate(objc.lookUpClass("NSObject")):
|
||||||
|
def init(self):
|
||||||
|
self = objc.super(LocationDelegate, self).init()
|
||||||
|
if self is None:
|
||||||
|
return None
|
||||||
|
self.location = None
|
||||||
|
self.error = None
|
||||||
|
self.done = threading.Event()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def locationManager_didUpdateLocations_(self, manager, locations):
|
||||||
|
if locations:
|
||||||
|
self.location = locations[-1]
|
||||||
|
manager.stopUpdatingLocation()
|
||||||
|
self.done.set()
|
||||||
|
|
||||||
|
def locationManager_didFailWithError_(self, manager, error):
|
||||||
|
self.error = str(error)
|
||||||
|
manager.stopUpdatingLocation()
|
||||||
|
self.done.set()
|
||||||
|
|
||||||
|
def locationManagerDidChangeAuthorization_(self, manager):
|
||||||
|
status = manager.authorizationStatus()
|
||||||
|
if status in (1, 2):
|
||||||
|
self.error = "Ortungsdienste verweigert"
|
||||||
|
self.done.set()
|
||||||
|
|
||||||
|
|
||||||
|
def get_location(retries=4, delay=3):
|
||||||
|
"""
|
||||||
|
Versucht den Standort zu ermitteln. Bei kCLErrorLocationUnknown (Code=0)
|
||||||
|
wird bis zu `retries`-mal neu gestartet – typisch wenn WLAN-Ortung
|
||||||
|
kurz nach dem Login noch nicht bereit ist.
|
||||||
|
|
||||||
|
Fehlercodes:
|
||||||
|
Code=0 LocationUnknown – kurz warten, nochmal versuchen
|
||||||
|
Code=1 Denied – Ortungsdienste verweigert
|
||||||
|
Code=2 Network – Netzwerkfehler bei Ortung
|
||||||
|
"""
|
||||||
|
for attempt in range(1, retries + 1):
|
||||||
|
delegate = LocationDelegate.alloc().init()
|
||||||
|
manager = CLLocationManager.alloc().init()
|
||||||
|
manager.setDelegate_(delegate)
|
||||||
|
manager.setDesiredAccuracy_(10)
|
||||||
|
manager.requestWhenInUseAuthorization()
|
||||||
|
manager.startUpdatingLocation()
|
||||||
|
deadline = time.time() + LOC_TIMEOUT
|
||||||
|
while not delegate.done.is_set() and time.time() < deadline:
|
||||||
|
NSRunLoop.currentRunLoop().runUntilDate_(NSDate.dateWithTimeIntervalSinceNow_(0.2))
|
||||||
|
|
||||||
|
if not delegate.done.is_set():
|
||||||
|
raise TimeoutError(
|
||||||
|
f"Standort-Timeout nach {LOC_TIMEOUT}s (Versuch {attempt}/{retries})")
|
||||||
|
|
||||||
|
if delegate.error:
|
||||||
|
err = delegate.error
|
||||||
|
# Code=0: vorübergehend unbekannt → retry
|
||||||
|
if "Code=0" in err:
|
||||||
|
if attempt < retries:
|
||||||
|
_log(f"Standort: kCLErrorLocationUnknown (Versuch {attempt}/{retries}) "
|
||||||
|
f"– warte {delay}s …", "WARN")
|
||||||
|
time.sleep(delay)
|
||||||
|
continue
|
||||||
|
raise RuntimeError(
|
||||||
|
"Standort nach mehreren Versuchen nicht ermittelbar "
|
||||||
|
"(WLAN-Ortung liefert kein Signal)")
|
||||||
|
# Code=1: Berechtigung verweigert
|
||||||
|
elif "Code=1" in err:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Ortungsdienste verweigert – bitte in Systemeinstellungen "
|
||||||
|
"→ Datenschutz & Sicherheit → Ortungsdienste → Terminal aktivieren")
|
||||||
|
# Code=2: Netzwerkfehler
|
||||||
|
elif "Code=2" in err:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Netzwerkfehler bei Standortermittlung (kCLErrorNetwork)")
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"CoreLocation-Fehler: {err}")
|
||||||
|
|
||||||
|
return delegate.location
|
||||||
|
raise RuntimeError("Standort nach mehreren Versuchen nicht ermittelbar")
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_geocode(lat, lon):
|
||||||
|
result = {"address": None, "done": threading.Event()}
|
||||||
|
cl_loc = CLLocation.alloc().initWithLatitude_longitude_(lat, lon)
|
||||||
|
geocoder = CLGeocoder.alloc().init()
|
||||||
|
|
||||||
|
def handler(placemarks, error):
|
||||||
|
if placemarks:
|
||||||
|
pm = placemarks[0]
|
||||||
|
parts = []
|
||||||
|
if pm.thoroughfare():
|
||||||
|
street = pm.thoroughfare()
|
||||||
|
if pm.subThoroughfare():
|
||||||
|
street += f" {pm.subThoroughfare()}"
|
||||||
|
parts.append(street)
|
||||||
|
if pm.postalCode():
|
||||||
|
parts.append(pm.postalCode())
|
||||||
|
if pm.locality():
|
||||||
|
parts.append(pm.locality())
|
||||||
|
result["address"] = ", ".join(parts) if parts else "Unbekannte Adresse"
|
||||||
|
else:
|
||||||
|
result["address"] = "Adresse nicht verfügbar"
|
||||||
|
result["done"].set()
|
||||||
|
|
||||||
|
geocoder.reverseGeocodeLocation_completionHandler_(cl_loc, handler)
|
||||||
|
deadline = time.time() + 10
|
||||||
|
while not result["done"].is_set() and time.time() < deadline:
|
||||||
|
NSRunLoop.currentRunLoop().runUntilDate_(NSDate.dateWithTimeIntervalSinceNow_(0.2))
|
||||||
|
return result.get("address") or "—"
|
||||||
|
|
||||||
|
|
||||||
|
# ── IP-Dienste ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
IPV4_SERVICES = [
|
||||||
|
{"url": "https://ipinfo.io/json",
|
||||||
|
"parse": lambda d: {"ip": d.get("ip"), "org": d.get("org"),
|
||||||
|
"city": d.get("city"), "region": d.get("region"),
|
||||||
|
"country": d.get("country")}},
|
||||||
|
{"url": "https://api4.ipify.org?format=json",
|
||||||
|
"parse": lambda d: {"ip": d.get("ip"), "org": None,
|
||||||
|
"city": None, "region": None, "country": None}},
|
||||||
|
]
|
||||||
|
IPV6_SERVICES = [
|
||||||
|
{"url": "https://api6.ipify.org?format=json",
|
||||||
|
"parse": lambda d: {"ip": d.get("ip")}},
|
||||||
|
{"url": "https://ipv6.icanhazip.com", "raw": True,
|
||||||
|
"parse": lambda d: {"ip": d.get("ip")}},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_ip(service):
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
service["url"],
|
||||||
|
headers={"User-Agent": "GeoTurtl/0.9.7-beta (macOS; python3)"})
|
||||||
|
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
|
||||||
|
body = resp.read().decode().strip()
|
||||||
|
return service["parse"]({"ip": body} if service.get("raw") else json.loads(body))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_ips():
|
||||||
|
v4, v6 = None, None
|
||||||
|
|
||||||
|
def do_v4():
|
||||||
|
nonlocal v4
|
||||||
|
for svc in IPV4_SERVICES:
|
||||||
|
r = fetch_ip(svc)
|
||||||
|
if r and r.get("ip"):
|
||||||
|
v4 = r; break
|
||||||
|
|
||||||
|
def do_v6():
|
||||||
|
nonlocal v6
|
||||||
|
for svc in IPV6_SERVICES:
|
||||||
|
r = fetch_ip(svc)
|
||||||
|
if r and r.get("ip"):
|
||||||
|
v6 = r; break
|
||||||
|
|
||||||
|
t4, t6 = threading.Thread(target=do_v4), threading.Thread(target=do_v6)
|
||||||
|
t4.start(); t6.start()
|
||||||
|
t4.join(); t6.join()
|
||||||
|
return v4, v6
|
||||||
|
|
||||||
|
|
||||||
|
# ── Logging ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _log(msg, level="INFO"):
|
||||||
|
"""Schreibt eine Zeile mit Zeitstempel und Level nach stdout (→ Log-Datei)."""
|
||||||
|
ts = datetime.now().strftime("%d.%m.%Y %H:%M:%S")
|
||||||
|
print(f"[{ts}] [{level}] {msg}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Netzwerk-Diagnose ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def check_internet():
|
||||||
|
"""
|
||||||
|
Gestaffelte Netzwerkprüfung:
|
||||||
|
1. TCP-Verbindung zu 8.8.8.8:53 → grundlegende IP-Konnektivität
|
||||||
|
2. DNS-Auflösung von google.com → DNS funktioniert
|
||||||
|
3. HTTP-Request zu ipinfo.io → Internet + Firewall OK
|
||||||
|
Gibt (ok: bool, detail: str) zurück.
|
||||||
|
"""
|
||||||
|
# Stufe 1: TCP
|
||||||
|
try:
|
||||||
|
sock = socket.create_connection(("8.8.8.8", 53), timeout=3)
|
||||||
|
sock.close()
|
||||||
|
except OSError:
|
||||||
|
return False, "Keine Netzwerkverbindung (TCP zu 8.8.8.8:53 fehlgeschlagen)"
|
||||||
|
|
||||||
|
# Stufe 2: DNS
|
||||||
|
try:
|
||||||
|
socket.getaddrinfo("google.com", 80)
|
||||||
|
except socket.gaierror:
|
||||||
|
return False, "Netzwerk erreichbar, aber DNS nicht funktionsfähig"
|
||||||
|
|
||||||
|
# Stufe 3: HTTP
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"https://ipinfo.io/json",
|
||||||
|
headers={"User-Agent": "GeoTurtl/0.9.7-beta (macOS; python3)"})
|
||||||
|
with urllib.request.urlopen(req, timeout=5):
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"DNS OK, aber HTTP-Anfragen blockiert oder kein Internet ({e})"
|
||||||
|
|
||||||
|
return True, "OK"
|
||||||
|
|
||||||
|
|
||||||
|
def diagnose_network():
|
||||||
|
"""Führt Netzwerkdiagnose durch und gibt lesbaren Status zurück."""
|
||||||
|
ok, detail = check_internet()
|
||||||
|
if ok:
|
||||||
|
_log("Netzwerk: Verbindung OK")
|
||||||
|
else:
|
||||||
|
_log(f"Netzwerk: {detail}", "WARN")
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
# ── Menüleisten-App ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class GeoTurtlApp(rumps.App):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(APP_TITLE, quit_button=None)
|
||||||
|
self._loc_data = None
|
||||||
|
self._ip_data = None
|
||||||
|
self._updating = False
|
||||||
|
self._interval = DEFAULT_INTERVAL
|
||||||
|
self._timer = None
|
||||||
|
self._v4_str = None
|
||||||
|
self._v6_str = None
|
||||||
|
self._build_menu()
|
||||||
|
threading.Thread(target=self._update_all, daemon=True).start()
|
||||||
|
|
||||||
|
def _build_menu(self):
|
||||||
|
self.menu.clear()
|
||||||
|
|
||||||
|
self.menu.add(rumps.MenuItem("🔄 Alles aktualisieren", callback=self._on_refresh))
|
||||||
|
self.menu.add(rumps.separator)
|
||||||
|
|
||||||
|
# ── Standort ──────────────────────────────────────────────────────────
|
||||||
|
self.menu.add(rumps.MenuItem("📍 STANDORT", callback=None))
|
||||||
|
self._item_address = rumps.MenuItem(" Adresse …")
|
||||||
|
self._item_coords = rumps.MenuItem(" Koordinaten …")
|
||||||
|
self._item_altitude = rumps.MenuItem(" Höhe …")
|
||||||
|
self._item_accuracy = rumps.MenuItem(" Genauigkeit …")
|
||||||
|
self.menu.add(self._item_address)
|
||||||
|
self.menu.add(self._item_coords)
|
||||||
|
self.menu.add(self._item_altitude)
|
||||||
|
self.menu.add(self._item_accuracy)
|
||||||
|
self.menu.add(rumps.separator)
|
||||||
|
|
||||||
|
self._item_gmaps = rumps.MenuItem(" 🗺 Google Maps öffnen", callback=self._open_gmaps)
|
||||||
|
self._item_amaps = rumps.MenuItem(" 🍎 Apple Maps öffnen", callback=self._open_amaps)
|
||||||
|
self._item_osm = rumps.MenuItem(" 🌍 OpenStreetMap öffnen", callback=self._open_osm)
|
||||||
|
self.menu.add(self._item_gmaps)
|
||||||
|
self.menu.add(self._item_amaps)
|
||||||
|
self.menu.add(self._item_osm)
|
||||||
|
self.menu.add(rumps.separator)
|
||||||
|
|
||||||
|
# ── IP-Adressen ───────────────────────────────────────────────────────
|
||||||
|
self.menu.add(rumps.MenuItem("🌐 IP-ADRESSEN", callback=None))
|
||||||
|
self._item_v4 = rumps.MenuItem(" IPv4 …")
|
||||||
|
self._item_v4_loc = rumps.MenuItem(" Standort …")
|
||||||
|
self._item_v4_org = rumps.MenuItem(" Provider …")
|
||||||
|
self._item_copy_v4 = rumps.MenuItem(" 📋 IPv4 kopieren", callback=self._copy_v4)
|
||||||
|
self._item_v6 = rumps.MenuItem(" IPv6 …")
|
||||||
|
self._item_copy_v6 = rumps.MenuItem(" 📋 IPv6 kopieren", callback=self._copy_v6)
|
||||||
|
self.menu.add(self._item_v4)
|
||||||
|
self.menu.add(self._item_v4_loc)
|
||||||
|
self.menu.add(self._item_v4_org)
|
||||||
|
self.menu.add(self._item_copy_v4)
|
||||||
|
self.menu.add(rumps.separator)
|
||||||
|
self.menu.add(self._item_v6)
|
||||||
|
self.menu.add(self._item_copy_v6)
|
||||||
|
self.menu.add(rumps.separator)
|
||||||
|
|
||||||
|
# ── Auto-Update ───────────────────────────────────────────────────────
|
||||||
|
auto_menu = rumps.MenuItem("⏱ Auto-Update")
|
||||||
|
for label in UPDATE_INTERVALS:
|
||||||
|
item = rumps.MenuItem(
|
||||||
|
f" {label}" + (" ✓" if label == self._interval else ""),
|
||||||
|
callback=self._on_interval)
|
||||||
|
auto_menu.add(item)
|
||||||
|
self.menu.add(auto_menu)
|
||||||
|
self.menu.add(rumps.separator)
|
||||||
|
|
||||||
|
# ── Hilfe ─────────────────────────────────────────────────────────────
|
||||||
|
hilfe_menu = rumps.MenuItem("❓ Hilfe")
|
||||||
|
hilfe_menu.add(rumps.MenuItem(f" GeoTurtl v{APP_VERSION}"))
|
||||||
|
hilfe_menu.add(rumps.separator)
|
||||||
|
hilfe_menu.add(rumps.MenuItem(" 📖 Dokumentation öffnen", callback=self._open_readme))
|
||||||
|
hilfe_menu.add(rumps.separator)
|
||||||
|
hilfe_menu.add(rumps.MenuItem(" TERMINAL-BEFEHLE"))
|
||||||
|
hilfe_menu.add(rumps.MenuItem(" ▶ Starten: geoturtl_start"))
|
||||||
|
hilfe_menu.add(rumps.MenuItem(" ■ Stoppen: geoturtl_stop"))
|
||||||
|
hilfe_menu.add(rumps.MenuItem(" 📄 Log: tail -f ~/Library/Logs/GeoTurtl.log"))
|
||||||
|
hilfe_menu.add(rumps.MenuItem(" 📝 Log öffnen", callback=self._open_log))
|
||||||
|
hilfe_menu.add(rumps.separator)
|
||||||
|
hilfe_menu.add(rumps.MenuItem(" KONTAKT"))
|
||||||
|
hilfe_menu.add(rumps.MenuItem(f" {AUTHOR}"))
|
||||||
|
hilfe_menu.add(rumps.MenuItem(f" ✉️ {CONTACT}", callback=self._open_mail))
|
||||||
|
self.menu.add(hilfe_menu)
|
||||||
|
self.menu.add(rumps.separator)
|
||||||
|
|
||||||
|
# ── Zeitstempel + Beenden ─────────────────────────────────────────────
|
||||||
|
self._item_updated = rumps.MenuItem(" Noch nicht aktualisiert")
|
||||||
|
self.menu.add(self._item_updated)
|
||||||
|
self.menu.add(rumps.separator)
|
||||||
|
self.menu.add(rumps.MenuItem("✖ Beenden", callback=self._on_quit))
|
||||||
|
|
||||||
|
# ── Callbacks ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _on_refresh(self, _):
|
||||||
|
if not self._updating:
|
||||||
|
threading.Thread(target=self._update_all, daemon=True).start()
|
||||||
|
|
||||||
|
def _on_interval(self, sender):
|
||||||
|
label = sender.title.strip().rstrip(" ✓").strip()
|
||||||
|
self._interval = label
|
||||||
|
for item in self.menu["⏱ Auto-Update"].values():
|
||||||
|
clean = item.title.strip().rstrip(" ✓").strip()
|
||||||
|
item.title = f" {clean}" + (" ✓" if clean == label else "")
|
||||||
|
self._restart_timer()
|
||||||
|
|
||||||
|
def _on_quit(self, _):
|
||||||
|
rumps.quit_application()
|
||||||
|
|
||||||
|
def _open_readme(self, _):
|
||||||
|
if os.path.exists(README_HTML):
|
||||||
|
os.system(f"open '{README_HTML}'")
|
||||||
|
else:
|
||||||
|
rumps.notification("GeoTurtl", "Dokumentation nicht gefunden",
|
||||||
|
README_HTML)
|
||||||
|
|
||||||
|
def _open_gmaps(self, _):
|
||||||
|
if self._loc_data and "lat" in self._loc_data:
|
||||||
|
lat, lon = self._loc_data["lat"], self._loc_data["lon"]
|
||||||
|
os.system(f"open 'https://maps.google.com/?q={lat},{lon}'")
|
||||||
|
|
||||||
|
def _open_amaps(self, _):
|
||||||
|
if self._loc_data and "lat" in self._loc_data:
|
||||||
|
lat, lon = self._loc_data["lat"], self._loc_data["lon"]
|
||||||
|
os.system(f"open 'https://maps.apple.com/?ll={lat},{lon}&q=GeoTurtl'")
|
||||||
|
|
||||||
|
def _open_osm(self, _):
|
||||||
|
if self._loc_data and "lat" in self._loc_data:
|
||||||
|
lat, lon = self._loc_data["lat"], self._loc_data["lon"]
|
||||||
|
os.system(f"open 'https://www.openstreetmap.org/?mlat={lat}&mlon={lon}&zoom=16'")
|
||||||
|
|
||||||
|
def _open_log(self, _):
|
||||||
|
log = os.path.expanduser("~/Library/Logs/GeoTurtl.log")
|
||||||
|
if not os.path.exists(log):
|
||||||
|
# Leere Log-Datei anlegen damit der Editor sie öffnen kann
|
||||||
|
open(log, "a").close()
|
||||||
|
os.system(f"open -e '{log}'") # -e = immer TextEdit
|
||||||
|
|
||||||
|
def _open_mail(self, _):
|
||||||
|
os.system(f"open 'mailto:{CONTACT}?subject=GeoTurtl%20v{APP_VERSION}'")
|
||||||
|
|
||||||
|
def _copy_v4(self, _):
|
||||||
|
if self._v4_str:
|
||||||
|
subprocess.run("pbcopy", input=self._v4_str.encode(), check=True)
|
||||||
|
rumps.notification("GeoTurtl", "IPv4 kopiert", self._v4_str)
|
||||||
|
|
||||||
|
def _copy_v6(self, _):
|
||||||
|
if self._v6_str:
|
||||||
|
subprocess.run("pbcopy", input=self._v6_str.encode(), check=True)
|
||||||
|
rumps.notification("GeoTurtl", "IPv6 kopiert", self._v6_str)
|
||||||
|
|
||||||
|
# ── Timer ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _restart_timer(self):
|
||||||
|
if self._timer:
|
||||||
|
self._timer.stop()
|
||||||
|
self._timer = None
|
||||||
|
secs = UPDATE_INTERVALS.get(self._interval, 0)
|
||||||
|
if secs > 0:
|
||||||
|
self._timer = rumps.Timer(self._timer_tick, secs)
|
||||||
|
self._timer.start()
|
||||||
|
|
||||||
|
def _timer_tick(self, _):
|
||||||
|
if not self._updating:
|
||||||
|
threading.Thread(target=self._update_all, daemon=True).start()
|
||||||
|
|
||||||
|
# ── Daten aktualisieren ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _update_all(self):
|
||||||
|
self._updating = True
|
||||||
|
self.title = "🐢 ⟳"
|
||||||
|
loc_thread = threading.Thread(target=self._fetch_location, daemon=True)
|
||||||
|
ip_thread = threading.Thread(target=self._fetch_ips, daemon=True)
|
||||||
|
loc_thread.start(); ip_thread.start()
|
||||||
|
loc_thread.join(); ip_thread.join()
|
||||||
|
self._refresh_menu()
|
||||||
|
self._updating = False
|
||||||
|
self._restart_timer()
|
||||||
|
|
||||||
|
def _fetch_location(self):
|
||||||
|
try:
|
||||||
|
loc = get_location()
|
||||||
|
lat = loc.coordinate().latitude
|
||||||
|
lon = loc.coordinate().longitude
|
||||||
|
self._loc_data = {
|
||||||
|
"lat": lat,
|
||||||
|
"lon": lon,
|
||||||
|
"alt": loc.altitude(),
|
||||||
|
"acc": loc.horizontalAccuracy(),
|
||||||
|
"address": reverse_geocode(lat, lon),
|
||||||
|
}
|
||||||
|
_log(f"Standort: {self._loc_data['address']} "
|
||||||
|
f"({lat:.6f}, {lon:.6f}), "
|
||||||
|
f"Genauigkeit ±{loc.horizontalAccuracy():.0f}m")
|
||||||
|
except Exception as e:
|
||||||
|
_log(f"Standort: {e}", "ERROR")
|
||||||
|
self._loc_data = {"error": str(e)}
|
||||||
|
|
||||||
|
def _fetch_ips(self):
|
||||||
|
v4, v6 = get_ips()
|
||||||
|
self._ip_data = {"v4": v4, "v6": v6}
|
||||||
|
self._v4_str = v4["ip"] if v4 else None
|
||||||
|
self._v6_str = v6["ip"] if v6 else None
|
||||||
|
|
||||||
|
if v4:
|
||||||
|
_log(f"IPv4: {v4['ip']} – {v4.get('org') or '—'}")
|
||||||
|
else:
|
||||||
|
# Gestaffelte Diagnose nur wenn IP-Abruf fehlschlug
|
||||||
|
_log("IPv4: nicht ermittelbar – starte Netzwerkdiagnose …", "WARN")
|
||||||
|
net_ok = diagnose_network()
|
||||||
|
if not net_ok:
|
||||||
|
self._ip_data["net_error"] = "Keine Internetverbindung"
|
||||||
|
_log("Kein Internet – IP-Lokalisierung nicht möglich", "ERROR")
|
||||||
|
else:
|
||||||
|
self._ip_data["net_error"] = "IP-Dienste nicht erreichbar"
|
||||||
|
_log("Internet OK, aber alle IP-Dienste nicht erreichbar", "WARN")
|
||||||
|
|
||||||
|
if v6:
|
||||||
|
_log(f"IPv6: {v6['ip']}")
|
||||||
|
else:
|
||||||
|
_log("IPv6: nicht verfügbar (Provider unterstützt kein IPv6)")
|
||||||
|
|
||||||
|
# ── Menü aktualisieren ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _refresh_menu(self):
|
||||||
|
loc = self._loc_data or {}
|
||||||
|
ips = self._ip_data or {}
|
||||||
|
v4 = ips.get("v4") or {}
|
||||||
|
v6 = ips.get("v6") or {}
|
||||||
|
|
||||||
|
loc_error = "error" in (self._loc_data or {})
|
||||||
|
ip_error = not (self._ip_data or {}).get("v4") and not (self._ip_data or {}).get("v6")
|
||||||
|
self.title = "🐢 ⚠️" if (loc_error or ip_error) else "🐢"
|
||||||
|
|
||||||
|
if "error" in loc:
|
||||||
|
self._item_address.title = f" ⚠️ {loc['error']}"
|
||||||
|
self._item_coords.title = " Koordinaten: —"
|
||||||
|
self._item_altitude.title = " Höhe: —"
|
||||||
|
self._item_accuracy.title = " Genauigkeit: —"
|
||||||
|
else:
|
||||||
|
self._item_address.title = f" {loc.get('address', '—')}"
|
||||||
|
self._item_coords.title = (f" {loc['lat']:.6f}°, {loc['lon']:.6f}°"
|
||||||
|
if "lat" in loc else " —")
|
||||||
|
alt = loc.get('alt', 0)
|
||||||
|
alt_str = f"{alt:.0f} m ü. NN" if alt and abs(alt) > 0.5 else "nicht verfügbar (WLAN-Ortung)"
|
||||||
|
self._item_altitude.title = f" Höhe: {alt_str}"
|
||||||
|
self._item_accuracy.title = f" Genauigkeit: ± {loc.get('acc', 0):.0f} m"
|
||||||
|
|
||||||
|
if v4.get("ip"):
|
||||||
|
self._item_v4.title = f" IPv4: {v4['ip']}"
|
||||||
|
loc_parts = [p for p in [v4.get("city"), v4.get("region"), v4.get("country")] if p]
|
||||||
|
self._item_v4_loc.title = f" Standort: {', '.join(loc_parts) if loc_parts else '—'}"
|
||||||
|
self._item_v4_org.title = f" Provider: {v4.get('org') or '—'}"
|
||||||
|
else:
|
||||||
|
net_err = ips.get("net_error", "")
|
||||||
|
if "Kein Internet" in net_err or "Keine Internet" in net_err:
|
||||||
|
self._item_v4.title = " ⚠️ Kein Internet"
|
||||||
|
self._item_v4_loc.title = " Prüfe deine Netzwerkverbindung"
|
||||||
|
self._item_v4_org.title = " —"
|
||||||
|
elif net_err:
|
||||||
|
self._item_v4.title = " ⚠️ IP-Dienste nicht erreichbar"
|
||||||
|
self._item_v4_loc.title = " Internet OK, Dienste blockiert?"
|
||||||
|
self._item_v4_org.title = " —"
|
||||||
|
else:
|
||||||
|
self._item_v4.title = " IPv4: nicht verfügbar"
|
||||||
|
self._item_v4_loc.title = " Standort: —"
|
||||||
|
self._item_v4_org.title = " Provider: —"
|
||||||
|
|
||||||
|
if v6.get("ip"):
|
||||||
|
ip6 = v6["ip"]
|
||||||
|
self._item_v6.title = f" IPv6: {ip6 if len(ip6) <= 30 else ip6[:27] + '…'}"
|
||||||
|
else:
|
||||||
|
self._item_v6.title = " IPv6: nicht verfügbar"
|
||||||
|
|
||||||
|
self._item_updated.title = f" Aktualisiert: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Start ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import platform
|
||||||
|
import fcntl
|
||||||
|
|
||||||
|
# ── Single-Instance-Lock: verhindert doppelte Prozesse ────────────────────
|
||||||
|
LOCK_FILE = os.path.expanduser("~/Library/Application Support/GeoTurtl.lock")
|
||||||
|
os.makedirs(os.path.dirname(LOCK_FILE), exist_ok=True)
|
||||||
|
try:
|
||||||
|
_lock_fh = open(LOCK_FILE, "w")
|
||||||
|
fcntl.flock(_lock_fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
except BlockingIOError:
|
||||||
|
print(f"[{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}] [WARN] "
|
||||||
|
f"GeoTurtl läuft bereits – zweite Instanz wird beendet.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# ── Dock-Icon unterdrücken ────────────────────────────────────────────────
|
||||||
|
info = NSBundle.mainBundle().infoDictionary()
|
||||||
|
info["LSUIElement"] = "1"
|
||||||
|
|
||||||
|
_log(f"GeoTurtl v{APP_VERSION} gestartet "
|
||||||
|
f"(PID {os.getpid()}, Python {platform.python_version()}, "
|
||||||
|
f"macOS {platform.mac_ver()[0]})")
|
||||||
|
app = GeoTurtlApp()
|
||||||
|
app.run()
|
||||||
|
PYEOF
|
||||||
|
chmod +x "$SCRIPT"
|
||||||
|
echo " ✓ GeoTurtl.py erzeugt"
|
||||||
|
|
||||||
|
# ── Starter ───────────────────────────────────────────────────────────────────
|
||||||
|
echo "→ Erzeuge Starter …"
|
||||||
|
cat > "$STARTER" << STARTEOF
|
||||||
|
#!/bin/zsh
|
||||||
|
LOGFILE="$LOGFILE"
|
||||||
|
PIDFILE="$PIDFILE"
|
||||||
|
mkdir -p "\$(dirname \$PIDFILE)"
|
||||||
|
if [ -f "\$PIDFILE" ]; then
|
||||||
|
OLD_PID=\$(cat "\$PIDFILE")
|
||||||
|
if kill -0 "\$OLD_PID" 2>/dev/null; then
|
||||||
|
echo "Beende laufende GeoTurtl-Instanz (PID \$OLD_PID) …"
|
||||||
|
kill "\$OLD_PID" && sleep 1
|
||||||
|
fi
|
||||||
|
rm -f "\$PIDFILE"
|
||||||
|
fi
|
||||||
|
nohup $(which python3) "$SCRIPT" > "\$LOGFILE" 2>&1 &
|
||||||
|
echo \$! > "\$PIDFILE"
|
||||||
|
echo "GeoTurtl gestartet (PID \$!)"
|
||||||
|
echo "Log: \$LOGFILE"
|
||||||
|
STARTEOF
|
||||||
|
chmod +x "$STARTER"
|
||||||
|
echo " ✓ Starter erzeugt"
|
||||||
|
|
||||||
|
# ── Stopper ───────────────────────────────────────────────────────────────────
|
||||||
|
echo "→ Erzeuge Stopper …"
|
||||||
|
cat > "$STOPPER" << STOPEOF
|
||||||
|
#!/bin/zsh
|
||||||
|
PIDFILE="$PIDFILE"
|
||||||
|
if [ -f "\$PIDFILE" ]; then
|
||||||
|
PID=\$(cat "\$PIDFILE")
|
||||||
|
if kill -0 "\$PID" 2>/dev/null; then
|
||||||
|
kill "\$PID" && echo "GeoTurtl beendet (PID \$PID)."
|
||||||
|
else
|
||||||
|
echo "GeoTurtl lief nicht mehr."
|
||||||
|
fi
|
||||||
|
rm -f "\$PIDFILE"
|
||||||
|
else
|
||||||
|
echo "Kein PID-File — GeoTurtl läuft nicht."
|
||||||
|
fi
|
||||||
|
STOPEOF
|
||||||
|
chmod +x "$STOPPER"
|
||||||
|
echo " ✓ Stopper erzeugt"
|
||||||
|
|
||||||
|
# ── launchd-Autostart ─────────────────────────────────────────────────────────
|
||||||
|
echo "→ Richte Autostart via launchd ein …"
|
||||||
|
mkdir -p "$PLIST_DIR"
|
||||||
|
PYTHON_BIN=$(which python3)
|
||||||
|
cat > "$PLIST" << PLISTEOF
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||||
|
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.user.geoturtl</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>$PYTHON_BIN</string>
|
||||||
|
<string>$SCRIPT</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<false/>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>$LOGFILE</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>$LOGFILE</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
PLISTEOF
|
||||||
|
launchctl unload "$PLIST" 2>/dev/null || true
|
||||||
|
launchctl load "$PLIST"
|
||||||
|
echo " ✓ Autostart eingerichtet"
|
||||||
|
|
||||||
|
# ── Aliases ───────────────────────────────────────────────────────────────────
|
||||||
|
for ENTRY in "geoturtl_start=$STARTER" "geoturtl_stop=$STOPPER"; do
|
||||||
|
ANAME="${ENTRY%%=*}"
|
||||||
|
AVAL="${ENTRY#*=}"
|
||||||
|
ALINE="alias $ANAME='$AVAL'"
|
||||||
|
if grep -q "alias $ANAME=" ~/.zshrc 2>/dev/null; then
|
||||||
|
echo "→ Alias '$ANAME' bereits vorhanden, wird übersprungen."
|
||||||
|
else
|
||||||
|
echo "$ALINE" >> ~/.zshrc
|
||||||
|
echo "→ Alias '$ANAME' eingetragen."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
source ~/.zshrc 2>/dev/null || true
|
||||||
|
|
||||||
|
# ── Fertig ────────────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "╔══════════════════════════════════════════════════╗"
|
||||||
|
echo "║ ✓ GeoTurtl v$VERSION – Installation fertig! ║"
|
||||||
|
echo "║ ║"
|
||||||
|
echo "║ 🐢 Starten: geoturtl_start ║"
|
||||||
|
echo "║ ■ Stoppen: geoturtl_stop ║"
|
||||||
|
echo "║ 📄 Log: ~/Library/Logs/GeoTurtl.log ║"
|
||||||
|
echo "║ 📖 Doku: $TARGET/README.html ║"
|
||||||
|
echo "╚══════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
echo "HINWEIS: Beim ersten Start Ortungsdienste erlauben:"
|
||||||
|
echo " Systemeinstellungen → Datenschutz & Sicherheit"
|
||||||
|
echo " → Ortungsdienste → Terminal → Beim Verwenden"
|
||||||
|
echo ""
|
||||||
|
echo "→ Starte GeoTurtl jetzt im Hintergrund …"
|
||||||
|
"$STARTER"
|
||||||
+666
@@ -0,0 +1,666 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>🐢 GeoTurtl v0.9.7-beta – Dokumentation</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--color-canvas-default: #ffffff;
|
||||||
|
--color-canvas-subtle: #f6f8fa;
|
||||||
|
--color-border-default: #d0d7de;
|
||||||
|
--color-border-muted: #d8dee4;
|
||||||
|
--color-fg-default: #1f2328;
|
||||||
|
--color-fg-muted: #656d76;
|
||||||
|
--color-fg-subtle: #6e7781;
|
||||||
|
--color-accent: #2da44e;
|
||||||
|
--color-accent-subtle: #dafbe1;
|
||||||
|
--color-accent-emphasis: #1a7f37;
|
||||||
|
--color-turtle: #2da44e;
|
||||||
|
--color-link: #0969da;
|
||||||
|
--color-warning-subtle: #fff8c5;
|
||||||
|
--color-warning-border: #d4a72c;
|
||||||
|
--color-danger-subtle: #ffebe9;
|
||||||
|
--color-danger-border: #ff8182;
|
||||||
|
--radius: 6px;
|
||||||
|
--font-mono: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--color-fg-default);
|
||||||
|
background: var(--color-canvas-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Header ── */
|
||||||
|
.gh-header {
|
||||||
|
background: #24292f;
|
||||||
|
padding: 16px 0;
|
||||||
|
border-bottom: 1px solid #30363d;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.gh-header-inner {
|
||||||
|
max-width: 1012px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.gh-logo { font-size: 24px; }
|
||||||
|
.gh-repo {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.3px;
|
||||||
|
}
|
||||||
|
.gh-repo span { color: #848d97; font-weight: 400; }
|
||||||
|
.gh-badge {
|
||||||
|
background: var(--color-accent-subtle);
|
||||||
|
color: var(--color-accent-emphasis);
|
||||||
|
border: 1px solid #82e0a0;
|
||||||
|
border-radius: 2em;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 10px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Layout ── */
|
||||||
|
.layout {
|
||||||
|
max-width: 1012px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 32px 32px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 260px;
|
||||||
|
gap: 32px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sidebar ── */
|
||||||
|
.sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 72px;
|
||||||
|
background: var(--color-canvas-subtle);
|
||||||
|
border: 1px solid var(--color-border-default);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.sidebar h3 {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-fg-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.sidebar ul { list-style: none; }
|
||||||
|
.sidebar ul li { margin: 4px 0; }
|
||||||
|
.sidebar ul li a {
|
||||||
|
color: var(--color-link);
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
padding: 3px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.sidebar ul li a:hover { color: var(--color-accent-emphasis); text-decoration: underline; }
|
||||||
|
.sidebar-meta {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--color-border-default);
|
||||||
|
}
|
||||||
|
.sidebar-meta p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-fg-muted);
|
||||||
|
margin: 4px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.sidebar-meta a { color: var(--color-link); text-decoration: none; }
|
||||||
|
.sidebar-meta a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
/* ── Markdown Body ── */
|
||||||
|
.markdown-body h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: 700;
|
||||||
|
border-bottom: 1px solid var(--color-border-muted);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-top: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.markdown-body h1 .turtle-badge {
|
||||||
|
font-size: 0.6em;
|
||||||
|
background: var(--color-accent-subtle);
|
||||||
|
color: var(--color-accent-emphasis);
|
||||||
|
border: 1px solid #82e0a0;
|
||||||
|
border-radius: 2em;
|
||||||
|
padding: 2px 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.markdown-body h2 {
|
||||||
|
font-size: 1.4em;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px solid var(--color-border-muted);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin-top: 36px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
scroll-margin-top: 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.markdown-body h2 .anchor {
|
||||||
|
color: var(--color-turtle);
|
||||||
|
font-size: 0.8em;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.markdown-body h2:hover .anchor { opacity: 1; }
|
||||||
|
.markdown-body h3 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.markdown-body p { margin-bottom: 16px; }
|
||||||
|
.markdown-body ul, .markdown-body ol {
|
||||||
|
padding-left: 2em;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.markdown-body li { margin: 4px 0; }
|
||||||
|
.markdown-body strong { font-weight: 600; }
|
||||||
|
.markdown-body a { color: var(--color-link); text-decoration: none; }
|
||||||
|
.markdown-body a:hover { text-decoration: underline; }
|
||||||
|
.markdown-body hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-border-muted);
|
||||||
|
margin: 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Code ── */
|
||||||
|
.markdown-body code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 85%;
|
||||||
|
background: var(--color-canvas-subtle);
|
||||||
|
border: 1px solid var(--color-border-default);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
color: #cf222e;
|
||||||
|
}
|
||||||
|
.markdown-body pre {
|
||||||
|
background: #f6f8fa;
|
||||||
|
border: 1px solid var(--color-border-default);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.markdown-body pre code {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--color-fg-default);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.copy-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
background: var(--color-canvas-default);
|
||||||
|
border: 1px solid var(--color-border-default);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-fg-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: var(--color-accent-subtle);
|
||||||
|
color: var(--color-accent-emphasis);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
.copy-btn.copied {
|
||||||
|
background: var(--color-accent-subtle);
|
||||||
|
color: var(--color-accent-emphasis);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Callouts ── */
|
||||||
|
.callout {
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-left: 4px solid;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.callout-note {
|
||||||
|
background: #ddf4ff;
|
||||||
|
border-color: #54aeff;
|
||||||
|
color: #0550ae;
|
||||||
|
}
|
||||||
|
.callout-warning {
|
||||||
|
background: var(--color-warning-subtle);
|
||||||
|
border-color: var(--color-warning-border);
|
||||||
|
color: #633c01;
|
||||||
|
}
|
||||||
|
.callout-danger {
|
||||||
|
background: var(--color-danger-subtle);
|
||||||
|
border-color: var(--color-danger-border);
|
||||||
|
color: #82071e;
|
||||||
|
}
|
||||||
|
.callout-success {
|
||||||
|
background: var(--color-accent-subtle);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
color: var(--color-accent-emphasis);
|
||||||
|
}
|
||||||
|
.callout strong { display: block; margin-bottom: 4px; font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
|
|
||||||
|
/* ── Steps ── */
|
||||||
|
.steps { counter-reset: step; margin-bottom: 20px; }
|
||||||
|
.step {
|
||||||
|
counter-increment: step;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.step-num {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-turtle);
|
||||||
|
color: white;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.step-body { flex: 1; }
|
||||||
|
.step-body p { margin-bottom: 8px; }
|
||||||
|
|
||||||
|
/* ── Disclaimer ── */
|
||||||
|
.disclaimer-box {
|
||||||
|
background: var(--color-danger-subtle);
|
||||||
|
border: 1px solid var(--color-danger-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 20px 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.disclaimer-box h3 {
|
||||||
|
color: #82071e;
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.disclaimer-box p, .disclaimer-box li {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #57071e;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Footer ── */
|
||||||
|
.gh-footer {
|
||||||
|
background: var(--color-canvas-subtle);
|
||||||
|
border-top: 1px solid var(--color-border-default);
|
||||||
|
padding: 24px 32px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-fg-muted);
|
||||||
|
margin-top: 48px;
|
||||||
|
}
|
||||||
|
.gh-footer a { color: var(--color-link); text-decoration: none; }
|
||||||
|
.gh-footer a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
/* ── Responsive ── */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.layout { grid-template-columns: 1fr; }
|
||||||
|
.sidebar { position: static; }
|
||||||
|
.gh-header-inner, .layout { padding: 16px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="gh-header">
|
||||||
|
<div class="gh-header-inner">
|
||||||
|
<span class="gh-logo">🐢</span>
|
||||||
|
<span class="gh-repo"><span>H. Linz /</span> GeoTurtl</span>
|
||||||
|
<span class="gh-badge">v0.9.7-beta</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="layout">
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="markdown-body">
|
||||||
|
|
||||||
|
<h1>🐢 GeoTurtl <span class="turtle-badge">v0.9.7-beta</span></h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>GeoTurtl</strong> ist eine kleine, unauffällige App für deinen Mac.
|
||||||
|
Sie sitzt als Schildkröten-Symbol 🐢 in der Menüleiste und zeigt dir jederzeit
|
||||||
|
auf Knopfdruck, wo du gerade bist – und wie deine Verbindung ins Internet aussieht.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="callout callout-note">
|
||||||
|
<strong>📋 Beta-Hinweis</strong>
|
||||||
|
Dies ist eine Beta-Version (v0.9.0). Sie ist funktionsfähig, kann aber noch
|
||||||
|
kleinere Fehler enthalten. Rückmeldungen sind herzlich willkommen!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Was ist GeoTurtl -->
|
||||||
|
<h2 id="was-ist">Was ist GeoTurtl und was macht es?</h2>
|
||||||
|
|
||||||
|
<p>GeoTurtl zeigt dir zwei Dinge auf einen Blick:</p>
|
||||||
|
|
||||||
|
<h3>📍 Deinen Standort</h3>
|
||||||
|
<p>
|
||||||
|
GeoTurtl nutzt die eingebaute Ortungsfunktion deines Macs – dieselbe Technik,
|
||||||
|
die auch Apple Maps verwendet. Es zeigt dir:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Deine aktuelle <strong>Adresse</strong></li>
|
||||||
|
<li><strong>Koordinaten</strong> (Breiten- und Längengrad)</li>
|
||||||
|
<li><strong>Höhe</strong> über dem Meeresspiegel</li>
|
||||||
|
<li><strong>Genauigkeit</strong> der Ortung</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Mit einem Klick öffnest du deinen Standort direkt in
|
||||||
|
Google Maps, Apple Maps oder OpenStreetMap.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>🌐 Deine IP-Adressen</h3>
|
||||||
|
<p>
|
||||||
|
GeoTurtl ermittelt deine öffentliche IPv4- und (falls vorhanden) IPv6-Adresse –
|
||||||
|
also die Adresse, unter der du im Internet sichtbar bist. Zusätzlich siehst du
|
||||||
|
den ungefähren Standort dieser Adresse und deinen Internetanbieter.
|
||||||
|
Per Klick kannst du beide Adressen direkt in die Zwischenablage kopieren.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="callout callout-success">
|
||||||
|
<strong>⏱ Auto-Update</strong>
|
||||||
|
GeoTurtl aktualisiert sich automatisch – du kannst selbst einstellen,
|
||||||
|
wie oft: alle 1, 5 oder 15 Minuten, oder manuell auf Knopfdruck.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Voraussetzungen -->
|
||||||
|
<h2 id="voraussetzungen">Welche Installations- und Systemvoraussetzungen gibt es?</h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Einen <strong>Mac mit macOS</strong> (ab macOS Ventura, optimiert für macOS Tahoe)</li>
|
||||||
|
<li><strong>Python 3</strong> – auf modernen Macs in der Regel bereits vorinstalliert</li>
|
||||||
|
<li>Eine aktive <strong>Internetverbindung</strong> für die Installation</li>
|
||||||
|
<li><strong>Aktivierte Ortungsdienste</strong> auf deinem Mac</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Die Installation erledigt alles Weitere automatisch für dich.</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Sicherheit -->
|
||||||
|
<h2 id="sicherheit">Ist die Software sicher?</h2>
|
||||||
|
|
||||||
|
<p>Ja. GeoTurtl ist quelloffen (MIT-Lizenz) – du kannst den kompletten
|
||||||
|
Programmcode jederzeit selbst einsehen. Das Programm:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>sendet <strong>keine</strong> Daten an Dritte außer den öffentlichen Ortungs- und IP-Diensten (ipinfo.io, ipify.org)</li>
|
||||||
|
<li>speichert <strong>keine</strong> persönlichen Daten dauerhaft</li>
|
||||||
|
<li>läuft ausschließlich <strong>lokal</strong> auf deinem Mac</li>
|
||||||
|
<li>ist <strong>werbefrei</strong> und kostenlos</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Die Ortungsdienste werden nur auf ausdrückliche Anfrage genutzt –
|
||||||
|
macOS fragt beim ersten Start nach deiner Erlaubnis.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Installation -->
|
||||||
|
<h2 id="installation">Wie installiere ich GeoTurtl?</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Du benötigst nur die Datei <code>GeoTurtl_install_v0.9.7-beta.sh</code>
|
||||||
|
aus dem heruntergeladenen ZIP-Paket. Öffne das Terminal
|
||||||
|
(Programme → Dienstprogramme → Terminal) und führe diese zwei Befehle aus:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="steps">
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">1</div>
|
||||||
|
<div class="step-body">
|
||||||
|
<p><strong>Datei ausführbar machen</strong></p>
|
||||||
|
<pre><code>chmod +x ~/Downloads/GeoTurtl_v0.9.7-beta/GeoTurtl_install_v0.9.7-beta.sh</code><button class="copy-btn" onclick="copyCode(this)">copy</button></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">2</div>
|
||||||
|
<div class="step-body">
|
||||||
|
<p><strong>Installation starten</strong></p>
|
||||||
|
<pre><code>~/Downloads/GeoTurtl_v0.9.7-beta/GeoTurtl_install_v0.9.7-beta.sh</code><button class="copy-btn" onclick="copyCode(this)">copy</button></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-num">3</div>
|
||||||
|
<div class="step-body">
|
||||||
|
<p><strong>Ortungsdienste erlauben</strong></p>
|
||||||
|
<p>Beim ersten Start erscheint eine macOS-Meldung. Klicke auf
|
||||||
|
<strong>„Beim Verwenden erlauben"</strong>. Falls die Meldung ausbleibt:</p>
|
||||||
|
<p>Systemeinstellungen → Datenschutz & Sicherheit → Ortungsdienste → Terminal → „Beim Verwenden"</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="callout callout-note">
|
||||||
|
<strong>ℹ️ Was passiert bei der Installation?</strong>
|
||||||
|
Das Script installiert die benötigten Python-Pakete, legt den Programmordner an,
|
||||||
|
richtet den automatischen Start beim Mac-Login ein und startet GeoTurtl sofort.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Starten & Stoppen -->
|
||||||
|
<h2 id="starten-stoppen">Wie starte und stoppe ich GeoTurtl?</h2>
|
||||||
|
|
||||||
|
<p>Nach der Installation erscheint das 🐢-Symbol automatisch in der Menüleiste.
|
||||||
|
Bei Bedarf kannst du GeoTurtl auch manuell über das Terminal steuern:</p>
|
||||||
|
|
||||||
|
<h3>▶ Starten</h3>
|
||||||
|
<pre><code>geoturtl_start</code><button class="copy-btn" onclick="copyCode(this)">copy</button></pre>
|
||||||
|
|
||||||
|
<h3>■ Stoppen</h3>
|
||||||
|
<pre><code>geoturtl_stop</code><button class="copy-btn" onclick="copyCode(this)">copy</button></pre>
|
||||||
|
|
||||||
|
<div class="callout callout-success">
|
||||||
|
<strong>🔄 Autostart</strong>
|
||||||
|
GeoTurtl startet nach der Installation automatisch bei jedem Mac-Login –
|
||||||
|
du musst nichts weiter tun.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Log -->
|
||||||
|
<h2 id="log">Wo finde ich die Log-Dateien?</h2>
|
||||||
|
|
||||||
|
<p>GeoTurtl schreibt alle wichtigen Ereignisse in eine Log-Datei:</p>
|
||||||
|
<pre><code>~/Library/Logs/GeoTurtl.log</code><button class="copy-btn" onclick="copyCode(this)">copy</button></pre>
|
||||||
|
|
||||||
|
<h3>Log live verfolgen</h3>
|
||||||
|
<pre><code>tail -f ~/Library/Logs/GeoTurtl.log</code><button class="copy-btn" onclick="copyCode(this)">copy</button></pre>
|
||||||
|
|
||||||
|
<h3>Letzte 50 Zeilen anzeigen</h3>
|
||||||
|
<pre><code>tail -50 ~/Library/Logs/GeoTurtl.log</code><button class="copy-btn" onclick="copyCode(this)">copy</button></pre>
|
||||||
|
|
||||||
|
<h3>Log im Texteditor öffnen</h3>
|
||||||
|
<pre><code>open ~/Library/Logs/GeoTurtl.log</code><button class="copy-btn" onclick="copyCode(this)">copy</button></pre>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Deinstallation -->
|
||||||
|
<h2 id="deinstallation">Wie deinstalliere ich GeoTurtl restlos?</h2>
|
||||||
|
|
||||||
|
<p>Alles mit einem einzigen Befehl entfernen – einfach ins Terminal kopieren:</p>
|
||||||
|
|
||||||
|
<pre><code>geoturtl_stop 2>/dev/null; \
|
||||||
|
launchctl unload ~/Library/LaunchAgents/com.user.geoturtl.plist 2>/dev/null; \
|
||||||
|
rm -f ~/Library/LaunchAgents/com.user.geoturtl.plist; \
|
||||||
|
rm -rf /Applications/Scripte/python/GeoTurtl; \
|
||||||
|
rm -f ~/Library/Logs/GeoTurtl.log; \
|
||||||
|
rm -f ~/Library/Application\ Support/GeoTurtl.pid; \
|
||||||
|
sed -i '' '/geoturtl/d' ~/.zshrc; \
|
||||||
|
source ~/.zshrc; \
|
||||||
|
echo "✓ GeoTurtl vollständig entfernt."</code><button class="copy-btn" onclick="copyCode(this)">copy</button></pre>
|
||||||
|
|
||||||
|
<p>Dieser Befehl:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Beendet GeoTurtl, falls es gerade läuft</li>
|
||||||
|
<li>Entfernt den Autostart beim Login</li>
|
||||||
|
<li>Löscht alle Programmdateien</li>
|
||||||
|
<li>Löscht die Log-Datei</li>
|
||||||
|
<li>Entfernt die Terminal-Kürzel</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Haftungsausschluss -->
|
||||||
|
<h2 id="haftung">Haftungsausschluss</h2>
|
||||||
|
|
||||||
|
<div class="disclaimer-box">
|
||||||
|
<h3>⚠️ Wichtiger Hinweis – Bitte vor der Installation lesen</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
GeoTurtl wurde von <strong>H. Linz</strong> nach bestem Wissen und Gewissen
|
||||||
|
entwickelt und kostenlos zur Verfügung gestellt. Dennoch gilt:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul style="margin: 12px 0 12px 20px;">
|
||||||
|
<li style="margin-bottom: 6px;">
|
||||||
|
Die Software wird <strong>„so wie sie ist" (as is)</strong> bereitgestellt –
|
||||||
|
ohne Garantie auf Fehlerfreiheit, ununterbrochenen Betrieb oder
|
||||||
|
Eignung für einen bestimmten Zweck.
|
||||||
|
</li>
|
||||||
|
<li style="margin-bottom: 6px;">
|
||||||
|
<strong>H. Linz übernimmt keine Haftung</strong> für direkte oder indirekte
|
||||||
|
Schäden, Datenverluste oder sonstige Beeinträchtigungen, die durch die
|
||||||
|
Installation, Nutzung oder Deinstallation dieser Software entstehen könnten.
|
||||||
|
</li>
|
||||||
|
<li style="margin-bottom: 6px;">
|
||||||
|
Die Nutzung von Drittdiensten wie <strong>ipinfo.io</strong> und
|
||||||
|
<strong>ipify.org</strong> zur IP-Ermittlung unterliegt den jeweiligen
|
||||||
|
Nutzungsbedingungen dieser Anbieter und liegt außerhalb der Verantwortung
|
||||||
|
des Autors.
|
||||||
|
</li>
|
||||||
|
<li style="margin-bottom: 6px;">
|
||||||
|
Die Ortungsfunktion verwendet ausschließlich die von Apple bereitgestellte
|
||||||
|
<strong>CoreLocation-API</strong>. Die Genauigkeit der Standortangaben
|
||||||
|
hängt von der Verfügbarkeit von WLAN-Signalen ab und kann variieren.
|
||||||
|
</li>
|
||||||
|
<li style="margin-bottom: 6px;">
|
||||||
|
<strong>Jede Person installiert und nutzt diese Software auf eigene
|
||||||
|
Gefahr und eigene Verantwortung.</strong>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Durch die Installation und Nutzung von GeoTurtl erkennst du diese
|
||||||
|
Bedingungen an.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p style="margin-top: 12px; font-size: 12px; color: #82071e;">
|
||||||
|
Diese Software steht unter der <strong>MIT-Lizenz</strong>.
|
||||||
|
Der vollständige Lizenztext ist im Programmordner enthalten.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Kontakt -->
|
||||||
|
<h2 id="kontakt">Fragen, Anregungen oder konstruktive Kritik?</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Du schickst mir eine Nachricht an
|
||||||
|
<a href="mailto:linz.on.tour@gmail.com?subject=GeoTurtl%20v0.9.7-beta">linz.on.tour@gmail.com</a>.
|
||||||
|
Ich beantworte sie dann, so schnell es mir möglich ist.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p><strong>H. Linz</strong></p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<h3>Inhalt</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#was-ist">Was ist GeoTurtl?</a></li>
|
||||||
|
<li><a href="#voraussetzungen">Voraussetzungen</a></li>
|
||||||
|
<li><a href="#sicherheit">Sicherheit</a></li>
|
||||||
|
<li><a href="#installation">Installation</a></li>
|
||||||
|
<li><a href="#starten-stoppen">Starten & Stoppen</a></li>
|
||||||
|
<li><a href="#log">Log-Dateien</a></li>
|
||||||
|
<li><a href="#deinstallation">Deinstallation</a></li>
|
||||||
|
<li><a href="#haftung">Haftungsausschluss</a></li>
|
||||||
|
<li><a href="#kontakt">Kontakt</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="sidebar-meta">
|
||||||
|
<p>🐢 GeoTurtl</p>
|
||||||
|
<p>📦 v0.9.7-beta</p>
|
||||||
|
<p>📅 Juni 2026</p>
|
||||||
|
<p>👤 H. Linz</p>
|
||||||
|
<p>⚖️ MIT License</p>
|
||||||
|
<p>✉️ <a href="mailto:linz.on.tour@gmail.com?subject=GeoTurtl%20v0.9.7-beta">linz.on.tour@gmail.com</a></p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="gh-footer">
|
||||||
|
🐢 GeoTurtl v0.9.7-beta ·
|
||||||
|
MIT License ·
|
||||||
|
© 2026 H. Linz ·
|
||||||
|
<a href="mailto:linz.on.tour@gmail.com?subject=GeoTurtl%20v0.9.7-beta">linz.on.tour@gmail.com</a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyCode(btn) {
|
||||||
|
const code = btn.previousElementSibling || btn.parentElement.querySelector('code');
|
||||||
|
navigator.clipboard.writeText(code.innerText.trim()).then(() => {
|
||||||
|
btn.textContent = '✓ copied';
|
||||||
|
btn.classList.add('copied');
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = 'copy';
|
||||||
|
btn.classList.remove('copied');
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,2 +1,195 @@
|
|||||||
# GeoTurtle
|
---
|
||||||
|
title: "GeoTurtl README"
|
||||||
|
version: "0.9.7-beta"
|
||||||
|
date: 2026-06-07
|
||||||
|
author: "H. Linz"
|
||||||
|
contact: "linz.on.tour@gmail.com"
|
||||||
|
license: "MIT"
|
||||||
|
tags: [geoturtl, ip, geolocation, standort, macOS, menubar]
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🐢 GeoTurtl v0.9.7-beta
|
||||||
|
|
||||||
|
**GeoTurtl** ist eine kleine, unauffällige App für deinen Mac. Sie sitzt als
|
||||||
|
Schildkröten-Symbol 🐢 in der Menüleiste und zeigt dir jederzeit auf Knopfdruck,
|
||||||
|
wo du gerade bist – und wie deine Verbindung ins Internet aussieht.
|
||||||
|
|
||||||
|
> **Beta-Hinweis:** Dies ist eine Beta-Version (v0.9.6). Sie ist funktionsfähig,
|
||||||
|
> kann aber noch kleinere Fehler enthalten. Rückmeldungen sind willkommen!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inhaltsverzeichnis
|
||||||
|
|
||||||
|
- [Was ist GeoTurtl und was macht es?](#was-ist-geoturtl-und-was-macht-es)
|
||||||
|
- [Welche Installations- und Systemvoraussetzungen gibt es?](#welche-installations--und-systemvoraussetzungen-gibt-es)
|
||||||
|
- [Ist die Software sicher?](#ist-die-software-sicher)
|
||||||
|
- [Wie installiere ich GeoTurtl?](#wie-installiere-ich-geoturtl)
|
||||||
|
- [Wie starte und stoppe ich GeoTurtl?](#wie-starte-und-stoppe-ich-geoturtl)
|
||||||
|
- [Wo finde ich die Log-Dateien und wie rufe ich sie auf?](#wo-finde-ich-die-log-dateien-und-wie-rufe-ich-sie-auf)
|
||||||
|
- [Wie deinstalliere ich GeoTurtl restlos?](#wie-deinstalliere-ich-geoturtl-restlos)
|
||||||
|
- [Haftungsausschluss](#haftungsausschluss)
|
||||||
|
- [Was mache ich, wenn ich Fragen, Anregungen oder konstruktive Kritik habe?](#was-mache-ich-wenn-ich-fragen-anregungen-oder-konstruktive-kritik-habe)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Was ist GeoTurtl und was macht es?
|
||||||
|
|
||||||
|
GeoTurtl zeigt dir zwei Dinge auf einen Blick:
|
||||||
|
|
||||||
|
**Deinen Standort**
|
||||||
|
GeoTurtl nutzt die eingebaute Ortungsfunktion deines Macs (dieselbe Technik,
|
||||||
|
die auch Apple Maps verwendet) und zeigt dir deine aktuelle Adresse, die genauen
|
||||||
|
Koordinaten (Breiten- und Längengrad), deine Höhe über dem Meeresspiegel sowie
|
||||||
|
die Genauigkeit der Ortung an. Mit einem Klick öffnest du deinen Standort direkt
|
||||||
|
in Google Maps, Apple Maps oder OpenStreetMap.
|
||||||
|
|
||||||
|
**Deine IP-Adressen**
|
||||||
|
GeoTurtl ermittelt deine öffentliche IPv4- und (falls vorhanden) IPv6-Adresse –
|
||||||
|
also die Adresse, unter der du im Internet sichtbar bist. Dazu zeigt es dir
|
||||||
|
den ungefähren Standort dieser Adresse und deinen Internetanbieter an.
|
||||||
|
Per Klick kannst du beide Adressen direkt in die Zwischenablage kopieren.
|
||||||
|
|
||||||
|
GeoTurtl aktualisiert diese Informationen automatisch – du kannst selbst
|
||||||
|
einstellen, wie oft (alle 1, 5 oder 15 Minuten, oder gar nicht).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Welche Installations- und Systemvoraussetzungen gibt es?
|
||||||
|
|
||||||
|
- Einen **Mac mit macOS** (ab macOS Ventura, optimiert für macOS Tahoe)
|
||||||
|
- **Python 3** – auf modernen Macs in der Regel bereits vorinstalliert
|
||||||
|
- Eine aktive **Internetverbindung** für die Installation
|
||||||
|
- **Aktivierte Ortungsdienste** auf deinem Mac
|
||||||
|
|
||||||
|
Die Installation erledigt alles Weitere automatisch für dich.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ist die Software sicher?
|
||||||
|
|
||||||
|
Ja. GeoTurtl ist quelloffen (MIT-Lizenz) – du kannst den kompletten
|
||||||
|
Programmcode jederzeit selbst einsehen. Das Programm:
|
||||||
|
|
||||||
|
- sendet **keine** Daten an Dritte außer den öffentlichen Ortungs- und
|
||||||
|
IP-Diensten (ipinfo.io, ipify.org)
|
||||||
|
- speichert **keine** persönlichen Daten dauerhaft
|
||||||
|
- läuft ausschließlich **lokal** auf deinem Mac
|
||||||
|
- ist **werbefrei** und kostenlos
|
||||||
|
|
||||||
|
Die Ortungsdienste werden nur auf ausdrückliche Anfrage genutzt –
|
||||||
|
macOS fragt beim ersten Start nach deiner Erlaubnis.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wie installiere ich GeoTurtl?
|
||||||
|
|
||||||
|
Öffne das Terminal (Programme → Dienstprogramme → Terminal) und führe
|
||||||
|
diese zwei Befehle aus:
|
||||||
|
|
||||||
|
**Schritt 1** – Datei ausführbar machen:
|
||||||
|
```
|
||||||
|
chmod +x ~/Downloads/GeoTurtl_v0.9.7-beta/GeoTurtl_install_v0.9.7-beta.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schritt 2** – Installation starten:
|
||||||
|
```
|
||||||
|
~/Downloads/GeoTurtl_v0.9.7-beta/GeoTurtl_install_v0.9.7-beta.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schritt 3** – Ortungsdienste erlauben:
|
||||||
|
Beim ersten Start erscheint eine macOS-Meldung. Klicke auf
|
||||||
|
**„Beim Verwenden erlauben"**. Falls die Meldung ausbleibt:
|
||||||
|
> Systemeinstellungen → Datenschutz & Sicherheit → Ortungsdienste
|
||||||
|
> → Terminal → „Beim Verwenden"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wie starte und stoppe ich GeoTurtl?
|
||||||
|
|
||||||
|
**Starten** – im Terminal:
|
||||||
|
```
|
||||||
|
geoturtl_start
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stoppen** – im Terminal:
|
||||||
|
```
|
||||||
|
geoturtl_stop
|
||||||
|
```
|
||||||
|
|
||||||
|
GeoTurtl startet nach der Installation **automatisch** bei jedem Mac-Login.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wo finde ich die Log-Dateien und wie rufe ich sie auf?
|
||||||
|
|
||||||
|
GeoTurtl schreibt alle wichtigen Ereignisse in eine Log-Datei:
|
||||||
|
```
|
||||||
|
~/Library/Logs/GeoTurtl.log
|
||||||
|
```
|
||||||
|
|
||||||
|
**Log live verfolgen:**
|
||||||
|
```
|
||||||
|
tail -f ~/Library/Logs/GeoTurtl.log
|
||||||
|
```
|
||||||
|
|
||||||
|
**Letzte 50 Zeilen anzeigen:**
|
||||||
|
```
|
||||||
|
tail -50 ~/Library/Logs/GeoTurtl.log
|
||||||
|
```
|
||||||
|
|
||||||
|
**Log im Texteditor öffnen:**
|
||||||
|
```
|
||||||
|
open ~/Library/Logs/GeoTurtl.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wie deinstalliere ich GeoTurtl restlos?
|
||||||
|
|
||||||
|
Alles auf einmal entfernen – diesen Befehl ins Terminal kopieren:
|
||||||
|
|
||||||
|
```
|
||||||
|
geoturtl_stop 2>/dev/null; \
|
||||||
|
launchctl unload ~/Library/LaunchAgents/com.user.geoturtl.plist 2>/dev/null; \
|
||||||
|
rm -f ~/Library/LaunchAgents/com.user.geoturtl.plist; \
|
||||||
|
rm -rf /Applications/Scripte/python/GeoTurtl; \
|
||||||
|
rm -f ~/Library/Logs/GeoTurtl.log; \
|
||||||
|
rm -f ~/Library/Application\ Support/GeoTurtl.pid; \
|
||||||
|
sed -i '' '/geoturtl/d' ~/.zshrc; \
|
||||||
|
source ~/.zshrc; \
|
||||||
|
echo "✓ GeoTurtl vollständig entfernt."
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Haftungsausschluss
|
||||||
|
|
||||||
|
GeoTurtl wurde von **H. Linz** nach bestem Wissen und Gewissen entwickelt
|
||||||
|
und kostenlos zur Verfügung gestellt. Dennoch gilt:
|
||||||
|
|
||||||
|
- Die Software wird **„so wie sie ist" (as is)** bereitgestellt – ohne Garantie
|
||||||
|
auf Fehlerfreiheit, ununterbrochenen Betrieb oder Eignung für einen bestimmten Zweck.
|
||||||
|
- **H. Linz übernimmt keine Haftung** für direkte oder indirekte Schäden,
|
||||||
|
Datenverluste oder sonstige Beeinträchtigungen durch Installation, Nutzung
|
||||||
|
oder Deinstallation dieser Software.
|
||||||
|
- Die Nutzung von Drittdiensten (ipinfo.io, ipify.org) unterliegt den jeweiligen
|
||||||
|
Nutzungsbedingungen dieser Anbieter und liegt außerhalb der Verantwortung des Autors.
|
||||||
|
- **Jede Person installiert und nutzt diese Software auf eigene Gefahr.**
|
||||||
|
- Durch die Installation erkennst du diese Bedingungen an.
|
||||||
|
|
||||||
|
Diese Software steht unter der **MIT-Lizenz**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Was mache ich, wenn ich Fragen, Anregungen oder konstruktive Kritik habe?
|
||||||
|
|
||||||
|
Du schickst mir eine Nachricht an **linz.on.tour@gmail.com**. Ich beantworte
|
||||||
|
sie dann, so schnell es mir möglich ist.
|
||||||
|
|
||||||
|
**H. Linz**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*GeoTurtl v0.9.7-beta · MIT License · © 2026 H. Linz*
|
||||||
|
|||||||
Reference in New Issue
Block a user