-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathmain.go
More file actions
324 lines (276 loc) · 7.78 KB
/
Copy pathmain.go
File metadata and controls
324 lines (276 loc) · 7.78 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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/floatpane/lattice/internal/layout"
"github.com/floatpane/lattice/internal/plugin"
"github.com/floatpane/lattice/pkg/config"
"github.com/floatpane/lattice/pkg/module"
"github.com/floatpane/lattice/pkg/registry"
// Built-in modules register themselves via init().
_ "github.com/floatpane/lattice/internal/modules"
tea "charm.land/bubbletea/v2"
"github.com/joho/godotenv"
)
func main() {
_ = godotenv.Load()
if len(os.Args) > 1 {
switch os.Args[1] {
case "import":
cmdImport(os.Args[2:])
return
case "remove":
cmdRemove(os.Args[2:])
return
case "list":
cmdList()
return
case "help", "--help", "-h":
cmdHelp()
return
}
}
runDashboard()
}
// --- Dashboard ---
func runDashboard() {
cfg := config.Load()
var mods []module.Module
for _, mc := range cfg.Modules {
// Try built-in registry first.
if ctor := registry.Get(mc.Type); ctor != nil {
mods = append(mods, ctor(mc))
continue
}
// Try external plugin binary (lattice-<name> in PATH or plugins dir).
if bin := findPlugin(mc.Type); bin != "" {
mods = append(mods, plugin.NewExternalModule(bin, mc))
continue
}
}
p := tea.NewProgram(
&app{modules: mods, columns: cfg.Columns},
)
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
// findPlugin looks for a "lattice-<name>" binary in the plugins dir and PATH.
func findPlugin(name string) string {
binName := "lattice-" + name
// 1. Check ~/.config/lattice/plugins/
if home, err := os.UserHomeDir(); err == nil {
p := filepath.Join(home, ".config", "lattice", "plugins", binName)
if isExecutable(p) {
return p
}
}
// 2. Check PATH
if p, err := exec.LookPath(binName); err == nil {
return p
}
return ""
}
func isExecutable(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return !info.IsDir() && info.Mode()&0111 != 0
}
type app struct {
modules []module.Module
columns int
width int
height int
placements []layout.ScreenPlacement // image placements from last render
needsImageDraw bool // true when images need to be (re)drawn
}
func (a *app) Init() tea.Cmd {
var cmds []tea.Cmd
for _, m := range a.modules {
if cmd := m.Init(); cmd != nil {
cmds = append(cmds, cmd)
}
}
return tea.Batch(cmds...)
}
func (a *app) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
if msg.String() == "q" || msg.String() == "ctrl+c" {
return a, tea.Quit
}
case tea.WindowSizeMsg:
a.width = msg.Width
a.height = msg.Height
a.needsImageDraw = true
return a, nil
}
var cmds []tea.Cmd
for _, m := range a.modules {
if cmd := m.Update(msg); cmd != nil {
cmds = append(cmds, cmd)
}
}
// Only re-display images when something changed (new upload, resize, track change).
if a.needsImageDraw && len(a.placements) > 0 {
a.needsImageDraw = false
cmds = append(cmds, renderImagePlacements(a.placements))
}
return a, tea.Batch(cmds...)
}
func (a *app) View() tea.View {
var content string
if a.width == 0 {
content = "Starting Lattice…"
} else {
var p []layout.ScreenPlacement
content, p = layout.Render(a.modules, a.columns, a.width, a.height)
// Check if placements changed (new image, track change, resize, etc.)
if !samePlacements(a.placements, p) {
a.needsImageDraw = true
}
a.placements = p
}
v := tea.NewView(content)
v.AltScreen = true
return v
}
// renderImagePlacements builds a tea.Raw command that displays images at their
// absolute screen positions using cursor save/restore.
func renderImagePlacements(placements []layout.ScreenPlacement) tea.Cmd {
var b strings.Builder
for _, p := range placements {
// Save cursor, move to absolute position, render image, restore cursor
fmt.Fprintf(&b, "\x1b[s\x1b[%d;%dH%s\x1b[u", p.Row, p.Col, p.Escape)
}
raw := b.String()
return tea.Raw(raw)
}
func samePlacements(a, b []layout.ScreenPlacement) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i].Row != b[i].Row || a[i].Col != b[i].Col || a[i].Escape != b[i].Escape {
return false
}
}
return true
}
// --- CLI subcommands ---
func pluginsDir() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config", "lattice", "plugins")
}
func cmdImport(args []string) {
if len(args) == 0 {
fmt.Println("Usage: lattice import <go-package>")
fmt.Println()
fmt.Println("Installs a plugin binary. The package must produce a binary")
fmt.Println("named lattice-<name> (e.g., lattice-spotify).")
fmt.Println()
fmt.Println("Example:")
fmt.Println(" lattice import github.com/someone/lattice-spotify@latest")
os.Exit(1)
}
pkg := args[0]
dir := pluginsDir()
// Ensure plugins directory exists.
if err := os.MkdirAll(dir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create plugins dir: %v\n", err)
os.Exit(1)
}
fmt.Printf("Installing %s...\n", pkg)
cmd := exec.Command("go", "install", pkg)
cmd.Env = append(os.Environ(), "GOBIN="+dir)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Failed to install: %v\n", err)
os.Exit(1)
}
// Show what was installed.
entries, _ := os.ReadDir(dir)
fmt.Println("Installed plugins:")
for _, e := range entries {
fmt.Printf(" %s\n", e.Name())
}
fmt.Println("\nAdd the module name to your config (~/.config/lattice/config.yaml).")
fmt.Println("The module name is the binary name minus the 'lattice-' prefix.")
}
func cmdRemove(args []string) {
if len(args) == 0 {
fmt.Println("Usage: lattice remove <name>")
fmt.Println(" lattice remove spotify")
os.Exit(1)
}
name := args[0]
binName := "lattice-" + name
path := filepath.Join(pluginsDir(), binName)
if err := os.Remove(path); err != nil {
if os.IsNotExist(err) {
fmt.Printf("Plugin %q not found in %s\n", name, pluginsDir())
} else {
fmt.Fprintf(os.Stderr, "Failed to remove: %v\n", err)
}
os.Exit(1)
}
fmt.Printf("Removed %s\n", name)
fmt.Println("Don't forget to remove it from your config too.")
}
func cmdList() {
fmt.Println("Built-in modules:")
for _, name := range registry.List() {
fmt.Printf(" %s\n", name)
}
dir := pluginsDir()
entries, _ := os.ReadDir(dir)
var plugins []string
for _, e := range entries {
if strings.HasPrefix(e.Name(), "lattice-") && !e.IsDir() {
name := strings.TrimPrefix(e.Name(), "lattice-")
plugins = append(plugins, name)
}
}
if len(plugins) > 0 {
fmt.Println("\nInstalled plugins:")
for _, name := range plugins {
fmt.Printf(" %s\n", name)
}
}
}
func cmdHelp() {
fmt.Println(`Lattice — modular terminal dashboard
Usage:
lattice Launch the dashboard
lattice import <pkg> Install an external plugin module
lattice remove <name> Remove an installed plugin
lattice list Show built-in and installed modules
lattice help Show this help
Plugin system:
Plugins are standalone binaries named "lattice-<name>" that speak
JSON over stdin/stdout. They are installed to:
~/.config/lattice/plugins/
Install a plugin:
lattice import github.com/someone/lattice-spotify@latest
Then add it to your config:
modules:
- type: spotify
Creating a plugin:
A plugin is any binary named lattice-<name> that reads JSON from
stdin and writes JSON to stdout (one object per line).
Request types sent by lattice:
{"type":"init","config":{"key":"val"}} — once at startup
{"type":"update"} — periodic refresh
{"type":"view","width":40,"height":10} — render request
Response format:
{"name":"TITLE","content":"rendered text","interval":5}
The "interval" field (seconds) controls how often "update" is sent.
See pkg/plugin for the full protocol definition.`)
}