@hamedb89/localghost 0.1.6 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ import Foundation
2
+
3
+ let localghostWidgetAppGroupIdentifier = "group.app.localghost"
4
+ let localghostWidgetSnapshotFileName = "LocalghostWidgetSnapshot.json"
5
+
6
+ struct LocalghostWidgetSnapshot: Codable {
7
+ var generatedAt: String
8
+ var instances: [LocalghostWidgetInstance]
9
+ }
10
+
11
+ struct LocalghostWidgetInstance: Codable, Identifiable {
12
+ var id: String
13
+ var projectName: String
14
+ var cwd: String
15
+ var running: Bool
16
+ var mode: String
17
+ var routes: [LocalghostWidgetRoute]
18
+ }
19
+
20
+ struct LocalghostWidgetRoute: Codable, Identifiable {
21
+ var id: String { "\(host):\(port)" }
22
+ var host: String
23
+ var port: Int
24
+ var listening: Bool
25
+ }
26
+
27
+ enum LocalghostWidgetSharedStore {
28
+ static func snapshotURL() -> URL {
29
+ if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: localghostWidgetAppGroupIdentifier) {
30
+ return appGroupURL.appendingPathComponent(localghostWidgetSnapshotFileName)
31
+ }
32
+
33
+ let fallback = FileManager.default
34
+ .homeDirectoryForCurrentUser
35
+ .appendingPathComponent("Library/Application Support/Localghost", isDirectory: true)
36
+ return fallback.appendingPathComponent(localghostWidgetSnapshotFileName)
37
+ }
38
+
39
+ static func readSnapshot() throws -> LocalghostWidgetSnapshot {
40
+ let data = try Data(contentsOf: snapshotURL())
41
+ return try JSONDecoder().decode(LocalghostWidgetSnapshot.self, from: data)
42
+ }
43
+
44
+ static func writeSnapshot(_ snapshot: LocalghostWidgetSnapshot) throws {
45
+ let url = snapshotURL()
46
+ try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
47
+ let data = try JSONEncoder().encode(snapshot)
48
+ try data.write(to: url, options: [.atomic])
49
+ }
50
+ }
@@ -0,0 +1,176 @@
1
+ import SwiftUI
2
+ import WidgetKit
3
+
4
+ struct LocalghostWidgetEntry: TimelineEntry {
5
+ let date: Date
6
+ let snapshot: LocalghostWidgetSnapshot
7
+ }
8
+
9
+ struct LocalghostWidgetProvider: TimelineProvider {
10
+ func placeholder(in context: Context) -> LocalghostWidgetEntry {
11
+ LocalghostWidgetEntry(date: Date(), snapshot: .placeholder)
12
+ }
13
+
14
+ func getSnapshot(in context: Context, completion: @escaping (LocalghostWidgetEntry) -> Void) {
15
+ completion(LocalghostWidgetEntry(date: Date(), snapshot: loadSnapshot()))
16
+ }
17
+
18
+ func getTimeline(in context: Context, completion: @escaping (Timeline<LocalghostWidgetEntry>) -> Void) {
19
+ let entry = LocalghostWidgetEntry(date: Date(), snapshot: loadSnapshot())
20
+ let nextRefresh = Calendar.current.date(byAdding: .minute, value: 5, to: Date()) ?? Date().addingTimeInterval(300)
21
+ completion(Timeline(entries: [entry], policy: .after(nextRefresh)))
22
+ }
23
+
24
+ private func loadSnapshot() -> LocalghostWidgetSnapshot {
25
+ (try? LocalghostWidgetSharedStore.readSnapshot()) ?? .placeholder
26
+ }
27
+ }
28
+
29
+ struct LocalghostDesktopWidgetView: View {
30
+ let entry: LocalghostWidgetEntry
31
+
32
+ private var instances: [LocalghostWidgetInstance] {
33
+ entry.snapshot.instances
34
+ }
35
+
36
+ private var routes: [LocalghostWidgetRoute] {
37
+ instances.flatMap(\.routes)
38
+ }
39
+
40
+ private var onlineCount: Int {
41
+ routes.filter(\.listening).count
42
+ }
43
+
44
+ var body: some View {
45
+ ZStack {
46
+ RoundedRectangle(cornerRadius: 24, style: .continuous)
47
+ .fill(.ultraThinMaterial)
48
+ .overlay(
49
+ LinearGradient(
50
+ colors: [
51
+ Color(red: 0.68, green: 0.56, blue: 1.0).opacity(0.18),
52
+ Color(red: 0.03, green: 0.04, blue: 0.16).opacity(0.48)
53
+ ],
54
+ startPoint: .topLeading,
55
+ endPoint: .bottomTrailing
56
+ )
57
+ )
58
+ .overlay(
59
+ RoundedRectangle(cornerRadius: 24, style: .continuous)
60
+ .stroke(.white.opacity(0.18), lineWidth: 1)
61
+ )
62
+
63
+ VStack(alignment: .leading, spacing: 14) {
64
+ HStack(spacing: 12) {
65
+ Text(">_")
66
+ .font(.system(size: 24, weight: .bold, design: .monospaced))
67
+ .foregroundStyle(Color(red: 0.68, green: 0.48, blue: 1.0))
68
+
69
+ Text("Localghost")
70
+ .font(.system(size: 24, weight: .bold))
71
+ .foregroundStyle(.white)
72
+ }
73
+
74
+ HStack(spacing: 8) {
75
+ Circle()
76
+ .fill(Color(red: 0.36, green: 0.95, blue: 0.58))
77
+ .frame(width: 8, height: 8)
78
+
79
+ Text("\(onlineCount) \(onlineCount == 1 ? "host" : "hosts") online")
80
+ .font(.system(size: 13, weight: .semibold))
81
+ .foregroundStyle(.white.opacity(0.72))
82
+ }
83
+ .padding(.horizontal, 12)
84
+ .padding(.vertical, 7)
85
+ .background(.white.opacity(0.08), in: Capsule())
86
+
87
+ Divider().overlay(.white.opacity(0.12))
88
+
89
+ if routes.isEmpty {
90
+ emptyState
91
+ } else {
92
+ VStack(spacing: 8) {
93
+ ForEach(routes.prefix(4)) { route in
94
+ routeRow(route)
95
+ }
96
+ }
97
+ }
98
+
99
+ Spacer(minLength: 0)
100
+ }
101
+ .padding(22)
102
+ }
103
+ .containerBackground(.clear, for: .widget)
104
+ }
105
+
106
+ private var emptyState: some View {
107
+ VStack(alignment: .leading, spacing: 8) {
108
+ Text("No hosts online")
109
+ .font(.system(size: 18, weight: .semibold))
110
+ .foregroundStyle(.white)
111
+
112
+ Text("Configured setups will stay here when idle.")
113
+ .font(.system(size: 13, weight: .medium))
114
+ .foregroundStyle(.white.opacity(0.64))
115
+ }
116
+ .frame(maxWidth: .infinity, alignment: .leading)
117
+ .padding(16)
118
+ .background(.white.opacity(0.08), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
119
+ .overlay(
120
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
121
+ .stroke(.white.opacity(0.10), lineWidth: 1)
122
+ )
123
+ }
124
+
125
+ private func routeRow(_ route: LocalghostWidgetRoute) -> some View {
126
+ HStack(spacing: 10) {
127
+ Circle()
128
+ .fill(route.listening ? Color(red: 0.36, green: 0.95, blue: 0.58) : Color(red: 1.0, green: 0.42, blue: 0.50))
129
+ .frame(width: 8, height: 8)
130
+
131
+ Text(route.host)
132
+ .font(.system(size: 13, weight: .semibold))
133
+ .foregroundStyle(.white)
134
+ .lineLimit(1)
135
+
136
+ Spacer()
137
+
138
+ Text(String(route.port))
139
+ .font(.system(size: 13, weight: .medium, design: .monospaced))
140
+ .foregroundStyle(.white.opacity(0.62))
141
+ }
142
+ .padding(.horizontal, 12)
143
+ .padding(.vertical, 9)
144
+ .background(.white.opacity(0.08), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
145
+ .overlay(
146
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
147
+ .stroke(.white.opacity(0.10), lineWidth: 1)
148
+ )
149
+ }
150
+ }
151
+
152
+ @main
153
+ struct LocalghostDesktopWidgetBundle: WidgetBundle {
154
+ var body: some Widget {
155
+ LocalghostDesktopWidget()
156
+ }
157
+ }
158
+
159
+ struct LocalghostDesktopWidget: Widget {
160
+ let kind = "LocalghostDesktopWidget"
161
+
162
+ var body: some WidgetConfiguration {
163
+ StaticConfiguration(kind: kind, provider: LocalghostWidgetProvider()) { entry in
164
+ LocalghostDesktopWidgetView(entry: entry)
165
+ }
166
+ .configurationDisplayName("Localghost")
167
+ .description("Shows Localghost setup and running local hosts.")
168
+ .supportedFamilies([.systemMedium])
169
+ }
170
+ }
171
+
172
+ extension LocalghostWidgetSnapshot {
173
+ static var placeholder: LocalghostWidgetSnapshot {
174
+ LocalghostWidgetSnapshot(generatedAt: ISO8601DateFormatter().string(from: Date()), instances: [])
175
+ }
176
+ }
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.app-sandbox</key>
6
+ <true/>
7
+ <key>com.apple.security.application-groups</key>
8
+ <array>
9
+ <string>group.app.localghost</string>
10
+ </array>
11
+ </dict>
12
+ </plist>
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
5
+ APP_DIR="$ROOT_DIR/dist/LocalghostWidget.app"
6
+ CONTENTS_DIR="$APP_DIR/Contents"
7
+ MACOS_DIR="$CONTENTS_DIR/MacOS"
8
+ RESOURCES_DIR="$CONTENTS_DIR/Resources"
9
+ SOURCE_FILE="$ROOT_DIR/apps/macos-widget/LocalghostWidget.swift"
10
+ SHARED_SOURCE_FILE="$ROOT_DIR/apps/macos-widget/Shared/LocalghostWidgetSnapshot.swift"
11
+ SOURCE_RESOURCES_DIR="$ROOT_DIR/apps/macos-widget/Resources"
12
+ EXECUTABLE="$MACOS_DIR/LocalghostWidget"
13
+ MODULE_CACHE_DIR="${TMPDIR:-/tmp}/localghost-swift-module-cache"
14
+
15
+ rm -rf "$APP_DIR"
16
+ mkdir -p "$MACOS_DIR" "$RESOURCES_DIR"
17
+ mkdir -p "$MODULE_CACHE_DIR"
18
+
19
+ cp "$SOURCE_RESOURCES_DIR/localghost-logo-source.png" "$RESOURCES_DIR/localghost-logo-source.png"
20
+ cp "$SOURCE_RESOURCES_DIR/localghost-widget-ui-reference.png" "$RESOURCES_DIR/localghost-widget-ui-reference.png"
21
+
22
+ swiftc \
23
+ -parse-as-library \
24
+ -O \
25
+ -module-cache-path "$MODULE_CACHE_DIR" \
26
+ -framework AppKit \
27
+ "$SHARED_SOURCE_FILE" \
28
+ "$SOURCE_FILE" \
29
+ -o "$EXECUTABLE"
30
+
31
+ cat > "$CONTENTS_DIR/Info.plist" <<'PLIST'
32
+ <?xml version="1.0" encoding="UTF-8"?>
33
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
34
+ <plist version="1.0">
35
+ <dict>
36
+ <key>CFBundleExecutable</key>
37
+ <string>LocalghostWidget</string>
38
+ <key>CFBundleIdentifier</key>
39
+ <string>app.localghost.widget</string>
40
+ <key>CFBundleName</key>
41
+ <string>Localghost Widget</string>
42
+ <key>CFBundleDisplayName</key>
43
+ <string>Localghost Widget</string>
44
+ <key>CFBundlePackageType</key>
45
+ <string>APPL</string>
46
+ <key>CFBundleShortVersionString</key>
47
+ <string>0.1.0</string>
48
+ <key>CFBundleVersion</key>
49
+ <string>1</string>
50
+ <key>LSMinimumSystemVersion</key>
51
+ <string>13.0</string>
52
+ <key>LSUIElement</key>
53
+ <true/>
54
+ <key>NSHighResolutionCapable</key>
55
+ <true/>
56
+ </dict>
57
+ </plist>
58
+ PLIST
59
+
60
+ echo "Built $APP_DIR"
@@ -0,0 +1,40 @@
1
+ name: LocalghostWidget
2
+ options:
3
+ bundleIdPrefix: app.localghost
4
+ deploymentTarget:
5
+ macOS: "14.0"
6
+ settings:
7
+ base:
8
+ SWIFT_VERSION: "6.0"
9
+ DEVELOPMENT_TEAM: ""
10
+ targets:
11
+ LocalghostWidget:
12
+ type: application
13
+ platform: macOS
14
+ deploymentTarget: "14.0"
15
+ sources:
16
+ - path: LocalghostWidget.swift
17
+ - path: Shared/LocalghostWidgetSnapshot.swift
18
+ - path: Resources
19
+ entitlements:
20
+ path: LocalghostWidget.entitlements
21
+ settings:
22
+ base:
23
+ PRODUCT_BUNDLE_IDENTIFIER: app.localghost.widget
24
+ INFOPLIST_KEY_CFBundleDisplayName: Localghost Widget
25
+ INFOPLIST_KEY_LSUIElement: true
26
+ dependencies:
27
+ - target: LocalghostDesktopWidgetExtension
28
+ LocalghostDesktopWidgetExtension:
29
+ type: app-extension
30
+ platform: macOS
31
+ deploymentTarget: "14.0"
32
+ sources:
33
+ - path: WidgetExtension/LocalghostDesktopWidget.swift
34
+ - path: Shared/LocalghostWidgetSnapshot.swift
35
+ entitlements:
36
+ path: WidgetExtension/LocalghostDesktopWidgetExtension.entitlements
37
+ settings:
38
+ base:
39
+ PRODUCT_BUNDLE_IDENTIFIER: app.localghost.widget.desktop-extension
40
+ INFOPLIST_KEY_NSExtension_NSExtensionPointIdentifier: com.apple.widgetkit-extension
Binary file
Binary file
Binary file