// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package preferences /* #cgo pkg-config: atspi-2 #include #include "native_applications.h" */ import "C" import ( "context" "encoding/json" "errors" "fmt" "io" "os" "sort" "strings" "time" "unsafe" "git.stormux.org/storm/cthulhu/internal/platform/xorg" ) type applicationIdentity struct { name string pid int } func application_choices(accessible []applicationIdentity, windows []xorg.ApplicationWindow) []string { names := make(map[string]bool) byPID := make(map[int]string) add := func(name string) { name = strings.TrimSpace(name) if name != "" && !strings.HasPrefix(strings.ToLower(name), "cthulhu") { names[name] = true } } for _, application := range accessible { add(application.name) if application.pid != 0 { byPID[application.pid] = application.name } } for _, window := range windows { name := window.Application if !window.Wine && byPID[window.PID] != "" { name = byPID[window.PID] } add(name) } var choices []string for name := range names { choices = append(choices, name) } sort.Slice(choices, func(i, j int) bool { return strings.ToLower(choices[i]) < strings.ToLower(choices[j]) }) return choices } func running_applications() []string { var count C.int native := C.prefs_application_names(&count) defer C.prefs_free_application_names(native, count) var accessible []applicationIdentity for _, application := range unsafe.Slice(native, int(count)) { name := C.GoString(application.name) if strings.TrimSpace(name) == "" && application.pid != 0 { // Match the reader's process-name fallback for nameless providers. if content, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", application.pid)); err == nil { name = strings.TrimSpace(string(content)) } } accessible = append(accessible, applicationIdentity{name, int(application.pid)}) } reader := xorg.OpenForegroundReader() defer reader.Close() return application_choices(accessible, reader.ApplicationWindows()) } // The chooser shares the preferences child and bounded pipe protocol. Cancel // emits no request, so it cannot open a profile or submit settings. func choose_application(ctx context.Context, updates <-chan Response, errorsIn <-chan error, output io.Writer) (Response, bool, error) { choices := running_applications() if C.prefs_application_chooser_init() == 0 { return Response{}, false, errors.New("cannot open application chooser on this display") } defer C.prefs_application_chooser_close() for _, choice := range choices { name := C.CString(choice) C.prefs_application_chooser_add(name) C.free(unsafe.Pointer(name)) } C.prefs_application_chooser_present() ticker := time.NewTicker(time.Millisecond) defer ticker.Stop() selected := false for { select { case <-ctx.Done(): return Response{}, false, ctx.Err() case err := <-errorsIn: return Response{}, false, err case response := <-updates: if response.Present { C.prefs_application_chooser_present() } else if selected { return response, true, nil } case <-ticker.C: C.prefs_application_chooser_iterate() switch C.prefs_application_chooser_action() { case 2: return Response{}, false, nil case 1: if selected { continue } name := C.prefs_application_chooser_selected() if name == nil { continue } application := C.GoString(name) C.free(unsafe.Pointer(name)) if err := json.NewEncoder(output).Encode(Request{Application: application}); err != nil { return Response{}, false, err } selected = true } } } }