-
Notifications
You must be signed in to change notification settings - Fork 45
Expand file tree
/
Copy pathmain.go
More file actions
240 lines (216 loc) · 7.95 KB
/
Copy pathmain.go
File metadata and controls
240 lines (216 loc) · 7.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
package main
import (
"embed"
"fmt"
"net/http"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"time"
// F-201: register pprof handlers on the default mux.
_ "net/http/pprof"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/ys-ll/uniterm/backend/log"
"github.com/ys-ll/uniterm/backend/store"
)
var Version = "dev"
// devBuild is true for `wails dev` (Version == "dev"); false for production
// builds where `-ldflags '-X main.Version=...'` sets a real version string.
// Used to gate the pprof HTTP listener so production binaries don't open it.
var devBuild = Version == "dev"
//go:embed all:frontend/dist
var assets embed.FS
func main() {
// Capture top-level panics
defer func() {
if r := recover(); r != nil {
_ = log.Init()
log.Writef("FATAL PANIC: %v\n%s", r, string(debug.Stack()))
log.Close()
os.Exit(1)
}
}()
if err := log.Init(); err != nil {
println("Failed to init log:", err.Error())
}
defer log.Close()
// F-201: expose net/http/pprof on localhost:6060 for dev builds only.
// Production builds (wails build) leave Version unchanged from "dev"
// unless ldflags set it; gate behind a build flag so production does
// not open a listener.
startPprofIfDev()
webviewDataPath := filepath.Join(os.TempDir(), fmt.Sprintf("uniTerm-webview2-%d", os.Getpid()))
os.MkdirAll(webviewDataPath, 0700)
app := NewApp(webviewDataPath)
// Linux multi-monitor maximize workaround:
// Wails sets default max size to primary display, which can clamp
// maximize on secondary monitors. Set to large values to disable.
// See: https://github.com/wailsapp/wails/issues/2431
maxW, maxH := 0, 0
if runtime.GOOS == "linux" {
maxW, maxH = 9999, 9999
}
// Read persisted window geometry before creating the window — it's fixed at
// creation in v3 (services start before the window exists, so ServiceStartup
// can't position it). Race the load against a short timeout so a slow disk
// doesn't delay first paint.
systemTitleBar := false
winW, winH := 1200, 800 // fallback before any saved geometry is applied
savedX, savedY := 0, 0
savedMaxed := false
if configDir, err := os.UserConfigDir(); err == nil {
ls := store.NewLocalStateStore(filepath.Join(configDir, "uniTerm"))
done := make(chan store.LocalState, 1)
go func() {
if state, err := ls.Load(); err == nil {
done <- state
return
}
done <- store.LocalState{}
}()
select {
case state := <-done:
systemTitleBar = state.SystemTitleBar
if state.WindowWidth > 0 && state.WindowHeight > 0 {
winW, winH = state.WindowWidth, state.WindowHeight
}
savedX, savedY = state.WindowX, state.WindowY
savedMaxed = state.WindowMaximised
case <-time.After(100 * time.Millisecond):
// Slow disk — paint the defaults. The goroutine continues to load
// in the background; its result is discarded because the window
// geometry options are fixed at startup.
}
}
// Restore a saved position only when one actually exists; otherwise keep v3's
// default (centered) so a fresh install doesn't land at (0,0).
startPos := application.WindowCentered
if savedX != 0 || savedY != 0 {
startPos = application.WindowXY
}
startState := application.WindowStateNormal
if savedMaxed {
startState = application.WindowStateMaximised
}
macTitleBar := application.MacTitleBarHiddenInset
if systemTitleBar {
macTitleBar = application.MacTitleBarDefault
}
// Clean external-edit scratch dirs left behind by previous runs that
// exited without cleanup (crash / force-kill). Old dirs only: a
// concurrently running instance keeps its own. Async: it only ever
// touches dirs older than the stale age, which no new session can
// collide with (fresh PID + fresh session ID), so startup never waits
// on it.
go sweepStaleExtEditDirs()
w3app := application.New(application.Options{
Name: "uniTerm",
Assets: application.AssetOptions{Handler: application.AssetFileServerFS(assets)},
OnShutdown: app.shutdown,
// WebviewUserDataPath is a Windows-only path for WebView2 user data; it is
// harmless (ignored) on other platforms.
Windows: application.WindowsOptions{
WebviewUserDataPath: webviewDataPath,
},
// Fixed program name so the window's WM_CLASS stays "uniterm" — the
// installed package's .desktop file sets StartupWMClass to the same
// value, which is what lets the dock/taskbar associate the running
// window with the app icon.
Linux: application.LinuxOptions{
ProgramName: "uniterm",
},
})
// On macOS, install the standard App + Edit menus. This must run AFTER
// application.New(): menu roles dereference the global app instance and
// panic with a nil pointer otherwise (macOS-only — NewAppMenu returns nil
// on other platforms, which masked the crash). The Edit menu is what
// routes the native Cmd+C/V/X/A/Z shortcuts to the first responder — every
// WKWebView text field (input/textarea/contenteditable) relies on it. An
// empty menu here used to suppress Wails' defaults but also killed those
// shortcuts app-wide, forcing per-component JS reimplementations. The menu
// lives in the top system menu bar, so it doesn't affect the frameless
// window. On Linux (GTK) a non-nil Menu creates an empty GtkMenuBar that
// shows as a thin white line in the frameless window, so leave it nil
// there. See issue #291.
var appMenu *application.Menu
if runtime.GOOS == "darwin" {
appMenu = application.NewMenu()
appMenu.AddRole(application.AppMenu)
appMenu.AddRole(application.EditMenu)
}
if appMenu != nil {
w3app.Menu.SetApplicationMenu(appMenu)
}
window := w3app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "uniTerm",
Width: winW,
Height: winH,
X: savedX,
Y: savedY,
InitialPosition: startPos,
StartState: startState,
MinWidth: 700,
MinHeight: 450,
MaxWidth: maxW,
MaxHeight: maxH,
Frameless: runtime.GOOS != "darwin" && !systemTitleBar,
BackgroundColour: application.RGBA{
Red: 27, Green: 38, Blue: 54, Alpha: 1,
},
EnableFileDrop: true,
Mac: application.MacWindow{
TitleBar: macTitleBar,
},
})
// Wails v3 delivers OS file drops to Go-side window-event listeners rather
// than (as v2 did) auto-forwarding them to the frontend. Re-emit the dropped
// absolute paths under the original v2 event name so `Events.On(...)` pickers
// (FileSidebar, SFTP tab) keep receiving them for path-based upload.
window.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
filenames := event.Context().DroppedFiles()
if len(filenames) == 0 {
return
}
x, y, elementID := 0, 0, ""
if details := event.Context().DropTargetDetails(); details != nil {
x, y = details.X, details.Y
elementID = details.ElementID
}
w3app.Event.Emit("common:WindowFilesDropped", map[string]any{
"x": x,
"y": y,
"elementId": elementID,
"filenames": filenames,
})
})
// Wire the bound App back to the runtime before registering it as a service:
// emit() / window operations route through these references.
app.app = w3app
app.window = window
w3app.RegisterService(application.NewService(app))
err := w3app.Run()
if err != nil {
log.Writef("Wails run error: %v", err)
}
}
// startPprofIfDev spawns a goroutine that serves net/http/pprof on
// localhost:6060 — only when running a dev build. Production builds
// (Version != "dev") deliberately skip this so end-users never have
// the debug listener open.
//
// The listener stays up for the lifetime of the process; its only job
// is to let `go tool pprof http://localhost:6060/debug/pprof/profile`
// connect and capture CPU/heap/block/goroutine profiles during
// reproduction of perf issues (see F-201 / audit §8.2).
func startPprofIfDev() {
if !devBuild {
return
}
go func() {
if err := http.ListenAndServe("localhost:6060", nil); err != nil && err != http.ErrServerClosed {
log.Writef("pprof listener failed: %v", err)
}
}()
}