update at 2026-03-18 13:35:19

This commit is contained in:
douboer@gmail.com
2026-03-18 13:35:19 +08:00
parent 192eb1b8d1
commit f9d715157f
72 changed files with 4035 additions and 972 deletions

View File

@@ -1,166 +0,0 @@
#!/usr/bin/env swift
import AppKit
import Foundation
struct HandConfig {
let sourcePath: String
let outputDirectoryName: String
let frameCount: Int
let sourceWidth: CGFloat
let sourceHeight: CGFloat
let pivotX: CGFloat
let pivotY: CGFloat
let digits: Int
let angleStep: CGFloat
}
enum AssetError: Error, CustomStringConvertible {
case invalidImage(String)
case pngEncodingFailed(String)
var description: String {
switch self {
case let .invalidImage(path):
return "无法读取图片:\(path)"
case let .pngEncodingFailed(path):
return "无法编码 PNG\(path)"
}
}
}
let fileManager = FileManager.default
let workingDirectory = URL(fileURLWithPath: fileManager.currentDirectoryPath, isDirectory: true)
let repoRoot = CommandLine.arguments.count > 1
? URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true)
: workingDirectory
let outputRoot = CommandLine.arguments.count > 2
? URL(fileURLWithPath: CommandLine.arguments[2], isDirectory: true)
: repoRoot.appendingPathComponent("assets/kindle-clock", isDirectory: true)
let faceSourceURL = repoRoot.appendingPathComponent("assets/clock-face.png")
let targetSize = NSSize(width: 220, height: 220)
let faceSourceSize = NSSize(width: 431, height: 431)
let scale = targetSize.width / faceSourceSize.width
let handConfigs = [
HandConfig(
sourcePath: "assets/hour-hand.png",
outputDirectoryName: "hour-hand",
frameCount: 720,
sourceWidth: 32,
sourceHeight: 205,
pivotX: 13,
pivotY: 138,
digits: 3,
angleStep: 0.5
),
HandConfig(
sourcePath: "assets/minite-hand.png",
outputDirectoryName: "minute-hand",
frameCount: 60,
sourceWidth: 32,
sourceHeight: 288,
pivotX: 15,
pivotY: 203,
digits: 2,
angleStep: 6
),
]
func loadImage(at url: URL) throws -> NSImage {
guard let image = NSImage(contentsOf: url) else {
throw AssetError.invalidImage(url.path)
}
return image
}
func savePNG(_ image: NSImage, to url: URL) throws {
guard
let tiffRepresentation = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: tiffRepresentation),
let pngData = bitmap.representation(using: .png, properties: [:])
else {
throw AssetError.pngEncodingFailed(url.path)
}
try fileManager.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try pngData.write(to: url)
}
func renderImage(size: NSSize, draw: () -> Void) -> NSImage {
let image = NSImage(size: size)
image.lockFocusFlipped(true)
NSColor.clear.setFill()
NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill()
draw()
image.unlockFocus()
return image
}
func renderFace() throws {
let faceImage = try loadImage(at: faceSourceURL)
let renderedFace = renderImage(size: targetSize) {
faceImage.draw(
in: NSRect(origin: .zero, size: targetSize),
from: NSRect(origin: .zero, size: faceImage.size),
operation: .sourceOver,
fraction: 1
)
}
try savePNG(renderedFace, to: outputRoot.appendingPathComponent("clock-face.png"))
}
func renderHandFrames(config: HandConfig) throws {
let sourceURL = repoRoot.appendingPathComponent(config.sourcePath)
let handImage = try loadImage(at: sourceURL)
let outputDirectory = outputRoot.appendingPathComponent(config.outputDirectoryName, isDirectory: true)
let scaledWidth = config.sourceWidth * scale
let scaledHeight = config.sourceHeight * scale
let scaledPivotX = config.pivotX * scale
let scaledPivotY = config.pivotY * scale
try fileManager.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
for frameIndex in 0..<config.frameCount {
let angle = CGFloat(frameIndex) * config.angleStep
let renderedFrame = renderImage(size: targetSize) {
guard let context = NSGraphicsContext.current?.cgContext else {
return
}
context.translateBy(x: targetSize.width / 2, y: targetSize.height / 2)
context.rotate(by: angle * .pi / 180)
context.translateBy(x: -scaledPivotX, y: -scaledPivotY)
handImage.draw(
in: NSRect(x: 0, y: 0, width: scaledWidth, height: scaledHeight),
from: NSRect(origin: .zero, size: handImage.size),
operation: .sourceOver,
fraction: 1
)
}
let filename = String(format: "%0\(config.digits)d.png", frameIndex)
try savePNG(renderedFrame, to: outputDirectory.appendingPathComponent(filename))
}
}
do {
try fileManager.createDirectory(at: outputRoot, withIntermediateDirectories: true)
try renderFace()
for config in handConfigs {
try renderHandFrames(config: config)
}
print("Generated Kindle clock assets at \(outputRoot.path)")
} catch {
fputs("\(error)\n", stderr)
exit(1)
}

View File

@@ -1,63 +0,0 @@
#!/bin/sh
set -eu
# 强制清理残留 SSH 进程,然后在 22 端口拉起一份 usbnet 自带的 OpenSSH。
# 这份 sshd 会优先读取 /mnt/us/usbnet/etc/dot.ssh/authorized_keys。
TS="$(date +%Y%m%d-%H%M%S 2>/dev/null || echo now)"
OUT_DIR="/mnt/us/ssh-debug/${TS}"
LOG_FILE="${OUT_DIR}/force-openssh-22.log"
PID_FILE="/mnt/us/usbnet/run/sshd-force-22.pid"
SOURCE_KEYS="/mnt/us/usbnet/etc/authorized_keys"
TARGET_KEYS="/mnt/us/usbnet/etc/dot.ssh/authorized_keys"
mkdir -p "${OUT_DIR}" /mnt/us/usbnet/run /mnt/us/usbnet/etc/dot.ssh
exec >"${LOG_FILE}" 2>&1
echo "=== FORCE OPENSSH 22 ==="
date 2>/dev/null || true
id 2>/dev/null || true
if [ -f "${SOURCE_KEYS}" ]; then
cp "${SOURCE_KEYS}" "${TARGET_KEYS}"
chmod 600 "${TARGET_KEYS}" 2>/dev/null || true
fi
chmod 755 /mnt/us/usbnet/etc/dot.ssh 2>/dev/null || true
killall sshd 2>/dev/null || true
killall dropbear 2>/dev/null || true
killall dropbearmulti 2>/dev/null || true
sleep 1
rm -f "${PID_FILE}" 2>/dev/null || true
iptables -I INPUT 1 -p tcp --dport 22 -j ACCEPT 2>/dev/null || true
(
exec /mnt/us/usbnet/sbin/sshd -D -e \
-f /mnt/us/usbnet/etc/sshd_config \
-o ListenAddress=0.0.0.0 \
-o Port=22 \
-o PidFile="${PID_FILE}" \
-o AuthorizedKeysFile="${TARGET_KEYS}" \
-o PasswordAuthentication=no \
-o KbdInteractiveAuthentication=no \
-o PubkeyAuthentication=yes \
-o PermitRootLogin=yes \
-o HostKey=/mnt/us/usbnet/etc/ssh_host_rsa_key \
-o HostKey=/mnt/us/usbnet/etc/ssh_host_ecdsa_key \
-o HostKey=/mnt/us/usbnet/etc/ssh_host_ed25519_key
) &
LAUNCHER_PID="$!"
echo "${LAUNCHER_PID}" > "${OUT_DIR}/launcher.pid"
sleep 1
echo "launcher pid: ${LAUNCHER_PID}"
echo "pid file: ${PID_FILE}"
if [ -x /mnt/us/usbnet/bin/lsof ]; then
/mnt/us/usbnet/bin/lsof -n -P -iTCP:22 2>/dev/null || true
fi
echo "=== DONE ==="
echo "${OUT_DIR}"

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env sh
set -eu
ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
TMP_DIR="${TMPDIR:-/tmp}/kindle-clock-assets"
KINDLE_TARGET=${1:-kindle}
rm -rf "$TMP_DIR"
/usr/bin/swift "$ROOT_DIR/scripts/generate-kindle-clock-assets.swift" "$ROOT_DIR" "$TMP_DIR"
ssh "$KINDLE_TARGET" 'mkdir -p /mnt/us/dashboard/assets/hour-hand /mnt/us/dashboard/assets/minute-hand'
rsync -av --no-o --no-g --delete "$TMP_DIR"/ "$KINDLE_TARGET":/mnt/us/dashboard/assets/
echo "Clock assets synced to $KINDLE_TARGET:/mnt/us/dashboard/assets"

View File

@@ -88,6 +88,9 @@ sync_dashboard_runtime() {
rsync -av --no-o --no-g \
"$ROOT_DIR/dash/src/start.sh" \
"$ROOT_DIR/dash/src/dash.sh" \
"$ROOT_DIR/dash/src/stop.sh" \
"$ROOT_DIR/dash/src/launch-from-kual.sh" \
"$ROOT_DIR/dash/src/launch-theme-from-kual.sh" \
"$ROOT_DIR/dash/src/switch-theme.sh" \
"$KINDLE_TARGET":/mnt/us/dashboard/
@@ -97,16 +100,24 @@ sync_dashboard_runtime() {
"$ROOT_DIR/dash/src/local/clock-index.sh" \
"$ROOT_DIR/dash/src/local/render-clock.lua" \
"$ROOT_DIR/dash/src/local/render-clock.sh" \
"$ROOT_DIR/dash/src/local/touch-home-service.sh" \
"$ROOT_DIR/dash/src/local/theme-menu-service.sh" \
"$ROOT_DIR/dash/src/local/theme-json.lua" \
"$ROOT_DIR/dash/src/local/theme-sync.sh" \
"$KINDLE_TARGET":/mnt/us/dashboard/local/
ssh "$KINDLE_TARGET" "chmod +x /mnt/us/dashboard/start.sh /mnt/us/dashboard/dash.sh /mnt/us/dashboard/switch-theme.sh /mnt/us/dashboard/local/fetch-dashboard.sh /mnt/us/dashboard/local/clock-index.sh /mnt/us/dashboard/local/render-clock.sh /mnt/us/dashboard/local/theme-menu-service.sh /mnt/us/dashboard/local/theme-sync.sh"
ssh "$KINDLE_TARGET" "chmod +x /mnt/us/dashboard/start.sh /mnt/us/dashboard/dash.sh /mnt/us/dashboard/stop.sh /mnt/us/dashboard/launch-from-kual.sh /mnt/us/dashboard/launch-theme-from-kual.sh /mnt/us/dashboard/switch-theme.sh /mnt/us/dashboard/local/fetch-dashboard.sh /mnt/us/dashboard/local/clock-index.sh /mnt/us/dashboard/local/render-clock.sh /mnt/us/dashboard/local/touch-home-service.sh /mnt/us/dashboard/local/theme-menu-service.sh /mnt/us/dashboard/local/theme-sync.sh"
ssh "$KINDLE_TARGET" "mkdir -p /mnt/us/dashboard/local/state"
ssh "$KINDLE_TARGET" "date '+%s' >/mnt/us/dashboard/local/state/background-updated-at"
}
sync_kual_extension() {
ssh "$KINDLE_TARGET" "mkdir -p /mnt/us/extensions/kindle-dash"
rsync -av --no-o --no-g \
"$ROOT_DIR/dash/KUAL/kindle-dash/" \
"$KINDLE_TARGET":/mnt/us/extensions/kindle-dash/
}
sync_theme_bundle() {
rsync -av --no-o --no-g \
"$ROOT_DIR/calendar/dist/themes.json" \
@@ -175,6 +186,7 @@ update_default_clock_region_env() {
sync_dashboard_runtime
sync_theme_bundle
sync_kual_extension
update_default_clock_region_env
if [ -n "$THEME_FILTER" ]; then