Files

136 lines
3.6 KiB
Go

// 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 <stdlib.h>
#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
}
}
}
}