136 lines
3.6 KiB
Go
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
|
|
}
|
|
}
|
|
}
|
|
}
|