mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-02 10:32:07 -04:00
Compare commits
4 Commits
0b88055742
...
3194fc3fbe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3194fc3fbe | ||
|
|
3318864ece | ||
|
|
e224417593 | ||
|
|
3f7f6c5d2c |
@@ -53,7 +53,6 @@ var (
|
|||||||
clipCopyPasteOnce bool
|
clipCopyPasteOnce bool
|
||||||
clipCopyType string
|
clipCopyType string
|
||||||
clipCopyDownload bool
|
clipCopyDownload bool
|
||||||
clipCopyCacheFile string
|
|
||||||
clipJSONOutput bool
|
clipJSONOutput bool
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -192,8 +191,6 @@ func init() {
|
|||||||
clipCopyCmd.Flags().BoolVarP(&clipCopyPasteOnce, "paste-once", "o", false, "Exit after first paste")
|
clipCopyCmd.Flags().BoolVarP(&clipCopyPasteOnce, "paste-once", "o", false, "Exit after first paste")
|
||||||
clipCopyCmd.Flags().StringVarP(&clipCopyType, "type", "t", "text/plain;charset=utf-8", "MIME type")
|
clipCopyCmd.Flags().StringVarP(&clipCopyType, "type", "t", "text/plain;charset=utf-8", "MIME type")
|
||||||
clipCopyCmd.Flags().BoolVarP(&clipCopyDownload, "download", "d", false, "Download URL as image and copy as file")
|
clipCopyCmd.Flags().BoolVarP(&clipCopyDownload, "download", "d", false, "Download URL as image and copy as file")
|
||||||
clipCopyCmd.Flags().StringVar(&clipCopyCacheFile, "cache-file", "", "")
|
|
||||||
clipCopyCmd.Flags().MarkHidden("cache-file")
|
|
||||||
|
|
||||||
clipWatchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
clipWatchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
||||||
clipHistoryCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
clipHistoryCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
||||||
@@ -224,13 +221,6 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||||
if clipCopyCacheFile != "" {
|
|
||||||
if err := clipboard.ServeCacheFile(clipCopyCacheFile, clipCopyType, clipCopyPasteOnce); err != nil {
|
|
||||||
log.Fatalf("serve cache file: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var data []byte
|
var data []byte
|
||||||
copyFromStdin := false
|
copyFromStdin := false
|
||||||
|
|
||||||
|
|||||||
@@ -820,10 +820,14 @@ func checkOptionalDependencies() []checkResult {
|
|||||||
results = append(results, checkImageFormatPlugins()...)
|
results = append(results, checkImageFormatPlugins()...)
|
||||||
|
|
||||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
terminals = slices.DeleteFunc(terminals, func(t string) bool {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
|
return !utils.CommandExists(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(terminals) > 0 {
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, strings.Join(terminals, ", "), "", optionalFeaturesURL})
|
||||||
} else {
|
} else {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, foot or alacritty", optionalFeaturesURL})
|
||||||
}
|
}
|
||||||
|
|
||||||
networkResult, err := network.DetectNetworkStack()
|
networkResult, err := network.DetectNetworkStack()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,7 +31,9 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if os.Geteuid() == 0 {
|
clipboard.MaybeServeAndExit()
|
||||||
|
|
||||||
|
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,7 +28,9 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if os.Geteuid() == 0 {
|
clipboard.MaybeServeAndExit()
|
||||||
|
|
||||||
|
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,22 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// isReadOnlyCommand returns true if the CLI args indicate a command that is
|
||||||
|
// safe to run as root (e.g. shell completion, help).
|
||||||
|
func isReadOnlyCommand(args []string) bool {
|
||||||
|
for _, arg := range args[1:] {
|
||||||
|
if strings.HasPrefix(arg, "-") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch arg {
|
||||||
|
case "completion", "help", "__complete":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func isArchPackageInstalled(packageName string) bool {
|
func isArchPackageInstalled(packageName string) bool {
|
||||||
cmd := exec.Command("pacman", "-Q", packageName)
|
cmd := exec.Command("pacman", "-Q", packageName)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
|||||||
@@ -12,35 +12,95 @@ import (
|
|||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const envServe = "_DMS_CLIPBOARD_SERVE"
|
||||||
|
const envMime = "_DMS_CLIPBOARD_MIME"
|
||||||
|
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
|
||||||
|
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
|
||||||
|
|
||||||
|
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
|
||||||
|
// child. Reads source data into memory, deletes any cache file, then serves.
|
||||||
|
func MaybeServeAndExit() {
|
||||||
|
if os.Getenv(envServe) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mimeType := os.Getenv(envMime)
|
||||||
|
pasteOnce := os.Getenv(envPasteOnce) == "1"
|
||||||
|
cachePath := os.Getenv(envCacheFile)
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cachePath != "":
|
||||||
|
data, err = os.ReadFile(cachePath)
|
||||||
|
os.Remove(cachePath)
|
||||||
|
default:
|
||||||
|
data, err = io.ReadAll(os.Stdin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "clipboard: read source: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
func Copy(data []byte, mimeType string) error {
|
func Copy(data []byte, mimeType string) error {
|
||||||
return copyForkCached(data, mimeType, false)
|
return copyForkCached(data, mimeType, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if foreground {
|
if foreground {
|
||||||
return copyServeWithWriter(func(writer io.Writer) error {
|
return serveClipboard(data, mimeType, pasteOnce)
|
||||||
total := 0
|
|
||||||
for total < len(data) {
|
|
||||||
n, err := writer.Write(data[total:])
|
|
||||||
total += n
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if total != len(data) {
|
|
||||||
return io.ErrShortWrite
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, mimeType, pasteOnce)
|
|
||||||
}
|
}
|
||||||
return copyForkCached(data, mimeType, pasteOnce)
|
return copyForkCached(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if !foreground {
|
if foreground {
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
buf, err := io.ReadAll(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read source: %w", err)
|
||||||
|
}
|
||||||
|
return serveClipboard(buf, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
return copyServeReader(data, mimeType, pasteOnce)
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
||||||
|
cmd := exec.Command(os.Args[0])
|
||||||
|
cmd.Stderr = nil
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
|
envServe+"=1",
|
||||||
|
envMime+"="+mimeType,
|
||||||
|
)
|
||||||
|
if pasteOnce {
|
||||||
|
cmd.Env = append(cmd.Env, envPasteOnce+"=1")
|
||||||
|
}
|
||||||
|
cmd.Env = append(cmd.Env, extra...)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitReady(cmd *exec.Cmd) error {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
var buf [1]byte
|
||||||
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
|
func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
@@ -60,65 +120,34 @@ func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
return fmt.Errorf("close cache file: %w", err)
|
return fmt.Errorf("close cache file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground", "--cache-file", cachePath}
|
cmd := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath)
|
||||||
if pasteOnce {
|
|
||||||
args = append(args, "--paste-once")
|
|
||||||
}
|
|
||||||
args = append(args, "--type", mimeType)
|
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
|
||||||
cmd.Stdin = nil
|
cmd.Stdin = nil
|
||||||
cmd.Stderr = nil
|
if err := waitReady(cmd); err != nil {
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
|
||||||
cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1")
|
|
||||||
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
os.Remove(cachePath)
|
os.Remove(cachePath)
|
||||||
return fmt.Errorf("stdout pipe: %w", err)
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
os.Remove(cachePath)
|
|
||||||
return fmt.Errorf("start: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf [1]byte
|
|
||||||
if _, err := stdout.Read(buf[:]); err != nil {
|
|
||||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
cmd := newForkCmd(mimeType, pasteOnce)
|
||||||
if pasteOnce {
|
|
||||||
args = append(args, "--paste-once")
|
|
||||||
}
|
|
||||||
args = append(args, "--type", mimeType)
|
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
|
||||||
cmd.Stderr = nil
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
|
||||||
cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1")
|
|
||||||
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("stdout pipe: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch src := data.(type) {
|
switch src := data.(type) {
|
||||||
case *os.File:
|
case *os.File:
|
||||||
cmd.Stdin = src
|
cmd.Stdin = src
|
||||||
if err := cmd.Start(); err != nil {
|
return waitReady(cmd)
|
||||||
return fmt.Errorf("start: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
stdin, err := cmd.StdinPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("start: %w", err)
|
return fmt.Errorf("start: %w", err)
|
||||||
}
|
}
|
||||||
@@ -129,63 +158,22 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
|||||||
if err := stdin.Close(); err != nil {
|
if err := stdin.Close(); err != nil {
|
||||||
return fmt.Errorf("close stdin: %w", err)
|
return fmt.Errorf("close stdin: %w", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var buf [1]byte
|
var buf [1]byte
|
||||||
if _, err := stdout.Read(buf[:]); err != nil {
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalReady() {
|
func signalReady() {
|
||||||
if os.Getenv("DMS_CLIP_FORKED") == "" {
|
if os.Getenv(envServe) == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
os.Stdout.Write([]byte{1})
|
os.Stdout.Write([]byte{1})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServeCacheFile(path, mimeType string, pasteOnce bool) error {
|
|
||||||
defer os.Remove(path)
|
|
||||||
return copyServeWithWriter(func(writer io.Writer) error {
|
|
||||||
f, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("open cache file: %w", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
_, err = io.Copy(writer, f)
|
|
||||||
return err
|
|
||||||
}, mimeType, pasteOnce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
|
|
||||||
cachedData, err := createClipboardCacheFile()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create clipboard cache file: %w", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(cachedData.Name())
|
|
||||||
|
|
||||||
if _, err := io.Copy(cachedData, data); err != nil {
|
|
||||||
return fmt.Errorf("cache clipboard data: %w", err)
|
|
||||||
}
|
|
||||||
if err := cachedData.Close(); err != nil {
|
|
||||||
return fmt.Errorf("close temp cache file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return copyServeWithWriter(func(writer io.Writer) error {
|
|
||||||
cachedFile, err := os.Open(cachedData.Name())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("open temp cache file: %w", err)
|
|
||||||
}
|
|
||||||
defer cachedFile.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(writer, cachedFile); err != nil {
|
|
||||||
return fmt.Errorf("write clipboard data: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, mimeType, pasteOnce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createClipboardCacheFile() (*os.File, error) {
|
func createClipboardCacheFile() (*os.File, error) {
|
||||||
preferredDirs := []string{}
|
preferredDirs := []string{}
|
||||||
|
|
||||||
@@ -206,7 +194,7 @@ func createClipboardCacheFile() (*os.File, error) {
|
|||||||
return os.CreateTemp("", "dms-clipboard-*")
|
return os.CreateTemp("", "dms-clipboard-*")
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
|
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
display, err := wlclient.Connect("")
|
display, err := wlclient.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
return fmt.Errorf("wayland connect: %w", err)
|
||||||
@@ -248,12 +236,10 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
if bindErr != nil {
|
if bindErr != nil {
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
return fmt.Errorf("registry bind: %w", bindErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
if dataControlMgr == nil {
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
}
|
}
|
||||||
defer dataControlMgr.Destroy()
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
if seat == nil {
|
if seat == nil {
|
||||||
return fmt.Errorf("no seat available")
|
return fmt.Errorf("no seat available")
|
||||||
}
|
}
|
||||||
@@ -292,18 +278,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
|
|
||||||
cancelled := make(chan struct{})
|
cancelled := make(chan struct{})
|
||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
sendErr := make(chan error, 1)
|
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
defer syscall.Close(e.Fd)
|
_ = syscall.SetNonblock(e.Fd, false)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
if err := writeTo(file); err != nil {
|
_, _ = file.Write(data)
|
||||||
select {
|
|
||||||
case sendErr <- err:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -325,8 +305,6 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
select {
|
select {
|
||||||
case <-cancelled:
|
case <-cancelled:
|
||||||
return nil
|
return nil
|
||||||
case err := <-sendErr:
|
|
||||||
return err
|
|
||||||
case <-pasted:
|
case <-pasted:
|
||||||
if pasteOnce {
|
if pasteOnce {
|
||||||
return nil
|
return nil
|
||||||
@@ -580,12 +558,10 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|||||||
if bindErr != nil {
|
if bindErr != nil {
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
return fmt.Errorf("registry bind: %w", bindErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
if dataControlMgr == nil {
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
}
|
}
|
||||||
defer dataControlMgr.Destroy()
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
if seat == nil {
|
if seat == nil {
|
||||||
return fmt.Errorf("no seat available")
|
return fmt.Errorf("no seat available")
|
||||||
}
|
}
|
||||||
@@ -613,12 +589,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
defer syscall.Close(e.Fd)
|
_ = syscall.SetNonblock(e.Fd, false)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
if data, ok := offerMap[e.MimeType]; ok {
|
if data, ok := offerMap[e.MimeType]; ok {
|
||||||
file.Write(data)
|
_, _ = file.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -434,6 +434,7 @@ Singleton {
|
|||||||
property bool soundNewNotification: true
|
property bool soundNewNotification: true
|
||||||
property bool soundVolumeChanged: true
|
property bool soundVolumeChanged: true
|
||||||
property bool soundPluggedIn: true
|
property bool soundPluggedIn: true
|
||||||
|
property bool soundLogin: false
|
||||||
|
|
||||||
property int acMonitorTimeout: 0
|
property int acMonitorTimeout: 0
|
||||||
property int acLockTimeout: 0
|
property int acLockTimeout: 0
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ var SPEC = {
|
|||||||
|
|
||||||
soundsEnabled: { def: true },
|
soundsEnabled: { def: true },
|
||||||
useSystemSoundTheme: { def: false },
|
useSystemSoundTheme: { def: false },
|
||||||
|
soundLogin: { def: false },
|
||||||
soundNewNotification: { def: true },
|
soundNewNotification: { def: true },
|
||||||
soundVolumeChanged: { def: true },
|
soundVolumeChanged: { def: true },
|
||||||
soundPluggedIn: { def: true },
|
soundPluggedIn: { def: true },
|
||||||
|
|||||||
@@ -221,10 +221,22 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: loginSoundTimer
|
||||||
|
// Half a second delay before playing login sound, otherwise the sound may be cut off
|
||||||
|
// 50 is the minimum that seems to work, but 500 is safer
|
||||||
|
interval: 500
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
AudioService.playLoginSoundIfApplicable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
dockRecreateDebounce.start();
|
dockRecreateDebounce.start();
|
||||||
// Force PolkitService singleton to initialize
|
// Force PolkitService singleton to initialize
|
||||||
PolkitService.polkitAvailable;
|
PolkitService.polkitAvailable;
|
||||||
|
loginSoundTimer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
|
|||||||
@@ -91,6 +91,16 @@ Item {
|
|||||||
visible: AudioService.gsettingsAvailable
|
visible: AudioService.gsettingsAvailable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tab: "sounds"
|
||||||
|
tags: ["sound", "login", "startup", "boot"]
|
||||||
|
settingKey: "soundLogin"
|
||||||
|
text: I18n.tr("Login")
|
||||||
|
description: I18n.tr("Play sound after logging in")
|
||||||
|
checked: SettingsData.soundLogin
|
||||||
|
onToggled: checked => SettingsData.set("soundLogin", checked)
|
||||||
|
}
|
||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
tab: "sounds"
|
tab: "sounds"
|
||||||
tags: ["sound", "notification", "new"]
|
tags: ["sound", "notification", "new"]
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ Singleton {
|
|||||||
property var powerUnplugSound: null
|
property var powerUnplugSound: null
|
||||||
property var normalNotificationSound: null
|
property var normalNotificationSound: null
|
||||||
property var criticalNotificationSound: null
|
property var criticalNotificationSound: null
|
||||||
|
property var loginSound: null
|
||||||
property real notificationsVolume: 1.0
|
property real notificationsVolume: 1.0
|
||||||
property bool notificationsAudioMuted: false
|
property bool notificationsAudioMuted: false
|
||||||
|
|
||||||
@@ -67,6 +68,16 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used in playLoginSoundIfApplicable()
|
||||||
|
Process {
|
||||||
|
id: loginSoundChecker
|
||||||
|
onExited: (exitCode) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
playLoginSound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getAvailableSinks() {
|
function getAvailableSinks() {
|
||||||
const hidden = SessionData.hiddenOutputDeviceNames ?? [];
|
const hidden = SessionData.hiddenOutputDeviceNames ?? [];
|
||||||
return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name));
|
return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name));
|
||||||
@@ -395,7 +406,7 @@ EOFCONFIG
|
|||||||
const themesToSearch = themeName !== "freedesktop" ? `${themeName} freedesktop` : themeName;
|
const themesToSearch = themeName !== "freedesktop" ? `${themeName} freedesktop` : themeName;
|
||||||
|
|
||||||
const script = `
|
const script = `
|
||||||
for event_key in audio-volume-change power-plug power-unplug message message-new-instant; do
|
for event_key in audio-volume-change power-plug power-unplug message message-new-instant desktop-login; do
|
||||||
found=0
|
found=0
|
||||||
|
|
||||||
case "$event_key" in
|
case "$event_key" in
|
||||||
@@ -457,7 +468,8 @@ EOFCONFIG
|
|||||||
"power-plug": "../assets/sounds/plasma/power-plug.wav",
|
"power-plug": "../assets/sounds/plasma/power-plug.wav",
|
||||||
"power-unplug": "../assets/sounds/plasma/power-unplug.wav",
|
"power-unplug": "../assets/sounds/plasma/power-unplug.wav",
|
||||||
"message": "../assets/sounds/freedesktop/message.wav",
|
"message": "../assets/sounds/freedesktop/message.wav",
|
||||||
"message-new-instant": "../assets/sounds/freedesktop/message-new-instant.wav"
|
"message-new-instant": "../assets/sounds/freedesktop/message-new-instant.wav",
|
||||||
|
"desktop-login": "../assets/sounds/freedesktop/desktop-login.wav"
|
||||||
};
|
};
|
||||||
|
|
||||||
const specialConditions = {
|
const specialConditions = {
|
||||||
@@ -551,6 +563,10 @@ EOFCONFIG
|
|||||||
criticalNotificationSound.destroy();
|
criticalNotificationSound.destroy();
|
||||||
criticalNotificationSound = null;
|
criticalNotificationSound = null;
|
||||||
}
|
}
|
||||||
|
if (loginSound) {
|
||||||
|
loginSound.destroy();
|
||||||
|
loginSound = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSoundPlayers() {
|
function createSoundPlayers() {
|
||||||
@@ -622,6 +638,19 @@ EOFCONFIG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, root, "AudioService.CriticalNotificationSound");
|
`, root, "AudioService.CriticalNotificationSound");
|
||||||
|
|
||||||
|
const loginPath = getSoundPath("desktop-login");
|
||||||
|
loginSound = Qt.createQmlObject(`
|
||||||
|
import QtQuick
|
||||||
|
import QtMultimedia
|
||||||
|
MediaPlayer {
|
||||||
|
source: "${loginPath}"
|
||||||
|
audioOutput: AudioOutput {
|
||||||
|
${deviceProperty}volume: notificationsVolume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, root, "AudioService.LoginSound");
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("AudioService: Error creating sound players:", e);
|
console.warn("AudioService: Error creating sound players:", e);
|
||||||
}
|
}
|
||||||
@@ -661,6 +690,31 @@ EOFCONFIG
|
|||||||
criticalNotificationSound.play();
|
criticalNotificationSound.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playLoginSound() {
|
||||||
|
if (!soundsAvailable || !loginSound || notificationsAudioMuted || isMediaPlaying()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loginSound.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
function playLoginSoundIfApplicable() {
|
||||||
|
if (SettingsData.soundsEnabled && SettingsData.soundLogin && !notificationsAudioMuted) {
|
||||||
|
// plays login sound on session start, but only if a specific file doesn't exist,
|
||||||
|
// to prevent it from playing on every DMS restart during the session
|
||||||
|
const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR");
|
||||||
|
const sessionId = Quickshell.env("XDG_SESSION_ID") || "0";
|
||||||
|
|
||||||
|
if (!runtimeDir) return;
|
||||||
|
|
||||||
|
const loginFile = `${runtimeDir}/danklinux.login-${sessionId}`;
|
||||||
|
|
||||||
|
// if file doesn't exist, touch it (0)
|
||||||
|
// If it exists, do nothing (1)
|
||||||
|
loginSoundChecker.command = ["sh", "-c", `[ ! -f ${loginFile} ] && touch ${loginFile}`];
|
||||||
|
loginSoundChecker.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function playVolumeChangeSoundIfEnabled() {
|
function playVolumeChangeSoundIfEnabled() {
|
||||||
if (SettingsData.soundsEnabled && SettingsData.soundVolumeChanged && !notificationsAudioMuted) {
|
if (SettingsData.soundsEnabled && SettingsData.soundVolumeChanged && !notificationsAudioMuted) {
|
||||||
playVolumeChangeSound();
|
playVolumeChangeSound();
|
||||||
|
|||||||
BIN
quickshell/assets/sounds/freedesktop/desktop-login.oga
Normal file
BIN
quickshell/assets/sounds/freedesktop/desktop-login.oga
Normal file
Binary file not shown.
BIN
quickshell/assets/sounds/freedesktop/desktop-login.wav
Normal file
BIN
quickshell/assets/sounds/freedesktop/desktop-login.wav
Normal file
Binary file not shown.
@@ -5101,6 +5101,26 @@
|
|||||||
],
|
],
|
||||||
"description": "Play sounds for system events"
|
"description": "Play sounds for system events"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "soundLogin",
|
||||||
|
"label": "Login",
|
||||||
|
"tabIndex": 15,
|
||||||
|
"category": "Sounds",
|
||||||
|
"keywords": [
|
||||||
|
"after",
|
||||||
|
"audio",
|
||||||
|
"boot",
|
||||||
|
"effects",
|
||||||
|
"logging",
|
||||||
|
"login",
|
||||||
|
"play",
|
||||||
|
"sfx",
|
||||||
|
"sound",
|
||||||
|
"sounds",
|
||||||
|
"startup"
|
||||||
|
],
|
||||||
|
"description": "Play sound after logging in"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "soundNewNotification",
|
"section": "soundNewNotification",
|
||||||
"label": "New Notification",
|
"label": "New Notification",
|
||||||
|
|||||||
Reference in New Issue
Block a user