initial commit
This commit is contained in:
commit
c64cf63125
|
|
@ -0,0 +1 @@
|
|||
/wmon
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
module wmon
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require github.com/gotk3/gotk3 v0.6.3
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
github.com/gotk3/gotk3 v0.6.3 h1:+Ke4WkM1TQUNOlM2TZH6szqknqo+zNbX3BZWVXjSHYw=
|
||||
github.com/gotk3/gotk3 v0.6.3/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||
github.com/gotk3/gotk3 v0.6.4 h1:5ur/PRr86PwCG8eSj98D1eXvhrNNK6GILS2zq779dCg=
|
||||
github.com/gotk3/gotk3 v0.6.4/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||
|
|
@ -0,0 +1,564 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gotk3/gotk3/cairo"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
type Monitor struct {
|
||||
Name string
|
||||
Model string
|
||||
Width int
|
||||
Height int
|
||||
X int
|
||||
Y int
|
||||
Enabled bool
|
||||
Refresh float64
|
||||
Scale float64
|
||||
}
|
||||
|
||||
type App struct {
|
||||
window *gtk.Window
|
||||
canvas *gtk.DrawingArea
|
||||
monitors []Monitor
|
||||
dragging int // index of monitor being dragged, -1 if none
|
||||
dragOffX float64
|
||||
dragOffY float64
|
||||
scale float64 // view scale (pixels per monitor-pixel)
|
||||
offsetX float64 // view pan offset
|
||||
offsetY float64
|
||||
panning bool
|
||||
panStartX float64
|
||||
panStartY float64
|
||||
panOffX float64
|
||||
panOffY float64
|
||||
layoutSet bool
|
||||
}
|
||||
|
||||
func parseWlrRandr() ([]Monitor, error) {
|
||||
out, err := exec.Command("wlr-randr").Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var monitors []Monitor
|
||||
var cur *Monitor
|
||||
lines := strings.Split(string(out), "\n")
|
||||
|
||||
nameRe := regexp.MustCompile(`^(\S+)\s+"(.+)"`)
|
||||
modeRe := regexp.MustCompile(`^\s+(\d+)x(\d+)\s+px,\s+([\d.]+)\s+Hz\s*(.*)`)
|
||||
posRe := regexp.MustCompile(`^\s+Position:\s+(\d+),(\d+)`)
|
||||
enabledRe := regexp.MustCompile(`^\s+Enabled:\s+(\w+)`)
|
||||
scaleRe := regexp.MustCompile(`^\s+Scale:\s+([\d.]+)`)
|
||||
|
||||
for _, line := range lines {
|
||||
if m := nameRe.FindStringSubmatch(line); m != nil {
|
||||
if cur != nil {
|
||||
monitors = append(monitors, *cur)
|
||||
}
|
||||
cur = &Monitor{Name: m[1], Model: m[2], Scale: 1.0}
|
||||
} else if cur != nil {
|
||||
if m := enabledRe.FindStringSubmatch(line); m != nil {
|
||||
cur.Enabled = m[1] == "yes"
|
||||
} else if m := modeRe.FindStringSubmatch(line); m != nil {
|
||||
if strings.Contains(m[4], "current") {
|
||||
cur.Width, _ = strconv.Atoi(m[1])
|
||||
cur.Height, _ = strconv.Atoi(m[2])
|
||||
cur.Refresh, _ = strconv.ParseFloat(m[3], 64)
|
||||
}
|
||||
} else if m := posRe.FindStringSubmatch(line); m != nil {
|
||||
cur.X, _ = strconv.Atoi(m[1])
|
||||
cur.Y, _ = strconv.Atoi(m[2])
|
||||
} else if m := scaleRe.FindStringSubmatch(line); m != nil {
|
||||
cur.Scale, _ = strconv.ParseFloat(m[1], 64)
|
||||
}
|
||||
}
|
||||
}
|
||||
if cur != nil {
|
||||
monitors = append(monitors, *cur)
|
||||
}
|
||||
return monitors, nil
|
||||
}
|
||||
|
||||
func (app *App) fitToCanvas() {
|
||||
alloc := app.canvas.GetAllocation()
|
||||
canvasW := float64(alloc.GetWidth())
|
||||
canvasH := float64(alloc.GetHeight())
|
||||
if canvasW < 10 || canvasH < 10 {
|
||||
canvasW, canvasH = 900, 600
|
||||
}
|
||||
|
||||
// Find bounding box of all monitors (enabled ones for position, disabled get parked)
|
||||
minX, minY := math.MaxFloat64, math.MaxFloat64
|
||||
maxX, maxY := -math.MaxFloat64, -math.MaxFloat64
|
||||
hasEnabled := false
|
||||
for _, m := range app.monitors {
|
||||
if !m.Enabled {
|
||||
continue
|
||||
}
|
||||
hasEnabled = true
|
||||
if float64(m.X) < minX {
|
||||
minX = float64(m.X)
|
||||
}
|
||||
if float64(m.Y) < minY {
|
||||
minY = float64(m.Y)
|
||||
}
|
||||
if float64(m.X+m.Width) > maxX {
|
||||
maxX = float64(m.X + m.Width)
|
||||
}
|
||||
if float64(m.Y+m.Height) > maxY {
|
||||
maxY = float64(m.Y + m.Height)
|
||||
}
|
||||
}
|
||||
if !hasEnabled {
|
||||
app.scale = 0.15
|
||||
app.offsetX = canvasW / 2
|
||||
app.offsetY = canvasH / 3
|
||||
return
|
||||
}
|
||||
|
||||
totalW := maxX - minX
|
||||
totalH := maxY - minY
|
||||
padding := 80.0
|
||||
|
||||
scaleX := (canvasW - padding) / totalW
|
||||
scaleY := (canvasH*0.7 - padding) / totalH
|
||||
app.scale = math.Min(scaleX, scaleY)
|
||||
|
||||
// Center
|
||||
cx := (minX + maxX) / 2
|
||||
cy := (minY + maxY) / 2
|
||||
app.offsetX = canvasW/2 - cx*app.scale
|
||||
app.offsetY = canvasH*0.35 - cy*app.scale
|
||||
}
|
||||
|
||||
// Convert monitor coords to canvas coords
|
||||
func (app *App) monToCanvas(mx, my float64) (float64, float64) {
|
||||
return mx*app.scale + app.offsetX, my*app.scale + app.offsetY
|
||||
}
|
||||
|
||||
// Convert canvas coords to monitor coords
|
||||
func (app *App) canvasToMon(cx, cy float64) (float64, float64) {
|
||||
return (cx - app.offsetX) / app.scale, (cy - app.offsetY) / app.scale
|
||||
}
|
||||
|
||||
func (app *App) monRect(m Monitor) (float64, float64, float64, float64) {
|
||||
x, y := app.monToCanvas(float64(m.X), float64(m.Y))
|
||||
w := float64(m.Width) * app.scale
|
||||
h := float64(m.Height) * app.scale
|
||||
return x, y, w, h
|
||||
}
|
||||
|
||||
func (app *App) inactiveY() float64 {
|
||||
alloc := app.canvas.GetAllocation()
|
||||
return float64(alloc.GetHeight()) * 0.78
|
||||
}
|
||||
|
||||
func (app *App) inactiveRect(idx int) (float64, float64, float64, float64) {
|
||||
inactiveIdx := 0
|
||||
for i := 0; i < idx; i++ {
|
||||
if !app.monitors[i].Enabled {
|
||||
inactiveIdx++
|
||||
}
|
||||
}
|
||||
w := 150.0
|
||||
h := 100.0
|
||||
x := 20.0 + float64(inactiveIdx)*(w+20)
|
||||
y := app.inactiveY() + 30
|
||||
return x, y, w, h
|
||||
}
|
||||
|
||||
func (app *App) hitTest(cx, cy float64) int {
|
||||
for i := len(app.monitors) - 1; i >= 0; i-- {
|
||||
m := app.monitors[i]
|
||||
var x, y, w, h float64
|
||||
if !m.Enabled {
|
||||
x, y, w, h = app.inactiveRect(i)
|
||||
} else {
|
||||
x, y, w, h = app.monRect(m)
|
||||
}
|
||||
if cx >= x && cx <= x+w && cy >= y && cy <= y+h {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (app *App) draw(da *gtk.DrawingArea, cr *cairo.Context) {
|
||||
alloc := da.GetAllocation()
|
||||
canvasW := float64(alloc.GetWidth())
|
||||
canvasH := float64(alloc.GetHeight())
|
||||
|
||||
if !app.layoutSet {
|
||||
app.fitToCanvas()
|
||||
app.layoutSet = true
|
||||
}
|
||||
|
||||
// Background
|
||||
cr.SetSourceRGB(0.15, 0.15, 0.18)
|
||||
cr.Rectangle(0, 0, canvasW, canvasH)
|
||||
cr.Fill()
|
||||
|
||||
// Inactive area
|
||||
iy := app.inactiveY()
|
||||
cr.SetSourceRGB(0.1, 0.1, 0.12)
|
||||
cr.Rectangle(0, iy, canvasW, canvasH-iy)
|
||||
cr.Fill()
|
||||
|
||||
cr.SetSourceRGB(0.4, 0.4, 0.4)
|
||||
cr.SetLineWidth(1)
|
||||
cr.MoveTo(0, iy)
|
||||
cr.LineTo(canvasW, iy)
|
||||
cr.Stroke()
|
||||
|
||||
cr.SetSourceRGB(0.5, 0.5, 0.5)
|
||||
cr.SetFontSize(12)
|
||||
cr.MoveTo(10, iy+18)
|
||||
cr.ShowText("Inactive / Off — drag monitors here to disable")
|
||||
|
||||
// Draw enabled monitors
|
||||
for i, m := range app.monitors {
|
||||
if !m.Enabled {
|
||||
continue
|
||||
}
|
||||
x, y, w, h := app.monRect(m)
|
||||
app.drawMonitor(cr, x, y, w, h, m, i == app.dragging)
|
||||
}
|
||||
|
||||
// Draw inactive monitors
|
||||
for i, m := range app.monitors {
|
||||
if m.Enabled {
|
||||
continue
|
||||
}
|
||||
x, y, w, h := app.inactiveRect(i)
|
||||
app.drawMonitor(cr, x, y, w, h, m, i == app.dragging)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) drawMonitor(cr *cairo.Context, x, y, w, h float64, m Monitor, selected bool) {
|
||||
bezel := 4.0
|
||||
|
||||
if selected {
|
||||
cr.SetSourceRGB(0.3, 0.6, 1.0)
|
||||
} else if m.Enabled {
|
||||
cr.SetSourceRGB(0.3, 0.3, 0.35)
|
||||
} else {
|
||||
cr.SetSourceRGB(0.25, 0.25, 0.28)
|
||||
}
|
||||
cr.Rectangle(x-bezel, y-bezel, w+bezel*2, h+bezel*2+15)
|
||||
cr.Fill()
|
||||
|
||||
if m.Enabled {
|
||||
cr.SetSourceRGB(0.2, 0.25, 0.35)
|
||||
} else {
|
||||
cr.SetSourceRGB(0.12, 0.12, 0.15)
|
||||
}
|
||||
cr.Rectangle(x, y, w, h)
|
||||
cr.Fill()
|
||||
|
||||
// Stand
|
||||
standW := w * 0.3
|
||||
cr.SetSourceRGB(0.25, 0.25, 0.28)
|
||||
cr.Rectangle(x+w/2-standW/2, y+h+bezel, standW, 12)
|
||||
cr.Fill()
|
||||
|
||||
// Labels
|
||||
cr.SetSourceRGB(0.9, 0.9, 0.9)
|
||||
fontSize := math.Max(10, math.Min(14, w/10))
|
||||
cr.SetFontSize(fontSize)
|
||||
cr.MoveTo(x+5, y+fontSize+3)
|
||||
cr.ShowText(m.Name)
|
||||
|
||||
cr.SetSourceRGB(0.6, 0.6, 0.6)
|
||||
cr.SetFontSize(fontSize * 0.8)
|
||||
cr.MoveTo(x+5, y+fontSize*2+3)
|
||||
cr.ShowText(fmt.Sprintf("%dx%d", m.Width, m.Height))
|
||||
|
||||
if m.Enabled {
|
||||
cr.SetSourceRGB(0.5, 0.5, 0.5)
|
||||
cr.SetFontSize(fontSize * 0.7)
|
||||
cr.MoveTo(x+5, y+fontSize*2.8+3)
|
||||
cr.ShowText(fmt.Sprintf("pos: %d,%d", m.X, m.Y))
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) onPress(ev *gdk.Event) {
|
||||
btn := gdk.EventButtonNewFromEvent(ev)
|
||||
x, y := btn.X(), btn.Y()
|
||||
|
||||
switch btn.Button() {
|
||||
case 1: // Left click - drag monitor
|
||||
app.dragging = app.hitTest(x, y)
|
||||
if app.dragging >= 0 {
|
||||
m := app.monitors[app.dragging]
|
||||
if m.Enabled {
|
||||
mx, my, _, _ := app.monRect(m)
|
||||
app.dragOffX = x - mx
|
||||
app.dragOffY = y - my
|
||||
} else {
|
||||
ix, iy, _, _ := app.inactiveRect(app.dragging)
|
||||
app.dragOffX = x - ix
|
||||
app.dragOffY = y - iy
|
||||
}
|
||||
}
|
||||
case 2: // Middle click - pan
|
||||
app.panning = true
|
||||
app.panStartX = x
|
||||
app.panStartY = y
|
||||
app.panOffX = app.offsetX
|
||||
app.panOffY = app.offsetY
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) onRelease(ev *gdk.Event) {
|
||||
btn := gdk.EventButtonNewFromEvent(ev)
|
||||
switch btn.Button() {
|
||||
case 1:
|
||||
app.dragging = -1
|
||||
case 2:
|
||||
app.panning = false
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) onMotion(ev *gdk.Event) {
|
||||
motion := gdk.EventMotionNewFromEvent(ev)
|
||||
x, y := motion.MotionVal()
|
||||
|
||||
if app.panning {
|
||||
app.offsetX = app.panOffX + (x - app.panStartX)
|
||||
app.offsetY = app.panOffY + (y - app.panStartY)
|
||||
app.canvas.QueueDraw()
|
||||
return
|
||||
}
|
||||
|
||||
if app.dragging < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if dragging into/out of inactive area
|
||||
if y > app.inactiveY() {
|
||||
app.monitors[app.dragging].Enabled = false
|
||||
app.canvas.QueueDraw()
|
||||
return
|
||||
}
|
||||
|
||||
// Enable if was inactive
|
||||
app.monitors[app.dragging].Enabled = true
|
||||
|
||||
// Convert canvas position to monitor coords
|
||||
newCX := x - app.dragOffX
|
||||
newCY := y - app.dragOffY
|
||||
mx, my := app.canvasToMon(newCX, newCY)
|
||||
|
||||
// Snap to 10px grid
|
||||
snapX := int(math.Round(mx/10)) * 10
|
||||
snapY := int(math.Round(my/10)) * 10
|
||||
|
||||
app.monitors[app.dragging].X = snapX
|
||||
app.monitors[app.dragging].Y = snapY
|
||||
app.canvas.QueueDraw()
|
||||
}
|
||||
|
||||
func (app *App) onScroll(ev *gdk.Event) {
|
||||
scroll := gdk.EventScrollNewFromEvent(ev)
|
||||
dy := scroll.DeltaY()
|
||||
|
||||
// Also handle discrete scroll (non-smooth)
|
||||
if dy == 0 {
|
||||
switch scroll.Direction() {
|
||||
case gdk.SCROLL_UP:
|
||||
dy = -1
|
||||
case gdk.SCROLL_DOWN:
|
||||
dy = 1
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Zoom toward cursor
|
||||
x, y := scroll.X(), scroll.Y()
|
||||
oldScale := app.scale
|
||||
|
||||
if dy < 0 {
|
||||
app.scale *= 1.15
|
||||
} else {
|
||||
app.scale /= 1.15
|
||||
}
|
||||
// Clamp
|
||||
app.scale = math.Max(0.02, math.Min(1.0, app.scale))
|
||||
|
||||
// Adjust offset so point under cursor stays fixed
|
||||
app.offsetX = x - (x-app.offsetX)*(app.scale/oldScale)
|
||||
app.offsetY = y - (y-app.offsetY)*(app.scale/oldScale)
|
||||
|
||||
app.canvas.QueueDraw()
|
||||
}
|
||||
|
||||
func configDir() string {
|
||||
dir, _ := os.UserConfigDir()
|
||||
p := filepath.Join(dir, "wmon")
|
||||
os.MkdirAll(p, 0755)
|
||||
return p
|
||||
}
|
||||
|
||||
func (app *App) saveConfig() {
|
||||
dialog, _ := gtk.FileChooserDialogNewWith2Buttons("Save Layout", app.window,
|
||||
gtk.FILE_CHOOSER_ACTION_SAVE, "Cancel", gtk.RESPONSE_CANCEL, "Save", gtk.RESPONSE_ACCEPT)
|
||||
dialog.SetCurrentFolder(configDir())
|
||||
dialog.SetCurrentName("layout.json")
|
||||
dialog.SetDoOverwriteConfirmation(true)
|
||||
|
||||
filter, _ := gtk.FileFilterNew()
|
||||
filter.AddPattern("*.json")
|
||||
filter.SetName("JSON files")
|
||||
dialog.AddFilter(filter)
|
||||
|
||||
if dialog.Run() == gtk.RESPONSE_ACCEPT {
|
||||
path := dialog.GetFilename()
|
||||
data, _ := json.MarshalIndent(app.monitors, "", " ")
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
log.Printf("Save error: %v", err)
|
||||
}
|
||||
}
|
||||
dialog.Destroy()
|
||||
}
|
||||
|
||||
func (app *App) loadConfig() {
|
||||
dialog, _ := gtk.FileChooserDialogNewWith2Buttons("Load Layout", app.window,
|
||||
gtk.FILE_CHOOSER_ACTION_OPEN, "Cancel", gtk.RESPONSE_CANCEL, "Open", gtk.RESPONSE_ACCEPT)
|
||||
dialog.SetCurrentFolder(configDir())
|
||||
|
||||
filter, _ := gtk.FileFilterNew()
|
||||
filter.AddPattern("*.json")
|
||||
filter.SetName("JSON files")
|
||||
dialog.AddFilter(filter)
|
||||
|
||||
if dialog.Run() == gtk.RESPONSE_ACCEPT {
|
||||
path := dialog.GetFilename()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Printf("Load error: %v", err)
|
||||
dialog.Destroy()
|
||||
return
|
||||
}
|
||||
var monitors []Monitor
|
||||
if err := json.Unmarshal(data, &monitors); err != nil {
|
||||
log.Printf("Parse error: %v", err)
|
||||
dialog.Destroy()
|
||||
return
|
||||
}
|
||||
app.monitors = monitors
|
||||
app.layoutSet = false
|
||||
app.canvas.QueueDraw()
|
||||
}
|
||||
dialog.Destroy()
|
||||
}
|
||||
|
||||
func (app *App) apply() {
|
||||
var args []string
|
||||
for _, m := range app.monitors {
|
||||
if m.Enabled {
|
||||
args = append(args, "--output", m.Name, "--on", "--pos",
|
||||
fmt.Sprintf("%d,%d", m.X, m.Y))
|
||||
} else {
|
||||
args = append(args, "--output", m.Name, "--off")
|
||||
}
|
||||
}
|
||||
cmd := exec.Command("wlr-randr", args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("wlr-randr error: %v\n%s", err, out)
|
||||
dialog := gtk.MessageDialogNew(app.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR,
|
||||
gtk.BUTTONS_OK, "wlr-randr failed: %s\n%s", err, string(out))
|
||||
dialog.Run()
|
||||
dialog.Destroy()
|
||||
} else {
|
||||
log.Printf("Applied: wlr-randr %s", strings.Join(args, " "))
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
gtk.Init(nil)
|
||||
|
||||
app := &App{dragging: -1, scale: 0.15}
|
||||
|
||||
var err error
|
||||
app.monitors, err = parseWlrRandr()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse wlr-randr: %v", err)
|
||||
}
|
||||
|
||||
app.window, _ = gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
|
||||
app.window.SetTitle("wmon")
|
||||
app.window.SetDefaultSize(900, 600)
|
||||
app.window.Connect("destroy", gtk.MainQuit)
|
||||
|
||||
vbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
app.window.Add(vbox)
|
||||
|
||||
app.canvas, _ = gtk.DrawingAreaNew()
|
||||
app.canvas.SetHExpand(true)
|
||||
app.canvas.SetVExpand(true)
|
||||
app.canvas.Connect("draw", app.draw)
|
||||
|
||||
app.canvas.AddEvents(int(gdk.BUTTON_PRESS_MASK | gdk.BUTTON_RELEASE_MASK |
|
||||
gdk.POINTER_MOTION_MASK | gdk.SCROLL_MASK | gdk.SMOOTH_SCROLL_MASK))
|
||||
app.canvas.Connect("button-press-event", func(da *gtk.DrawingArea, ev *gdk.Event) {
|
||||
app.onPress(ev)
|
||||
})
|
||||
app.canvas.Connect("button-release-event", func(da *gtk.DrawingArea, ev *gdk.Event) {
|
||||
app.onRelease(ev)
|
||||
})
|
||||
app.canvas.Connect("motion-notify-event", func(da *gtk.DrawingArea, ev *gdk.Event) {
|
||||
app.onMotion(ev)
|
||||
})
|
||||
app.canvas.Connect("scroll-event", func(da *gtk.DrawingArea, ev *gdk.Event) {
|
||||
app.onScroll(ev)
|
||||
})
|
||||
|
||||
vbox.PackStart(app.canvas, true, true, 0)
|
||||
|
||||
btnBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
|
||||
btnBox.SetMarginTop(5)
|
||||
btnBox.SetMarginBottom(5)
|
||||
btnBox.SetMarginStart(5)
|
||||
btnBox.SetMarginEnd(5)
|
||||
|
||||
applyBtn, _ := gtk.ButtonNewWithLabel("Apply")
|
||||
applyBtn.Connect("clicked", func() { app.apply() })
|
||||
|
||||
reloadBtn, _ := gtk.ButtonNewWithLabel("Reload")
|
||||
reloadBtn.Connect("clicked", func() {
|
||||
app.monitors, _ = parseWlrRandr()
|
||||
app.layoutSet = false
|
||||
app.canvas.QueueDraw()
|
||||
})
|
||||
|
||||
btnBox.PackEnd(applyBtn, false, false, 0)
|
||||
btnBox.PackEnd(reloadBtn, false, false, 0)
|
||||
|
||||
saveBtn, _ := gtk.ButtonNewWithLabel("Save")
|
||||
saveBtn.Connect("clicked", func() { app.saveConfig() })
|
||||
btnBox.PackStart(saveBtn, false, false, 0)
|
||||
|
||||
loadBtn, _ := gtk.ButtonNewWithLabel("Load")
|
||||
loadBtn.Connect("clicked", func() { app.loadConfig() })
|
||||
btnBox.PackStart(loadBtn, false, false, 0)
|
||||
|
||||
vbox.PackStart(btnBox, false, false, 0)
|
||||
|
||||
app.window.ShowAll()
|
||||
gtk.Main()
|
||||
}
|
||||
Loading…
Reference in New Issue