mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-28 15:32:50 -05:00
keyboard shortcuts: comprehensive keyboard shortcut management interface
- niri only for now - requires quickshell-git, hidden otherwise - Add, Edit, Delete keybinds - Large suite of pre-defined and custom actions - Works with niri 25.11+ include feature
This commit is contained in:
@@ -40,11 +40,34 @@ var keybindsShowCmd = &cobra.Command{
|
|||||||
Run: runKeybindsShow,
|
Run: runKeybindsShow,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var keybindsSetCmd = &cobra.Command{
|
||||||
|
Use: "set <provider> <key> <action>",
|
||||||
|
Short: "Set a keybind override",
|
||||||
|
Long: "Create or update a keybind override for the specified provider",
|
||||||
|
Args: cobra.ExactArgs(3),
|
||||||
|
Run: runKeybindsSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
var keybindsRemoveCmd = &cobra.Command{
|
||||||
|
Use: "remove <provider> <key>",
|
||||||
|
Short: "Remove a keybind override",
|
||||||
|
Long: "Remove a keybind override from the specified provider",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
Run: runKeybindsRemove,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
||||||
|
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
|
||||||
|
keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked")
|
||||||
|
keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds")
|
||||||
|
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
|
||||||
|
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
|
||||||
|
|
||||||
keybindsCmd.AddCommand(keybindsListCmd)
|
keybindsCmd.AddCommand(keybindsListCmd)
|
||||||
keybindsCmd.AddCommand(keybindsShowCmd)
|
keybindsCmd.AddCommand(keybindsShowCmd)
|
||||||
|
keybindsCmd.AddCommand(keybindsSetCmd)
|
||||||
|
keybindsCmd.AddCommand(keybindsRemoveCmd)
|
||||||
|
|
||||||
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
|
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
|
||||||
return providers.NewJSONFileProvider(filePath)
|
return providers.NewJSONFileProvider(filePath)
|
||||||
@@ -82,69 +105,122 @@ func initializeProviders() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKeybindsList(cmd *cobra.Command, args []string) {
|
func runKeybindsList(_ *cobra.Command, _ []string) {
|
||||||
registry := keybinds.GetDefaultRegistry()
|
providerList := keybinds.GetDefaultRegistry().List()
|
||||||
providers := registry.List()
|
if len(providerList) == 0 {
|
||||||
|
|
||||||
if len(providers) == 0 {
|
|
||||||
fmt.Fprintln(os.Stdout, "No providers available")
|
fmt.Fprintln(os.Stdout, "No providers available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintln(os.Stdout, "Available providers:")
|
fmt.Fprintln(os.Stdout, "Available providers:")
|
||||||
for _, name := range providers {
|
for _, name := range providerList {
|
||||||
fmt.Fprintf(os.Stdout, " - %s\n", name)
|
fmt.Fprintf(os.Stdout, " - %s\n", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKeybindsShow(cmd *cobra.Command, args []string) {
|
func makeProviderWithPath(name, path string) keybinds.Provider {
|
||||||
providerName := args[0]
|
switch name {
|
||||||
registry := keybinds.GetDefaultRegistry()
|
case "hyprland":
|
||||||
|
return providers.NewHyprlandProvider(path)
|
||||||
customPath, _ := cmd.Flags().GetString("path")
|
case "mangowc":
|
||||||
if customPath != "" {
|
return providers.NewMangoWCProvider(path)
|
||||||
var provider keybinds.Provider
|
case "sway":
|
||||||
switch providerName {
|
return providers.NewSwayProvider(path)
|
||||||
case "hyprland":
|
case "niri":
|
||||||
provider = providers.NewHyprlandProvider(customPath)
|
return providers.NewNiriProvider(path)
|
||||||
case "mangowc":
|
default:
|
||||||
provider = providers.NewMangoWCProvider(customPath)
|
return nil
|
||||||
case "sway":
|
|
||||||
provider = providers.NewSwayProvider(customPath)
|
|
||||||
case "niri":
|
|
||||||
provider = providers.NewNiriProvider(customPath)
|
|
||||||
default:
|
|
||||||
log.Fatalf("Provider %s does not support custom path", providerName)
|
|
||||||
}
|
|
||||||
|
|
||||||
sheet, err := provider.GetCheatSheet()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error getting cheatsheet: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := json.MarshalIndent(sheet, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error generating JSON: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(os.Stdout, string(output))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
provider, err := registry.Get(providerName)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error: %v", err)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printCheatSheet(provider keybinds.Provider) {
|
||||||
sheet, err := provider.GetCheatSheet()
|
sheet, err := provider.GetCheatSheet()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error getting cheatsheet: %v", err)
|
log.Fatalf("Error getting cheatsheet: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := json.MarshalIndent(sheet, "", " ")
|
output, err := json.MarshalIndent(sheet, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error generating JSON: %v", err)
|
log.Fatalf("Error generating JSON: %v", err)
|
||||||
}
|
}
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKeybindsShow(cmd *cobra.Command, args []string) {
|
||||||
|
providerName := args[0]
|
||||||
|
customPath, _ := cmd.Flags().GetString("path")
|
||||||
|
|
||||||
|
if customPath != "" {
|
||||||
|
provider := makeProviderWithPath(providerName, customPath)
|
||||||
|
if provider == nil {
|
||||||
|
log.Fatalf("Provider %s does not support custom path", providerName)
|
||||||
|
}
|
||||||
|
printCheatSheet(provider)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := keybinds.GetDefaultRegistry().Get(providerName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
printCheatSheet(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWritableProvider(name string) keybinds.WritableProvider {
|
||||||
|
provider, err := keybinds.GetDefaultRegistry().Get(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
writable, ok := provider.(keybinds.WritableProvider)
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("Provider %s does not support writing keybinds", name)
|
||||||
|
}
|
||||||
|
return writable
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKeybindsSet(cmd *cobra.Command, args []string) {
|
||||||
|
providerName, key, action := args[0], args[1], args[2]
|
||||||
|
writable := getWritableProvider(providerName)
|
||||||
|
|
||||||
|
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
|
||||||
|
_ = writable.RemoveBind(replaceKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := make(map[string]any)
|
||||||
|
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
|
||||||
|
options["allow-when-locked"] = true
|
||||||
|
}
|
||||||
|
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
|
||||||
|
options["cooldown-ms"] = v
|
||||||
|
}
|
||||||
|
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
||||||
|
options["repeat"] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
desc, _ := cmd.Flags().GetString("desc")
|
||||||
|
if err := writable.SetBind(key, action, desc, options); err != nil {
|
||||||
|
log.Fatalf("Error setting keybind: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := json.MarshalIndent(map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"key": key,
|
||||||
|
"action": action,
|
||||||
|
"path": writable.GetOverridePath(),
|
||||||
|
}, "", " ")
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKeybindsRemove(_ *cobra.Command, args []string) {
|
||||||
|
providerName, key := args[0], args[1]
|
||||||
|
writable := getWritableProvider(providerName)
|
||||||
|
|
||||||
|
if err := writable.RemoveBind(key); err != nil {
|
||||||
|
log.Fatalf("Error removing keybind: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := json.MarshalIndent(map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"key": key,
|
||||||
|
"removed": true,
|
||||||
|
}, "", " ")
|
||||||
fmt.Fprintln(os.Stdout, string(output))
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,19 @@ package providers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
|
"github.com/sblinch/kdl-go"
|
||||||
|
"github.com/sblinch/kdl-go/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NiriProvider struct {
|
type NiriProvider struct {
|
||||||
configDir string
|
configDir string
|
||||||
|
dmsBindsIncluded bool
|
||||||
|
parsed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNiriProvider(configDir string) *NiriProvider {
|
func NewNiriProvider(configDir string) *NiriProvider {
|
||||||
@@ -23,8 +28,7 @@ func NewNiriProvider(configDir string) *NiriProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func defaultNiriConfigDir() string {
|
func defaultNiriConfigDir() string {
|
||||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
if configHome := os.Getenv("XDG_CONFIG_HOME"); configHome != "" {
|
||||||
if configHome != "" {
|
|
||||||
return filepath.Join(configHome, "niri")
|
return filepath.Join(configHome, "niri")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,21 +44,40 @@ func (n *NiriProvider) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||||
section, err := ParseNiriKeys(n.configDir)
|
result, err := ParseNiriKeys(n.configDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse niri config: %w", err)
|
return nil, fmt.Errorf("failed to parse niri config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n.dmsBindsIncluded = result.DMSBindsIncluded
|
||||||
|
n.parsed = true
|
||||||
|
|
||||||
categorizedBinds := make(map[string][]keybinds.Keybind)
|
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||||
n.convertSection(section, "", categorizedBinds)
|
n.convertSection(result.Section, "", categorizedBinds)
|
||||||
|
|
||||||
return &keybinds.CheatSheet{
|
return &keybinds.CheatSheet{
|
||||||
Title: "Niri Keybinds",
|
Title: "Niri Keybinds",
|
||||||
Provider: n.Name(),
|
Provider: n.Name(),
|
||||||
Binds: categorizedBinds,
|
Binds: categorizedBinds,
|
||||||
|
DMSBindsIncluded: result.DMSBindsIncluded,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) HasDMSBindsIncluded() bool {
|
||||||
|
if n.parsed {
|
||||||
|
return n.dmsBindsIncluded
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(n.configDir)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
n.dmsBindsIncluded = result.DMSBindsIncluded
|
||||||
|
n.parsed = true
|
||||||
|
return n.dmsBindsIncluded
|
||||||
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) convertSection(section *NiriSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
|
func (n *NiriProvider) convertSection(section *NiriSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
|
||||||
currentSubcat := subcategory
|
currentSubcat := subcategory
|
||||||
if section.Name != "" {
|
if section.Name != "" {
|
||||||
@@ -106,19 +129,19 @@ func (n *NiriProvider) categorizeByAction(action string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string) keybinds.Keybind {
|
func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string) keybinds.Keybind {
|
||||||
key := n.formatKey(kb)
|
|
||||||
desc := kb.Description
|
|
||||||
rawAction := n.formatRawAction(kb.Action, kb.Args)
|
rawAction := n.formatRawAction(kb.Action, kb.Args)
|
||||||
|
|
||||||
if desc == "" {
|
source := "config"
|
||||||
desc = rawAction
|
if strings.Contains(kb.Source, "dms/binds.kdl") {
|
||||||
|
source = "dms"
|
||||||
}
|
}
|
||||||
|
|
||||||
return keybinds.Keybind{
|
return keybinds.Keybind{
|
||||||
Key: key,
|
Key: n.formatKey(kb),
|
||||||
Description: desc,
|
Description: kb.Description,
|
||||||
Action: rawAction,
|
Action: rawAction,
|
||||||
Subcategory: subcategory,
|
Subcategory: subcategory,
|
||||||
|
Source: source,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +149,15 @@ func (n *NiriProvider) formatRawAction(action string, args []string) string {
|
|||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return action
|
return action
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if action == "spawn" && len(args) >= 3 && args[1] == "-c" {
|
||||||
|
switch args[0] {
|
||||||
|
case "sh", "bash":
|
||||||
|
cmd := strings.Join(args[2:], " ")
|
||||||
|
return fmt.Sprintf("spawn %s -c \"%s\"", args[0], strings.ReplaceAll(cmd, "\"", "\\\""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return action + " " + strings.Join(args, " ")
|
return action + " " + strings.Join(args, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,3 +167,308 @@ func (n *NiriProvider) formatKey(kb *NiriKeyBinding) string {
|
|||||||
parts = append(parts, kb.Key)
|
parts = append(parts, kb.Key)
|
||||||
return strings.Join(parts, "+")
|
return strings.Join(parts, "+")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) GetOverridePath() string {
|
||||||
|
return filepath.Join(n.configDir, "dms", "binds.kdl")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) validateAction(action string) error {
|
||||||
|
action = strings.TrimSpace(action)
|
||||||
|
switch {
|
||||||
|
case action == "":
|
||||||
|
return fmt.Errorf("action cannot be empty")
|
||||||
|
case action == "spawn" || action == "spawn ":
|
||||||
|
return fmt.Errorf("spawn command requires arguments")
|
||||||
|
case strings.HasPrefix(action, "spawn "):
|
||||||
|
rest := strings.TrimSpace(strings.TrimPrefix(action, "spawn "))
|
||||||
|
switch rest {
|
||||||
|
case "":
|
||||||
|
return fmt.Errorf("spawn command requires arguments")
|
||||||
|
case "sh -c \"\"", "sh -c ''", "bash -c \"\"", "bash -c ''":
|
||||||
|
return fmt.Errorf("shell command cannot be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) SetBind(key, action, description string, options map[string]any) error {
|
||||||
|
if err := n.validateAction(action); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
overridePath := n.GetOverridePath()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
existingBinds, err := n.loadOverrideBinds()
|
||||||
|
if err != nil {
|
||||||
|
existingBinds = make(map[string]*overrideBind)
|
||||||
|
}
|
||||||
|
|
||||||
|
existingBinds[key] = &overrideBind{
|
||||||
|
Key: key,
|
||||||
|
Action: action,
|
||||||
|
Description: description,
|
||||||
|
Options: options,
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.writeOverrideBinds(existingBinds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) RemoveBind(key string) error {
|
||||||
|
existingBinds, err := n.loadOverrideBinds()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(existingBinds, key)
|
||||||
|
return n.writeOverrideBinds(existingBinds)
|
||||||
|
}
|
||||||
|
|
||||||
|
type overrideBind struct {
|
||||||
|
Key string
|
||||||
|
Action string
|
||||||
|
Description string
|
||||||
|
Options map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
||||||
|
overridePath := n.GetOverridePath()
|
||||||
|
binds := make(map[string]*overrideBind)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(overridePath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return binds, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := NewNiriParser(filepath.Dir(overridePath))
|
||||||
|
parser.currentSource = overridePath
|
||||||
|
|
||||||
|
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, node := range doc.Nodes {
|
||||||
|
if node.Name.String() != "binds" || node.Children == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, child := range node.Children {
|
||||||
|
kb := parser.parseKeybindNode(child, "")
|
||||||
|
if kb == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keyStr := parser.formatBindKey(kb)
|
||||||
|
binds[keyStr] = &overrideBind{
|
||||||
|
Key: keyStr,
|
||||||
|
Action: n.formatRawAction(kb.Action, kb.Args),
|
||||||
|
Description: kb.Description,
|
||||||
|
Options: n.extractOptions(child),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return binds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) extractOptions(node *document.Node) map[string]any {
|
||||||
|
if node.Properties == nil {
|
||||||
|
return make(map[string]any)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := make(map[string]any)
|
||||||
|
if val, ok := node.Properties.Get("repeat"); ok {
|
||||||
|
opts["repeat"] = val.String() == "true"
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("cooldown-ms"); ok {
|
||||||
|
opts["cooldown-ms"] = val.String()
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("allow-when-locked"); ok {
|
||||||
|
opts["allow-when-locked"] = val.String() == "true"
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) isRecentWindowsAction(action string) bool {
|
||||||
|
switch action {
|
||||||
|
case "next-window", "previous-window":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) parseSpawnArgs(s string) []string {
|
||||||
|
var args []string
|
||||||
|
var current strings.Builder
|
||||||
|
var inQuote, escaped bool
|
||||||
|
|
||||||
|
for _, r := range s {
|
||||||
|
switch {
|
||||||
|
case escaped:
|
||||||
|
current.WriteRune(r)
|
||||||
|
escaped = false
|
||||||
|
case r == '\\':
|
||||||
|
escaped = true
|
||||||
|
case r == '"':
|
||||||
|
inQuote = !inQuote
|
||||||
|
case r == ' ' && !inQuote:
|
||||||
|
if current.Len() > 0 {
|
||||||
|
args = append(args, current.String())
|
||||||
|
current.Reset()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
current.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current.Len() > 0 {
|
||||||
|
args = append(args, current.String())
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node {
|
||||||
|
node := document.NewNode()
|
||||||
|
node.SetName(bind.Key)
|
||||||
|
|
||||||
|
if bind.Options != nil {
|
||||||
|
if v, ok := bind.Options["repeat"]; ok && v == false {
|
||||||
|
node.AddProperty("repeat", false, "")
|
||||||
|
}
|
||||||
|
if v, ok := bind.Options["cooldown-ms"]; ok {
|
||||||
|
node.AddProperty("cooldown-ms", v, "")
|
||||||
|
}
|
||||||
|
if v, ok := bind.Options["allow-when-locked"]; ok && v == true {
|
||||||
|
node.AddProperty("allow-when-locked", true, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bind.Description != "" {
|
||||||
|
node.AddProperty("hotkey-overlay-title", bind.Description, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
actionNode := n.buildActionNode(bind.Action)
|
||||||
|
node.AddNode(actionNode)
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) buildActionNode(action string) *document.Node {
|
||||||
|
action = strings.TrimSpace(action)
|
||||||
|
node := document.NewNode()
|
||||||
|
|
||||||
|
if !strings.HasPrefix(action, "spawn ") {
|
||||||
|
node.SetName(action)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
node.SetName("spawn")
|
||||||
|
args := n.parseSpawnArgs(strings.TrimPrefix(action, "spawn "))
|
||||||
|
for _, arg := range args {
|
||||||
|
node.AddArgument(arg, "")
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error {
|
||||||
|
overridePath := n.GetOverridePath()
|
||||||
|
content := n.generateBindsContent(binds)
|
||||||
|
|
||||||
|
if err := n.validateBindsContent(content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(overridePath, []byte(content), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string {
|
||||||
|
if len(binds) == 0 {
|
||||||
|
return "binds {}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
var regularBinds, recentWindowsBinds []*overrideBind
|
||||||
|
for _, bind := range binds {
|
||||||
|
switch {
|
||||||
|
case n.isRecentWindowsAction(bind.Action):
|
||||||
|
recentWindowsBinds = append(recentWindowsBinds, bind)
|
||||||
|
default:
|
||||||
|
regularBinds = append(regularBinds, bind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
sb.WriteString("binds {\n")
|
||||||
|
for _, bind := range regularBinds {
|
||||||
|
n.writeBindNode(&sb, bind, " ")
|
||||||
|
}
|
||||||
|
sb.WriteString("}\n")
|
||||||
|
|
||||||
|
if len(recentWindowsBinds) > 0 {
|
||||||
|
sb.WriteString("\nrecent-windows {\n")
|
||||||
|
sb.WriteString(" binds {\n")
|
||||||
|
for _, bind := range recentWindowsBinds {
|
||||||
|
n.writeBindNode(&sb, bind, " ")
|
||||||
|
}
|
||||||
|
sb.WriteString(" }\n")
|
||||||
|
sb.WriteString("}\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) writeBindNode(sb *strings.Builder, bind *overrideBind, indent string) {
|
||||||
|
node := n.buildBindNode(bind)
|
||||||
|
|
||||||
|
sb.WriteString(indent)
|
||||||
|
sb.WriteString(node.Name.String())
|
||||||
|
|
||||||
|
if node.Properties.Exist() {
|
||||||
|
sb.WriteString(" ")
|
||||||
|
sb.WriteString(strings.TrimLeft(node.Properties.String(), " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(" { ")
|
||||||
|
if len(node.Children) > 0 {
|
||||||
|
child := node.Children[0]
|
||||||
|
sb.WriteString(child.Name.String())
|
||||||
|
for _, arg := range child.Arguments {
|
||||||
|
sb.WriteString(" ")
|
||||||
|
n.writeQuotedArg(sb, arg.ValueString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString("; }\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) writeQuotedArg(sb *strings.Builder, val string) {
|
||||||
|
sb.WriteString("\"")
|
||||||
|
sb.WriteString(strings.ReplaceAll(val, "\"", "\\\""))
|
||||||
|
sb.WriteString("\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) validateBindsContent(content string) error {
|
||||||
|
tmpFile, err := os.CreateTemp("", "dms-binds-*.kdl")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp file: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpFile.Name())
|
||||||
|
|
||||||
|
if _, err := tmpFile.WriteString(content); err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
return fmt.Errorf("failed to write temp file: %w", err)
|
||||||
|
}
|
||||||
|
tmpFile.Close()
|
||||||
|
|
||||||
|
cmd := exec.Command("niri", "validate", "-c", tmpFile.Name())
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid config: %s", strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type NiriKeyBinding struct {
|
|||||||
Action string
|
Action string
|
||||||
Args []string
|
Args []string
|
||||||
Description string
|
Description string
|
||||||
|
Source string
|
||||||
}
|
}
|
||||||
|
|
||||||
type NiriSection struct {
|
type NiriSection struct {
|
||||||
@@ -25,10 +26,12 @@ type NiriSection struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NiriParser struct {
|
type NiriParser struct {
|
||||||
configDir string
|
configDir string
|
||||||
processedFiles map[string]bool
|
processedFiles map[string]bool
|
||||||
bindMap map[string]*NiriKeyBinding
|
bindMap map[string]*NiriKeyBinding
|
||||||
bindOrder []string
|
bindOrder []string
|
||||||
|
currentSource string
|
||||||
|
dmsBindsIncluded bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNiriParser(configDir string) *NiriParser {
|
func NewNiriParser(configDir string) *NiriParser {
|
||||||
@@ -37,6 +40,7 @@ func NewNiriParser(configDir string) *NiriParser {
|
|||||||
processedFiles: make(map[string]bool),
|
processedFiles: make(map[string]bool),
|
||||||
bindMap: make(map[string]*NiriKeyBinding),
|
bindMap: make(map[string]*NiriKeyBinding),
|
||||||
bindOrder: []string{},
|
bindOrder: []string{},
|
||||||
|
currentSource: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +105,7 @@ func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, erro
|
|||||||
Name: sectionName,
|
Name: sectionName,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.currentSource = absPath
|
||||||
baseDir := filepath.Dir(absPath)
|
baseDir := filepath.Dir(absPath)
|
||||||
p.processNodes(doc.Nodes, section, baseDir)
|
p.processNodes(doc.Nodes, section, baseDir)
|
||||||
|
|
||||||
@@ -127,14 +132,14 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
includePath := node.Arguments[0].String()
|
includePath := strings.Trim(node.Arguments[0].String(), "\"")
|
||||||
includePath = strings.Trim(includePath, "\"")
|
if includePath == "dms/binds.kdl" || strings.HasSuffix(includePath, "/dms/binds.kdl") {
|
||||||
|
p.dmsBindsIncluded = true
|
||||||
|
}
|
||||||
|
|
||||||
var fullPath string
|
fullPath := filepath.Join(baseDir, includePath)
|
||||||
if filepath.IsAbs(includePath) {
|
if filepath.IsAbs(includePath) {
|
||||||
fullPath = includePath
|
fullPath = includePath
|
||||||
} else {
|
|
||||||
fullPath = filepath.Join(baseDir, includePath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
includedSection, err := p.parseFile(fullPath, "")
|
includedSection, err := p.parseFile(fullPath, "")
|
||||||
@@ -145,6 +150,10 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba
|
|||||||
section.Children = append(section.Children, includedSection.Children...)
|
section.Children = append(section.Children, includedSection.Children...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) HasDMSBindsIncluded() bool {
|
||||||
|
return p.dmsBindsIncluded
|
||||||
|
}
|
||||||
|
|
||||||
func (p *NiriParser) handleRecentWindows(node *document.Node, section *NiriSection) {
|
func (p *NiriParser) handleRecentWindows(node *document.Node, section *NiriSection) {
|
||||||
if node.Children == nil {
|
if node.Children == nil {
|
||||||
return
|
return
|
||||||
@@ -172,7 +181,7 @@ func (p *NiriParser) extractBinds(node *document.Node, section *NiriSection, sub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NiriParser) parseKeybindNode(node *document.Node, subcategory string) *NiriKeyBinding {
|
func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBinding {
|
||||||
keyCombo := node.Name.String()
|
keyCombo := node.Name.String()
|
||||||
if keyCombo == "" {
|
if keyCombo == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -182,19 +191,18 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, subcategory string) *
|
|||||||
|
|
||||||
var action string
|
var action string
|
||||||
var args []string
|
var args []string
|
||||||
|
|
||||||
if len(node.Children) > 0 {
|
if len(node.Children) > 0 {
|
||||||
actionNode := node.Children[0]
|
actionNode := node.Children[0]
|
||||||
action = actionNode.Name.String()
|
action = actionNode.Name.String()
|
||||||
for _, arg := range actionNode.Arguments {
|
for _, arg := range actionNode.Arguments {
|
||||||
args = append(args, strings.Trim(arg.String(), "\""))
|
args = append(args, arg.ValueString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
description := ""
|
var description string
|
||||||
if node.Properties != nil {
|
if node.Properties != nil {
|
||||||
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
|
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
|
||||||
description = strings.Trim(val.String(), "\"")
|
description = val.ValueString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,26 +212,36 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, subcategory string) *
|
|||||||
Action: action,
|
Action: action,
|
||||||
Args: args,
|
Args: args,
|
||||||
Description: description,
|
Description: description,
|
||||||
|
Source: p.currentSource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) {
|
func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) {
|
||||||
parts := strings.Split(combo, "+")
|
parts := strings.Split(combo, "+")
|
||||||
if len(parts) == 0 {
|
|
||||||
|
switch len(parts) {
|
||||||
|
case 0:
|
||||||
return nil, combo
|
return nil, combo
|
||||||
}
|
case 1:
|
||||||
|
|
||||||
if len(parts) == 1 {
|
|
||||||
return nil, parts[0]
|
return nil, parts[0]
|
||||||
|
default:
|
||||||
|
return parts[:len(parts)-1], parts[len(parts)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
mods := parts[:len(parts)-1]
|
|
||||||
key := parts[len(parts)-1]
|
|
||||||
|
|
||||||
return mods, key
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseNiriKeys(configDir string) (*NiriSection, error) {
|
type NiriParseResult struct {
|
||||||
|
Section *NiriSection
|
||||||
|
DMSBindsIncluded bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseNiriKeys(configDir string) (*NiriParseResult, error) {
|
||||||
parser := NewNiriParser(configDir)
|
parser := NewNiriParser(configDir)
|
||||||
return parser.Parse()
|
section, err := parser.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &NiriParseResult{
|
||||||
|
Section: section,
|
||||||
|
DMSBindsIncluded: parser.HasDMSBindsIncluded(),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,20 +57,20 @@ func TestNiriParseBasicBinds(t *testing.T) {
|
|||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 3 {
|
if len(result.Section.Keybinds) != 3 {
|
||||||
t.Errorf("Expected 3 keybinds, got %d", len(section.Keybinds))
|
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
|
|
||||||
foundClose := false
|
foundClose := false
|
||||||
foundFullscreen := false
|
foundFullscreen := false
|
||||||
foundTerminal := false
|
foundTerminal := false
|
||||||
|
|
||||||
for _, kb := range section.Keybinds {
|
for _, kb := range result.Section.Keybinds {
|
||||||
switch kb.Action {
|
switch kb.Action {
|
||||||
case "close-window":
|
case "close-window":
|
||||||
foundClose = true
|
foundClose = true
|
||||||
@@ -116,19 +116,19 @@ func TestNiriParseRecentWindows(t *testing.T) {
|
|||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 2 {
|
if len(result.Section.Keybinds) != 2 {
|
||||||
t.Errorf("Expected 2 keybinds from recent-windows, got %d", len(section.Keybinds))
|
t.Errorf("Expected 2 keybinds from recent-windows, got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
|
|
||||||
foundNext := false
|
foundNext := false
|
||||||
foundPrev := false
|
foundPrev := false
|
||||||
|
|
||||||
for _, kb := range section.Keybinds {
|
for _, kb := range result.Section.Keybinds {
|
||||||
switch kb.Action {
|
switch kb.Action {
|
||||||
case "next-window":
|
case "next-window":
|
||||||
foundNext = true
|
foundNext = true
|
||||||
@@ -172,13 +172,13 @@ include "dms/binds.kdl"
|
|||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 2 {
|
if len(result.Section.Keybinds) != 2 {
|
||||||
t.Errorf("Expected 2 keybinds (1 main + 1 include), got %d", len(section.Keybinds))
|
t.Errorf("Expected 2 keybinds (1 main + 1 include), got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,17 +209,17 @@ include "dms/binds.kdl"
|
|||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 1 {
|
if len(result.Section.Keybinds) != 1 {
|
||||||
t.Errorf("Expected 1 keybind (later overrides earlier), got %d", len(section.Keybinds))
|
t.Errorf("Expected 1 keybind (later overrides earlier), got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) > 0 {
|
if len(result.Section.Keybinds) > 0 {
|
||||||
kb := section.Keybinds[0]
|
kb := result.Section.Keybinds[0]
|
||||||
if kb.Description != "Override Terminal" {
|
if kb.Description != "Override Terminal" {
|
||||||
t.Errorf("Expected description 'Override Terminal' (from include), got %q", kb.Description)
|
t.Errorf("Expected description 'Override Terminal' (from include), got %q", kb.Description)
|
||||||
}
|
}
|
||||||
@@ -253,13 +253,13 @@ include "config.kdl"
|
|||||||
t.Fatalf("Failed to write other config: %v", err)
|
t.Fatalf("Failed to write other config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed (should handle circular includes): %v", err)
|
t.Fatalf("ParseNiriKeys failed (should handle circular includes): %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 2 {
|
if len(result.Section.Keybinds) != 2 {
|
||||||
t.Errorf("Expected 2 keybinds (circular include handled), got %d", len(section.Keybinds))
|
t.Errorf("Expected 2 keybinds (circular include handled), got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,13 +276,13 @@ include "nonexistent/file.kdl"
|
|||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed (should skip missing include): %v", err)
|
t.Fatalf("ParseNiriKeys failed (should skip missing include): %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 1 {
|
if len(result.Section.Keybinds) != 1 {
|
||||||
t.Errorf("Expected 1 keybind (missing include skipped), got %d", len(section.Keybinds))
|
t.Errorf("Expected 1 keybind (missing include skipped), got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,13 +305,13 @@ input {
|
|||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 0 {
|
if len(result.Section.Keybinds) != 0 {
|
||||||
t.Errorf("Expected 0 keybinds, got %d", len(section.Keybinds))
|
t.Errorf("Expected 0 keybinds, got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,18 +352,18 @@ func TestNiriBindOverrideBehavior(t *testing.T) {
|
|||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 3 {
|
if len(result.Section.Keybinds) != 3 {
|
||||||
t.Fatalf("Expected 3 unique keybinds, got %d", len(section.Keybinds))
|
t.Fatalf("Expected 3 unique keybinds, got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
|
|
||||||
var modT *NiriKeyBinding
|
var modT *NiriKeyBinding
|
||||||
for i := range section.Keybinds {
|
for i := range result.Section.Keybinds {
|
||||||
kb := §ion.Keybinds[i]
|
kb := &result.Section.Keybinds[i]
|
||||||
if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" && kb.Key == "T" {
|
if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" && kb.Key == "T" {
|
||||||
modT = kb
|
modT = kb
|
||||||
break
|
break
|
||||||
@@ -416,18 +416,18 @@ binds {
|
|||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 4 {
|
if len(result.Section.Keybinds) != 4 {
|
||||||
t.Errorf("Expected 4 unique keybinds, got %d", len(section.Keybinds))
|
t.Errorf("Expected 4 unique keybinds, got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
|
|
||||||
bindMap := make(map[string]*NiriKeyBinding)
|
bindMap := make(map[string]*NiriKeyBinding)
|
||||||
for i := range section.Keybinds {
|
for i := range result.Section.Keybinds {
|
||||||
kb := §ion.Keybinds[i]
|
kb := &result.Section.Keybinds[i]
|
||||||
key := ""
|
key := ""
|
||||||
for _, m := range kb.Mods {
|
for _, m := range kb.Mods {
|
||||||
key += m + "+"
|
key += m + "+"
|
||||||
@@ -475,16 +475,16 @@ func TestNiriParseMultipleArgs(t *testing.T) {
|
|||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 1 {
|
if len(result.Section.Keybinds) != 1 {
|
||||||
t.Fatalf("Expected 1 keybind, got %d", len(section.Keybinds))
|
t.Fatalf("Expected 1 keybind, got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
|
|
||||||
kb := section.Keybinds[0]
|
kb := result.Section.Keybinds[0]
|
||||||
if len(kb.Args) != 5 {
|
if len(kb.Args) != 5 {
|
||||||
t.Errorf("Expected 5 args, got %d: %v", len(kb.Args), kb.Args)
|
t.Errorf("Expected 5 args, got %d: %v", len(kb.Args), kb.Args)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,144 @@ func TestNiriDefaultConfigDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNiriGenerateBindsContent(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
binds map[string]*overrideBind
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty binds",
|
||||||
|
binds: map[string]*overrideBind{},
|
||||||
|
expected: "binds {}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple spawn bind",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Mod+T": {
|
||||||
|
Key: "Mod+T",
|
||||||
|
Action: "spawn kitty",
|
||||||
|
Description: "Open Terminal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spawn with multiple args",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Mod+Space": {
|
||||||
|
Key: "Mod+Space",
|
||||||
|
Action: `spawn "dms" "ipc" "call" "spotlight" "toggle"`,
|
||||||
|
Description: "Application Launcher",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bind with allow-when-locked",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"XF86AudioMute": {
|
||||||
|
Key: "XF86AudioMute",
|
||||||
|
Action: `spawn "dms" "ipc" "call" "audio" "mute"`,
|
||||||
|
Options: map[string]any{"allow-when-locked": true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
XF86AudioMute allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "mute"; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple action without args",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Mod+Q": {
|
||||||
|
Key: "Mod+Q",
|
||||||
|
Action: "close-window",
|
||||||
|
Description: "Close Window",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Mod+Q hotkey-overlay-title="Close Window" { close-window; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "recent-windows action",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Alt+Tab": {
|
||||||
|
Key: "Alt+Tab",
|
||||||
|
Action: "next-window",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
}
|
||||||
|
|
||||||
|
recent-windows {
|
||||||
|
binds {
|
||||||
|
Alt+Tab { next-window; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := provider.generateBindsContent(tt.binds)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("generateBindsContent() =\n%q\nwant:\n%q", result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriGenerateBindsContentRoundTrip(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
binds := map[string]*overrideBind{
|
||||||
|
"Mod+Space": {
|
||||||
|
Key: "Mod+Space",
|
||||||
|
Action: `spawn "dms" "ipc" "call" "spotlight" "toggle"`,
|
||||||
|
Description: "Application Launcher",
|
||||||
|
},
|
||||||
|
"XF86AudioMute": {
|
||||||
|
Key: "XF86AudioMute",
|
||||||
|
Action: `spawn "dms" "ipc" "call" "audio" "mute"`,
|
||||||
|
Options: map[string]any{"allow-when-locked": true},
|
||||||
|
},
|
||||||
|
"Mod+Q": {
|
||||||
|
Key: "Mod+Q",
|
||||||
|
Action: "close-window",
|
||||||
|
Description: "Close Window",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
content := provider.generateBindsContent(binds)
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write temp file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse generated content: %v\nContent was:\n%s", err, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Section.Keybinds) != 3 {
|
||||||
|
t.Errorf("Expected 3 keybinds after round-trip, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNiriProviderWithRealWorldConfig(t *testing.T) {
|
func TestNiriProviderWithRealWorldConfig(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "config.kdl")
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|||||||
@@ -5,15 +5,24 @@ type Keybind struct {
|
|||||||
Description string `json:"desc"`
|
Description string `json:"desc"`
|
||||||
Action string `json:"action,omitempty"`
|
Action string `json:"action,omitempty"`
|
||||||
Subcategory string `json:"subcat,omitempty"`
|
Subcategory string `json:"subcat,omitempty"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CheatSheet struct {
|
type CheatSheet struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Binds map[string][]Keybind `json:"binds"`
|
Binds map[string][]Keybind `json:"binds"`
|
||||||
|
DMSBindsIncluded bool `json:"dmsBindsIncluded"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
Name() string
|
Name() string
|
||||||
GetCheatSheet() (*CheatSheet, error)
|
GetCheatSheet() (*CheatSheet, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WritableProvider interface {
|
||||||
|
Provider
|
||||||
|
SetBind(key, action, description string, options map[string]any) error
|
||||||
|
RemoveBind(key string) error
|
||||||
|
GetOverridePath() string
|
||||||
|
}
|
||||||
|
|||||||
@@ -329,79 +329,79 @@ Item {
|
|||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function toggle(provider: string): string {
|
function toggle(provider: string): string {
|
||||||
if (!provider) {
|
if (!provider)
|
||||||
return "ERROR: No provider specified";
|
return "ERROR: No provider specified";
|
||||||
}
|
|
||||||
|
|
||||||
KeybindsService.loadProvider(provider);
|
KeybindsService.currentProvider = provider;
|
||||||
|
KeybindsService.loadBinds();
|
||||||
root.hyprKeybindsModalLoader.active = true;
|
root.hyprKeybindsModalLoader.active = true;
|
||||||
|
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
if (!root.hyprKeybindsModalLoader.item)
|
||||||
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
|
||||||
root.hyprKeybindsModalLoader.item.close();
|
|
||||||
} else {
|
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
||||||
root.hyprKeybindsModalLoader.item.open();
|
root.hyprKeybindsModalLoader.item.close();
|
||||||
}
|
} else {
|
||||||
return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`;
|
root.hyprKeybindsModalLoader.item.open();
|
||||||
}
|
}
|
||||||
return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
|
return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleWithPath(provider: string, path: string): string {
|
function toggleWithPath(provider: string, path: string): string {
|
||||||
if (!provider) {
|
if (!provider)
|
||||||
return "ERROR: No provider specified";
|
return "ERROR: No provider specified";
|
||||||
}
|
|
||||||
|
|
||||||
KeybindsService.loadProviderWithPath(provider, path);
|
KeybindsService.currentProvider = provider;
|
||||||
|
KeybindsService.loadBinds();
|
||||||
root.hyprKeybindsModalLoader.active = true;
|
root.hyprKeybindsModalLoader.active = true;
|
||||||
|
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
if (!root.hyprKeybindsModalLoader.item)
|
||||||
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
|
||||||
root.hyprKeybindsModalLoader.item.close();
|
|
||||||
} else {
|
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
||||||
root.hyprKeybindsModalLoader.item.open();
|
root.hyprKeybindsModalLoader.item.close();
|
||||||
}
|
} else {
|
||||||
return `KEYBINDS_TOGGLE_SUCCESS: ${provider} (${path})`;
|
root.hyprKeybindsModalLoader.item.open();
|
||||||
}
|
}
|
||||||
return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
|
return `KEYBINDS_TOGGLE_SUCCESS: ${provider} (${path})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function open(provider: string): string {
|
function open(provider: string): string {
|
||||||
if (!provider) {
|
if (!provider)
|
||||||
return "ERROR: No provider specified";
|
return "ERROR: No provider specified";
|
||||||
}
|
|
||||||
|
|
||||||
KeybindsService.loadProvider(provider);
|
KeybindsService.currentProvider = provider;
|
||||||
|
KeybindsService.loadBinds();
|
||||||
root.hyprKeybindsModalLoader.active = true;
|
root.hyprKeybindsModalLoader.active = true;
|
||||||
|
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
if (!root.hyprKeybindsModalLoader.item)
|
||||||
root.hyprKeybindsModalLoader.item.open();
|
return `KEYBINDS_OPEN_FAILED: ${provider}`;
|
||||||
return `KEYBINDS_OPEN_SUCCESS: ${provider}`;
|
|
||||||
}
|
root.hyprKeybindsModalLoader.item.open();
|
||||||
return `KEYBINDS_OPEN_FAILED: ${provider}`;
|
return `KEYBINDS_OPEN_SUCCESS: ${provider}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openWithPath(provider: string, path: string): string {
|
function openWithPath(provider: string, path: string): string {
|
||||||
if (!provider) {
|
if (!provider)
|
||||||
return "ERROR: No provider specified";
|
return "ERROR: No provider specified";
|
||||||
}
|
|
||||||
|
|
||||||
KeybindsService.loadProviderWithPath(provider, path);
|
KeybindsService.currentProvider = provider;
|
||||||
|
KeybindsService.loadBinds();
|
||||||
root.hyprKeybindsModalLoader.active = true;
|
root.hyprKeybindsModalLoader.active = true;
|
||||||
|
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
if (!root.hyprKeybindsModalLoader.item)
|
||||||
root.hyprKeybindsModalLoader.item.open();
|
return `KEYBINDS_OPEN_FAILED: ${provider}`;
|
||||||
return `KEYBINDS_OPEN_SUCCESS: ${provider} (${path})`;
|
|
||||||
}
|
root.hyprKeybindsModalLoader.item.open();
|
||||||
return `KEYBINDS_OPEN_FAILED: ${provider}`;
|
return `KEYBINDS_OPEN_SUCCESS: ${provider} (${path})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function close(): string {
|
function close(): string {
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
if (!root.hyprKeybindsModalLoader.item)
|
||||||
root.hyprKeybindsModalLoader.item.close();
|
return "KEYBINDS_CLOSE_FAILED";
|
||||||
return "KEYBINDS_CLOSE_SUCCESS";
|
|
||||||
}
|
root.hyprKeybindsModalLoader.item.close();
|
||||||
return "KEYBINDS_CLOSE_FAILED";
|
return "KEYBINDS_CLOSE_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
target: "keybinds"
|
target: "keybinds"
|
||||||
@@ -409,44 +409,48 @@ Item {
|
|||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function openBinds(): string {
|
function openBinds(): string {
|
||||||
if (!CompositorService.isHyprland) {
|
if (!CompositorService.isHyprland)
|
||||||
return "HYPR_NOT_AVAILABLE";
|
return "HYPR_NOT_AVAILABLE";
|
||||||
}
|
|
||||||
KeybindsService.loadProvider("hyprland");
|
KeybindsService.currentProvider = "hyprland";
|
||||||
|
KeybindsService.loadBinds();
|
||||||
root.hyprKeybindsModalLoader.active = true;
|
root.hyprKeybindsModalLoader.active = true;
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
|
||||||
root.hyprKeybindsModalLoader.item.open();
|
if (!root.hyprKeybindsModalLoader.item)
|
||||||
return "HYPR_KEYBINDS_OPEN_SUCCESS";
|
return "HYPR_KEYBINDS_OPEN_FAILED";
|
||||||
}
|
|
||||||
return "HYPR_KEYBINDS_OPEN_FAILED";
|
root.hyprKeybindsModalLoader.item.open();
|
||||||
|
return "HYPR_KEYBINDS_OPEN_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeBinds(): string {
|
function closeBinds(): string {
|
||||||
if (!CompositorService.isHyprland) {
|
if (!CompositorService.isHyprland)
|
||||||
return "HYPR_NOT_AVAILABLE";
|
return "HYPR_NOT_AVAILABLE";
|
||||||
}
|
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
if (!root.hyprKeybindsModalLoader.item)
|
||||||
root.hyprKeybindsModalLoader.item.close();
|
return "HYPR_KEYBINDS_CLOSE_FAILED";
|
||||||
return "HYPR_KEYBINDS_CLOSE_SUCCESS";
|
|
||||||
}
|
root.hyprKeybindsModalLoader.item.close();
|
||||||
return "HYPR_KEYBINDS_CLOSE_FAILED";
|
return "HYPR_KEYBINDS_CLOSE_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleBinds(): string {
|
function toggleBinds(): string {
|
||||||
if (!CompositorService.isHyprland) {
|
if (!CompositorService.isHyprland)
|
||||||
return "HYPR_NOT_AVAILABLE";
|
return "HYPR_NOT_AVAILABLE";
|
||||||
}
|
|
||||||
KeybindsService.loadProvider("hyprland");
|
KeybindsService.currentProvider = "hyprland";
|
||||||
|
KeybindsService.loadBinds();
|
||||||
root.hyprKeybindsModalLoader.active = true;
|
root.hyprKeybindsModalLoader.active = true;
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
|
||||||
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
if (!root.hyprKeybindsModalLoader.item)
|
||||||
root.hyprKeybindsModalLoader.item.close();
|
return "HYPR_KEYBINDS_TOGGLE_FAILED";
|
||||||
} else {
|
|
||||||
root.hyprKeybindsModalLoader.item.open();
|
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
||||||
}
|
root.hyprKeybindsModalLoader.item.close();
|
||||||
return "HYPR_KEYBINDS_TOGGLE_SUCCESS";
|
} else {
|
||||||
|
root.hyprKeybindsModalLoader.item.open();
|
||||||
}
|
}
|
||||||
return "HYPR_KEYBINDS_TOGGLE_FAILED";
|
return "HYPR_KEYBINDS_TOGGLE_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleOverview(): string {
|
function toggleOverview(): string {
|
||||||
@@ -490,60 +494,108 @@ Item {
|
|||||||
|
|
||||||
function getBarConfig(selector: string, value: string): var {
|
function getBarConfig(selector: string, value: string): var {
|
||||||
const barSelectors = ["id", "name", "index"];
|
const barSelectors = ["id", "name", "index"];
|
||||||
if (!barSelectors.includes(selector)) return { error: "BAR_INVALID_SELECTOR" };
|
if (!barSelectors.includes(selector))
|
||||||
|
return {
|
||||||
|
error: "BAR_INVALID_SELECTOR"
|
||||||
|
};
|
||||||
const index = selector === "index" ? Number(value) : SettingsData.barConfigs.findIndex(bar => bar[selector] == value);
|
const index = selector === "index" ? Number(value) : SettingsData.barConfigs.findIndex(bar => bar[selector] == value);
|
||||||
const barConfig = SettingsData.barConfigs?.[index];
|
const barConfig = SettingsData.barConfigs?.[index];
|
||||||
if (!barConfig) return { error: "BAR_NOT_FOUND" };
|
if (!barConfig)
|
||||||
return { barConfig };
|
return {
|
||||||
|
error: "BAR_NOT_FOUND"
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
barConfig
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function reveal(selector: string, value: string): string {
|
function reveal(selector: string, value: string): string {
|
||||||
const { barConfig, error } = getBarConfig(selector, value);
|
const {
|
||||||
if (error) return error;
|
barConfig,
|
||||||
SettingsData.updateBarConfig(barConfig.id, {visible: true});
|
error
|
||||||
|
} = getBarConfig(selector, value);
|
||||||
|
if (error)
|
||||||
|
return error;
|
||||||
|
SettingsData.updateBarConfig(barConfig.id, {
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
return "BAR_SHOW_SUCCESS";
|
return "BAR_SHOW_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide(selector: string, value: string): string {
|
function hide(selector: string, value: string): string {
|
||||||
const { barConfig, error } = getBarConfig(selector, value);
|
const {
|
||||||
if (error) return error;
|
barConfig,
|
||||||
SettingsData.updateBarConfig(barConfig.id, {visible: false});
|
error
|
||||||
|
} = getBarConfig(selector, value);
|
||||||
|
if (error)
|
||||||
|
return error;
|
||||||
|
SettingsData.updateBarConfig(barConfig.id, {
|
||||||
|
visible: false
|
||||||
|
});
|
||||||
return "BAR_HIDE_SUCCESS";
|
return "BAR_HIDE_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle(selector: string, value: string): string {
|
function toggle(selector: string, value: string): string {
|
||||||
const { barConfig, error } = getBarConfig(selector, value);
|
const {
|
||||||
if (error) return error;
|
barConfig,
|
||||||
SettingsData.updateBarConfig(barConfig.id, {visible: !barConfig.visible});
|
error
|
||||||
|
} = getBarConfig(selector, value);
|
||||||
|
if (error)
|
||||||
|
return error;
|
||||||
|
SettingsData.updateBarConfig(barConfig.id, {
|
||||||
|
visible: !barConfig.visible
|
||||||
|
});
|
||||||
return !barConfig.visible ? "BAR_SHOW_SUCCESS" : "BAR_HIDE_SUCCESS";
|
return !barConfig.visible ? "BAR_SHOW_SUCCESS" : "BAR_HIDE_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
function status(selector: string, value: string): string {
|
function status(selector: string, value: string): string {
|
||||||
const { barConfig, error } = getBarConfig(selector, value);
|
const {
|
||||||
if (error) return error;
|
barConfig,
|
||||||
|
error
|
||||||
|
} = getBarConfig(selector, value);
|
||||||
|
if (error)
|
||||||
|
return error;
|
||||||
return barConfig.visible ? "visible" : "hidden";
|
return barConfig.visible ? "visible" : "hidden";
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoHide(selector: string, value: string): string {
|
function autoHide(selector: string, value: string): string {
|
||||||
const { barConfig, error } = getBarConfig(selector, value);
|
const {
|
||||||
if (error) return error;
|
barConfig,
|
||||||
SettingsData.updateBarConfig(barConfig.id, {autoHide: true});
|
error
|
||||||
|
} = getBarConfig(selector, value);
|
||||||
|
if (error)
|
||||||
|
return error;
|
||||||
|
SettingsData.updateBarConfig(barConfig.id, {
|
||||||
|
autoHide: true
|
||||||
|
});
|
||||||
return "BAR_AUTO_HIDE_SUCCESS";
|
return "BAR_AUTO_HIDE_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
function manualHide(selector: string, value: string): string {
|
function manualHide(selector: string, value: string): string {
|
||||||
const { barConfig, error } = getBarConfig(selector, value);
|
const {
|
||||||
if (error) return error;
|
barConfig,
|
||||||
SettingsData.updateBarConfig(barConfig.id, {autoHide: false});
|
error
|
||||||
|
} = getBarConfig(selector, value);
|
||||||
|
if (error)
|
||||||
|
return error;
|
||||||
|
SettingsData.updateBarConfig(barConfig.id, {
|
||||||
|
autoHide: false
|
||||||
|
});
|
||||||
return "BAR_MANUAL_HIDE_SUCCESS";
|
return "BAR_MANUAL_HIDE_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAutoHide(selector: string, value: string): string {
|
function toggleAutoHide(selector: string, value: string): string {
|
||||||
const { barConfig, error } = getBarConfig(selector, value);
|
const {
|
||||||
if (error) return error;
|
barConfig,
|
||||||
SettingsData.updateBarConfig(barConfig.id, {autoHide: !barConfig.autoHide});
|
error
|
||||||
return barConfig.autoHide ? "BAR_MANUAL_HIDE_SUCCESS": "BAR_AUTO_HIDE_SUCCESS";
|
} = getBarConfig(selector, value);
|
||||||
|
if (error)
|
||||||
|
return error;
|
||||||
|
SettingsData.updateBarConfig(barConfig.id, {
|
||||||
|
autoHide: !barConfig.autoHide
|
||||||
|
});
|
||||||
|
return barConfig.autoHide ? "BAR_MANUAL_HIDE_SUCCESS" : "BAR_AUTO_HIDE_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
target: "bar"
|
target: "bar"
|
||||||
@@ -570,20 +622,20 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function autoHide(): string {
|
function autoHide(): string {
|
||||||
SettingsData.dockAutoHide = true
|
SettingsData.dockAutoHide = true;
|
||||||
SettingsData.saveSettings()
|
SettingsData.saveSettings();
|
||||||
return "BAR_AUTO_HIDE_SUCCESS";
|
return "BAR_AUTO_HIDE_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
function manualHide(): string {
|
function manualHide(): string {
|
||||||
SettingsData.dockAutoHide = false
|
SettingsData.dockAutoHide = false;
|
||||||
SettingsData.saveSettings()
|
SettingsData.saveSettings();
|
||||||
return "BAR_MANUAL_HIDE_SUCCESS";
|
return "BAR_MANUAL_HIDE_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAutoHide(): string {
|
function toggleAutoHide(): string {
|
||||||
SettingsData.dockAutoHide = !SettingsData.dockAutoHide
|
SettingsData.dockAutoHide = !SettingsData.dockAutoHide;
|
||||||
SettingsData.saveSettings()
|
SettingsData.saveSettings();
|
||||||
return SettingsData.dockAutoHide ? "BAR_AUTO_HIDE_SUCCESS" : "BAR_MANUAL_HIDE_SUCCESS";
|
return SettingsData.dockAutoHide ? "BAR_AUTO_HIDE_SUCCESS" : "BAR_MANUAL_HIDE_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,49 +664,51 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function get(key: string): string {
|
function get(key: string): string {
|
||||||
return JSON.stringify(SettingsData?.[key])
|
return JSON.stringify(SettingsData?.[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function set(key: string, value: string): string {
|
function set(key: string, value: string): string {
|
||||||
|
|
||||||
if (!(key in SettingsData)) {
|
if (!(key in SettingsData)) {
|
||||||
console.warn("Cannot set property, not found:", key)
|
console.warn("Cannot set property, not found:", key);
|
||||||
return "SETTINGS_INVALID_KEY"
|
return "SETTINGS_INVALID_KEY";
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeName = typeof SettingsData?.[key]
|
const typeName = typeof SettingsData?.[key];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (typeName) {
|
switch (typeName) {
|
||||||
case "boolean":
|
case "boolean":
|
||||||
if (value === "true" || value === "false") value = (value === "true")
|
if (value === "true" || value === "false")
|
||||||
else throw `${value} is not a Boolean`
|
value = (value === "true");
|
||||||
break
|
else
|
||||||
|
throw `${value} is not a Boolean`;
|
||||||
|
break;
|
||||||
case "number":
|
case "number":
|
||||||
value = Number(value)
|
value = Number(value);
|
||||||
if (isNaN(value)) throw `${value} is not a Number`
|
if (isNaN(value))
|
||||||
break
|
throw `${value} is not a Number`;
|
||||||
|
break;
|
||||||
case "string":
|
case "string":
|
||||||
value = String(value)
|
value = String(value);
|
||||||
break
|
break;
|
||||||
case "object":
|
case "object":
|
||||||
// NOTE: Parsing lists is messed up upstream and not sure if we want
|
// NOTE: Parsing lists is messed up upstream and not sure if we want
|
||||||
// to make sure objects are well structured or just let people set
|
// to make sure objects are well structured or just let people set
|
||||||
// whatever they want but risking messed up settings.
|
// whatever they want but risking messed up settings.
|
||||||
// Objects & Arrays are disabled for now
|
// Objects & Arrays are disabled for now
|
||||||
// https://github.com/quickshell-mirror/quickshell/pull/22
|
// https://github.com/quickshell-mirror/quickshell/pull/22
|
||||||
throw "Setting Objects and Arrays not supported"
|
throw "Setting Objects and Arrays not supported";
|
||||||
default:
|
default:
|
||||||
throw "Unsupported type"
|
throw "Unsupported type";
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn("Setting:", key, value)
|
console.warn("Setting:", key, value);
|
||||||
SettingsData[key] = value
|
SettingsData[key] = value;
|
||||||
SettingsData.saveSettings()
|
SettingsData.saveSettings();
|
||||||
return "SETTINGS_SET_SUCCESS"
|
return "SETTINGS_SET_SUCCESS";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to set property:", key, "error:", e)
|
console.warn("Failed to set property:", key, "error:", e);
|
||||||
return "SETTINGS_SET_FAILURE"
|
return "SETTINGS_SET_FAILURE";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,14 +97,12 @@ DankModal {
|
|||||||
const bind = binds[i];
|
const bind = binds[i];
|
||||||
if (bind.subcat) {
|
if (bind.subcat) {
|
||||||
hasSubcats = true;
|
hasSubcats = true;
|
||||||
if (!subcats[bind.subcat]) {
|
if (!subcats[bind.subcat])
|
||||||
subcats[bind.subcat] = [];
|
subcats[bind.subcat] = [];
|
||||||
}
|
|
||||||
subcats[bind.subcat].push(bind);
|
subcats[bind.subcat].push(bind);
|
||||||
} else {
|
} else {
|
||||||
if (!subcats["_root"]) {
|
if (!subcats["_root"])
|
||||||
subcats["_root"] = [];
|
subcats["_root"] = [];
|
||||||
}
|
|
||||||
subcats["_root"].push(bind);
|
subcats["_root"].push(bind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,12 +119,10 @@ DankModal {
|
|||||||
|
|
||||||
function distributeCategories(cols) {
|
function distributeCategories(cols) {
|
||||||
const columns = [];
|
const columns = [];
|
||||||
for (let i = 0; i < cols; i++) {
|
for (let i = 0; i < cols; i++)
|
||||||
columns.push([]);
|
columns.push([]);
|
||||||
}
|
for (let i = 0; i < categoryKeys.length; i++)
|
||||||
for (let i = 0; i < categoryKeys.length; i++) {
|
|
||||||
columns[i % cols].push(categoryKeys[i]);
|
columns[i % cols].push(categoryKeys[i]);
|
||||||
}
|
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,13 +57,32 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: topBarLoader
|
id: keybindsLoader
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentIndex === 2
|
active: root.currentIndex === 2
|
||||||
visible: active
|
visible: active
|
||||||
focus: active
|
focus: active
|
||||||
|
|
||||||
|
sourceComponent: KeybindsTab {
|
||||||
|
parentModal: root.parentModal
|
||||||
|
}
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active && item) {
|
||||||
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: topBarLoader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 3
|
||||||
|
visible: active
|
||||||
|
focus: active
|
||||||
|
|
||||||
sourceComponent: DankBarTab {
|
sourceComponent: DankBarTab {
|
||||||
parentModal: root.parentModal
|
parentModal: root.parentModal
|
||||||
}
|
}
|
||||||
@@ -79,7 +98,7 @@ FocusScope {
|
|||||||
id: widgetsLoader
|
id: widgetsLoader
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentIndex === 3
|
active: root.currentIndex === 4
|
||||||
visible: active
|
visible: active
|
||||||
focus: active
|
focus: active
|
||||||
|
|
||||||
@@ -96,7 +115,7 @@ FocusScope {
|
|||||||
id: dockLoader
|
id: dockLoader
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentIndex === 4
|
active: root.currentIndex === 5
|
||||||
visible: active
|
visible: active
|
||||||
focus: active
|
focus: active
|
||||||
|
|
||||||
@@ -115,7 +134,7 @@ FocusScope {
|
|||||||
id: displaysLoader
|
id: displaysLoader
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentIndex === 5
|
active: root.currentIndex === 6
|
||||||
visible: active
|
visible: active
|
||||||
focus: active
|
focus: active
|
||||||
|
|
||||||
@@ -132,7 +151,7 @@ FocusScope {
|
|||||||
id: networkLoader
|
id: networkLoader
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentIndex === 6
|
active: root.currentIndex === 7
|
||||||
visible: active
|
visible: active
|
||||||
focus: active
|
focus: active
|
||||||
|
|
||||||
@@ -149,7 +168,7 @@ FocusScope {
|
|||||||
id: printerLoader
|
id: printerLoader
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentIndex === 7
|
active: root.currentIndex === 8
|
||||||
visible: active
|
visible: active
|
||||||
focus: active
|
focus: active
|
||||||
|
|
||||||
@@ -166,7 +185,7 @@ FocusScope {
|
|||||||
id: launcherLoader
|
id: launcherLoader
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentIndex === 8
|
active: root.currentIndex === 9
|
||||||
visible: active
|
visible: active
|
||||||
focus: active
|
focus: active
|
||||||
|
|
||||||
@@ -183,7 +202,7 @@ FocusScope {
|
|||||||
id: themeColorsLoader
|
id: themeColorsLoader
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentIndex === 9
|
active: root.currentIndex === 10
|
||||||
visible: active
|
visible: active
|
||||||
focus: active
|
focus: active
|
||||||
|
|
||||||
@@ -200,7 +219,7 @@ FocusScope {
|
|||||||
id: powerLoader
|
id: powerLoader
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentIndex === 10
|
active: root.currentIndex === 11
|
||||||
visible: active
|
visible: active
|
||||||
focus: active
|
focus: active
|
||||||
|
|
||||||
@@ -217,7 +236,7 @@ FocusScope {
|
|||||||
id: pluginsLoader
|
id: pluginsLoader
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentIndex === 11
|
active: root.currentIndex === 12
|
||||||
visible: active
|
visible: active
|
||||||
focus: active
|
focus: active
|
||||||
|
|
||||||
@@ -236,7 +255,7 @@ FocusScope {
|
|||||||
id: aboutLoader
|
id: aboutLoader
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: root.currentIndex === 12
|
active: root.currentIndex === 13
|
||||||
visible: active
|
visible: active
|
||||||
focus: active
|
focus: active
|
||||||
|
|
||||||
|
|||||||
@@ -22,62 +22,68 @@ Rectangle {
|
|||||||
"icon": "schedule",
|
"icon": "schedule",
|
||||||
"tabIndex": 1
|
"tabIndex": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"text": I18n.tr("Keyboard Shortcuts"),
|
||||||
|
"icon": "keyboard",
|
||||||
|
"shortcutsOnly": true,
|
||||||
|
"tabIndex": 2
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"text": I18n.tr("Dank Bar"),
|
"text": I18n.tr("Dank Bar"),
|
||||||
"icon": "toolbar",
|
"icon": "toolbar",
|
||||||
"tabIndex": 2
|
"tabIndex": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": I18n.tr("Widgets"),
|
"text": I18n.tr("Widgets"),
|
||||||
"icon": "widgets",
|
"icon": "widgets",
|
||||||
"tabIndex": 3
|
"tabIndex": 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": I18n.tr("Dock"),
|
"text": I18n.tr("Dock"),
|
||||||
"icon": "dock_to_bottom",
|
"icon": "dock_to_bottom",
|
||||||
"tabIndex": 4
|
"tabIndex": 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": I18n.tr("Displays"),
|
"text": I18n.tr("Displays"),
|
||||||
"icon": "monitor",
|
"icon": "monitor",
|
||||||
"tabIndex": 5
|
"tabIndex": 6
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": I18n.tr("Network"),
|
"text": I18n.tr("Network"),
|
||||||
"icon": "wifi",
|
"icon": "wifi",
|
||||||
"dmsOnly": true,
|
"dmsOnly": true,
|
||||||
"tabIndex": 6
|
"tabIndex": 7
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": I18n.tr("Printers"),
|
"text": I18n.tr("Printers"),
|
||||||
"icon": "print",
|
"icon": "print",
|
||||||
"cupsOnly": true,
|
"cupsOnly": true,
|
||||||
"tabIndex": 7
|
"tabIndex": 8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": I18n.tr("Launcher"),
|
"text": I18n.tr("Launcher"),
|
||||||
"icon": "apps",
|
"icon": "apps",
|
||||||
"tabIndex": 8
|
"tabIndex": 9
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": I18n.tr("Theme & Colors"),
|
"text": I18n.tr("Theme & Colors"),
|
||||||
"icon": "palette",
|
"icon": "palette",
|
||||||
"tabIndex": 9
|
"tabIndex": 10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": I18n.tr("Power & Security"),
|
"text": I18n.tr("Power & Security"),
|
||||||
"icon": "power",
|
"icon": "power",
|
||||||
"tabIndex": 10
|
"tabIndex": 11
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": I18n.tr("Plugins"),
|
"text": I18n.tr("Plugins"),
|
||||||
"icon": "extension",
|
"icon": "extension",
|
||||||
"tabIndex": 11
|
"tabIndex": 12
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": I18n.tr("About"),
|
"text": I18n.tr("About"),
|
||||||
"icon": "info",
|
"icon": "info",
|
||||||
"tabIndex": 12
|
"tabIndex": 13
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
readonly property var sidebarItems: allSidebarItems.filter(item => {
|
readonly property var sidebarItems: allSidebarItems.filter(item => {
|
||||||
@@ -85,6 +91,8 @@ Rectangle {
|
|||||||
return false;
|
return false;
|
||||||
if (item.cupsOnly && !CupsService.cupsAvailable)
|
if (item.cupsOnly && !CupsService.cupsAvailable)
|
||||||
return false;
|
return false;
|
||||||
|
if (item.shortcutsOnly && !KeybindsService.available)
|
||||||
|
return false;
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
548
quickshell/Modules/Settings/KeybindsTab.qml
Normal file
548
quickshell/Modules/Settings/KeybindsTab.qml
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: keybindsTab
|
||||||
|
|
||||||
|
property var parentModal: null
|
||||||
|
property string selectedCategory: ""
|
||||||
|
property string searchQuery: ""
|
||||||
|
property string expandedKey: ""
|
||||||
|
property bool showingNewBind: false
|
||||||
|
|
||||||
|
property int _lastDataVersion: -1
|
||||||
|
property var _cachedCategories: []
|
||||||
|
property var _filteredBinds: []
|
||||||
|
|
||||||
|
function _updateFiltered() {
|
||||||
|
const allBinds = KeybindsService.getFlatBinds();
|
||||||
|
if (!searchQuery && !selectedCategory) {
|
||||||
|
_filteredBinds = allBinds;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = searchQuery.toLowerCase();
|
||||||
|
const isOverrideFilter = selectedCategory === "__overrides__";
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < allBinds.length; i++) {
|
||||||
|
const group = allBinds[i];
|
||||||
|
if (q) {
|
||||||
|
let keyMatch = false;
|
||||||
|
for (let k = 0; k < group.keys.length; k++) {
|
||||||
|
if (group.keys[k].key.toLowerCase().indexOf(q) !== -1) {
|
||||||
|
keyMatch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!keyMatch && group.desc.toLowerCase().indexOf(q) === -1 && group.action.toLowerCase().indexOf(q) === -1)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isOverrideFilter) {
|
||||||
|
let hasOverride = false;
|
||||||
|
for (let k = 0; k < group.keys.length; k++) {
|
||||||
|
if (group.keys[k].isOverride) {
|
||||||
|
hasOverride = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasOverride)
|
||||||
|
continue;
|
||||||
|
} else if (selectedCategory && group.category !== selectedCategory) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.push(group);
|
||||||
|
}
|
||||||
|
_filteredBinds = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateCategories() {
|
||||||
|
_cachedCategories = ["__overrides__"].concat(KeybindsService.getCategories());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCategoryLabel(cat) {
|
||||||
|
if (cat === "__overrides__")
|
||||||
|
return I18n.tr("Overrides");
|
||||||
|
return cat;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExpanded(action) {
|
||||||
|
expandedKey = expandedKey === action ? "" : action;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startNewBind() {
|
||||||
|
showingNewBind = true;
|
||||||
|
expandedKey = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelNewBind() {
|
||||||
|
showingNewBind = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveNewBind(bindData) {
|
||||||
|
KeybindsService.saveBind("", bindData);
|
||||||
|
showingNewBind = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToTop() {
|
||||||
|
flickable.contentY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: searchDebounce
|
||||||
|
interval: 150
|
||||||
|
onTriggered: keybindsTab._updateFiltered()
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: KeybindsService
|
||||||
|
function onBindsLoaded() {
|
||||||
|
keybindsTab._lastDataVersion = KeybindsService._dataVersion;
|
||||||
|
keybindsTab._updateCategories();
|
||||||
|
keybindsTab._updateFiltered();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _ensureNiriProvider() {
|
||||||
|
if (!KeybindsService.available)
|
||||||
|
return;
|
||||||
|
const cachedProvider = KeybindsService.keybinds?.provider;
|
||||||
|
if (cachedProvider !== "niri" || KeybindsService._dataVersion === 0) {
|
||||||
|
KeybindsService.currentProvider = "niri";
|
||||||
|
KeybindsService.loadBinds();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_lastDataVersion !== KeybindsService._dataVersion) {
|
||||||
|
_lastDataVersion = KeybindsService._dataVersion;
|
||||||
|
_updateCategories();
|
||||||
|
_updateFiltered();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: _ensureNiriProvider()
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (!visible)
|
||||||
|
return;
|
||||||
|
Qt.callLater(scrollToTop);
|
||||||
|
_ensureNiriProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: flickable
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: contentColumn.implicitHeight
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: contentColumn
|
||||||
|
width: flickable.width
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
topPadding: Theme.spacingXL
|
||||||
|
bottomPadding: Theme.spacingXL
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: Math.min(650, parent.width - Theme.spacingL * 2)
|
||||||
|
height: headerSection.implicitHeight + Theme.spacingL * 2
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: headerSection
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "keyboard"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - Theme.iconSize - Theme.spacingM * 2
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Keyboard Shortcuts")
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Click any shortcut to edit. Changes save to dms/binds.kdl")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: searchField
|
||||||
|
width: parent.width - addButton.width - Theme.spacingM
|
||||||
|
height: 44
|
||||||
|
placeholderText: I18n.tr("Search keybinds...")
|
||||||
|
leftIconName: "search"
|
||||||
|
onTextChanged: {
|
||||||
|
keybindsTab.searchQuery = text;
|
||||||
|
searchDebounce.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: addButton
|
||||||
|
width: 44
|
||||||
|
height: 44
|
||||||
|
circular: false
|
||||||
|
iconName: "add"
|
||||||
|
iconSize: Theme.iconSize
|
||||||
|
iconColor: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
enabled: !keybindsTab.showingNewBind
|
||||||
|
opacity: enabled ? 1 : 0.5
|
||||||
|
onClicked: keybindsTab.startNewBind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: Math.min(650, parent.width - Theme.spacingL * 2)
|
||||||
|
height: warningSection.implicitHeight + Theme.spacingL * 2
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.error, 0.15)
|
||||||
|
border.color: Theme.withAlpha(Theme.error, 0.3)
|
||||||
|
border.width: 1
|
||||||
|
visible: !KeybindsService.dmsBindsIncluded && !KeybindsService.loading
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: warningSection
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "warning"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.error
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - Theme.iconSize - 100 - Theme.spacingM * 2
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Binds Include Missing")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.error
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("dms/binds.kdl is not included in config.kdl. Custom keybinds will not work until this is fixed.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: fixButton
|
||||||
|
width: fixButtonText.implicitWidth + Theme.spacingL * 2
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: KeybindsService.fixing ? Theme.withAlpha(Theme.error, 0.6) : Theme.error
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: fixButtonText
|
||||||
|
text: KeybindsService.fixing ? I18n.tr("Fixing...") : I18n.tr("Fix Now")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surface
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: !KeybindsService.fixing
|
||||||
|
onClicked: KeybindsService.fixDmsBindsInclude()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: Math.min(650, parent.width - Theme.spacingL * 2)
|
||||||
|
height: categorySection.implicitHeight + Theme.spacingL * 2
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: categorySection
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: allChip.implicitWidth + Theme.spacingL
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: !keybindsTab.selectedCategory ? Theme.primary : Theme.surfaceContainerHighest
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: allChip
|
||||||
|
text: I18n.tr("All")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: !keybindsTab.selectedCategory ? Theme.primaryText : Theme.surfaceVariantText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
keybindsTab.selectedCategory = "";
|
||||||
|
keybindsTab._updateFiltered();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: keybindsTab._cachedCategories
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property string modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: catText.implicitWidth + Theme.spacingL
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: keybindsTab.selectedCategory === modelData ? Theme.primary : (modelData === "__overrides__" ? Theme.withAlpha(Theme.primary, 0.15) : Theme.surfaceContainerHighest)
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: catText
|
||||||
|
text: keybindsTab.getCategoryLabel(modelData)
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: keybindsTab.selectedCategory === modelData ? Theme.primaryText : (modelData === "__overrides__" ? Theme.primary : Theme.surfaceVariantText)
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
keybindsTab.selectedCategory = modelData;
|
||||||
|
keybindsTab._updateFiltered();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: Math.min(650, parent.width - Theme.spacingL * 2)
|
||||||
|
height: newBindSection.implicitHeight + Theme.spacingL * 2
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
|
border.color: Theme.outlineVariant
|
||||||
|
border.width: 1
|
||||||
|
visible: keybindsTab.showingNewBind
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: newBindSection
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "add"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("New Keybind")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeybindItem {
|
||||||
|
width: parent.width
|
||||||
|
isNew: true
|
||||||
|
isExpanded: true
|
||||||
|
bindData: ({
|
||||||
|
keys: [
|
||||||
|
{
|
||||||
|
key: "",
|
||||||
|
source: "dms",
|
||||||
|
isOverride: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
action: "",
|
||||||
|
desc: ""
|
||||||
|
})
|
||||||
|
panelWindow: keybindsTab.parentModal
|
||||||
|
onSaveBind: (originalKey, newData) => keybindsTab.saveNewBind(newData)
|
||||||
|
onCancelEdit: keybindsTab.cancelNewBind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: Math.min(650, parent.width - Theme.spacingL * 2)
|
||||||
|
height: bindsListHeader.implicitHeight + Theme.spacingL * 2
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: bindsListHeader
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "list"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: KeybindsService.loading ? I18n.tr("Shortcuts") : I18n.tr("Shortcuts") + " (" + keybindsTab._filteredBinds.length + ")"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
visible: KeybindsService.loading
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: loadingIcon
|
||||||
|
name: "sync"
|
||||||
|
size: 20
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1000
|
||||||
|
loops: Animation.Infinite
|
||||||
|
running: KeybindsService.loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Loading keybinds...")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("No keybinds found")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: !KeybindsService.loading && keybindsTab._filteredBinds.length === 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ScriptModel {
|
||||||
|
values: keybindsTab._filteredBinds
|
||||||
|
objectProp: "action"
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: bindItem.height
|
||||||
|
|
||||||
|
KeybindItem {
|
||||||
|
id: bindItem
|
||||||
|
width: Math.min(650, parent.width - Theme.spacingL * 2)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
bindData: modelData
|
||||||
|
isExpanded: keybindsTab.expandedKey === modelData.action
|
||||||
|
panelWindow: keybindsTab.parentModal
|
||||||
|
onToggleExpand: keybindsTab.toggleExpanded(modelData.action)
|
||||||
|
onSaveBind: (originalKey, newData) => {
|
||||||
|
KeybindsService.saveBind(originalKey, newData);
|
||||||
|
keybindsTab.expandedKey = modelData.action;
|
||||||
|
}
|
||||||
|
onRemoveBind: key => KeybindsService.removeBind(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,6 @@ import QtQuick
|
|||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Widgets
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -22,13 +20,13 @@ PanelWindow {
|
|||||||
target: ToastService
|
target: ToastService
|
||||||
function onToastVisibleChanged() {
|
function onToastVisibleChanged() {
|
||||||
if (ToastService.toastVisible) {
|
if (ToastService.toastVisible) {
|
||||||
shouldBeVisible = true
|
shouldBeVisible = true;
|
||||||
visible = true
|
visible = true;
|
||||||
} else {
|
} else {
|
||||||
// Freeze the width before starting exit animation
|
// Freeze the width before starting exit animation
|
||||||
frozenWidth = toast.width
|
frozenWidth = toast.width;
|
||||||
shouldBeVisible = false
|
shouldBeVisible = false;
|
||||||
closeTimer.restart()
|
closeTimer.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +36,7 @@ PanelWindow {
|
|||||||
interval: Theme.mediumDuration + 50
|
interval: Theme.mediumDuration + 50
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (!shouldBeVisible) {
|
if (!shouldBeVisible) {
|
||||||
visible = false
|
visible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +63,7 @@ PanelWindow {
|
|||||||
Connections {
|
Connections {
|
||||||
target: ToastService
|
target: ToastService
|
||||||
function onResetToastState() {
|
function onResetToastState() {
|
||||||
toast.expanded = false
|
toast.expanded = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,13 +74,13 @@ PanelWindow {
|
|||||||
color: {
|
color: {
|
||||||
switch (ToastService.currentLevel) {
|
switch (ToastService.currentLevel) {
|
||||||
case ToastService.levelError:
|
case ToastService.levelError:
|
||||||
return Theme.error
|
return Theme.error;
|
||||||
case ToastService.levelWarn:
|
case ToastService.levelWarn:
|
||||||
return Theme.warning
|
return Theme.warning;
|
||||||
case ToastService.levelInfo:
|
case ToastService.levelInfo:
|
||||||
return Theme.surfaceContainer
|
return Theme.surfaceContainer;
|
||||||
default:
|
default:
|
||||||
return Theme.surfaceContainer
|
return Theme.surfaceContainer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
@@ -109,13 +107,13 @@ PanelWindow {
|
|||||||
name: {
|
name: {
|
||||||
switch (ToastService.currentLevel) {
|
switch (ToastService.currentLevel) {
|
||||||
case ToastService.levelError:
|
case ToastService.levelError:
|
||||||
return "error"
|
return "error";
|
||||||
case ToastService.levelWarn:
|
case ToastService.levelWarn:
|
||||||
return "warning"
|
return "warning";
|
||||||
case ToastService.levelInfo:
|
case ToastService.levelInfo:
|
||||||
return "info"
|
return "info";
|
||||||
default:
|
default:
|
||||||
return "info"
|
return "info";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
size: Theme.iconSize
|
size: Theme.iconSize
|
||||||
@@ -123,9 +121,9 @@ PanelWindow {
|
|||||||
switch (ToastService.currentLevel) {
|
switch (ToastService.currentLevel) {
|
||||||
case ToastService.levelError:
|
case ToastService.levelError:
|
||||||
case ToastService.levelWarn:
|
case ToastService.levelWarn:
|
||||||
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
|
return SessionData.isLightMode ? Theme.surfaceText : Theme.background;
|
||||||
default:
|
default:
|
||||||
return Theme.surfaceText
|
return Theme.surfaceText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -140,9 +138,9 @@ PanelWindow {
|
|||||||
switch (ToastService.currentLevel) {
|
switch (ToastService.currentLevel) {
|
||||||
case ToastService.levelError:
|
case ToastService.levelError:
|
||||||
case ToastService.levelWarn:
|
case ToastService.levelWarn:
|
||||||
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
|
return SessionData.isLightMode ? Theme.surfaceText : Theme.background;
|
||||||
default:
|
default:
|
||||||
return Theme.surfaceText
|
return Theme.surfaceText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
@@ -163,9 +161,9 @@ PanelWindow {
|
|||||||
switch (ToastService.currentLevel) {
|
switch (ToastService.currentLevel) {
|
||||||
case ToastService.levelError:
|
case ToastService.levelError:
|
||||||
case ToastService.levelWarn:
|
case ToastService.levelWarn:
|
||||||
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
|
return SessionData.isLightMode ? Theme.surfaceText : Theme.background;
|
||||||
default:
|
default:
|
||||||
return Theme.surfaceText
|
return Theme.surfaceText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buttonSize: Theme.iconSize + 8
|
buttonSize: Theme.iconSize + 8
|
||||||
@@ -175,11 +173,11 @@ PanelWindow {
|
|||||||
visible: ToastService.hasDetails
|
visible: ToastService.hasDetails
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
toast.expanded = !toast.expanded
|
toast.expanded = !toast.expanded;
|
||||||
if (toast.expanded) {
|
if (toast.expanded) {
|
||||||
ToastService.stopTimer()
|
ToastService.stopTimer();
|
||||||
} else {
|
} else {
|
||||||
ToastService.restartTimer()
|
ToastService.restartTimer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,9 +190,9 @@ PanelWindow {
|
|||||||
switch (ToastService.currentLevel) {
|
switch (ToastService.currentLevel) {
|
||||||
case ToastService.levelError:
|
case ToastService.levelError:
|
||||||
case ToastService.levelWarn:
|
case ToastService.levelWarn:
|
||||||
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
|
return SessionData.isLightMode ? Theme.surfaceText : Theme.background;
|
||||||
default:
|
default:
|
||||||
return Theme.surfaceText
|
return Theme.surfaceText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buttonSize: Theme.iconSize + 8
|
buttonSize: Theme.iconSize + 8
|
||||||
@@ -203,7 +201,7 @@ PanelWindow {
|
|||||||
visible: ToastService.hasDetails || ToastService.currentLevel === ToastService.levelError
|
visible: ToastService.hasDetails || ToastService.currentLevel === ToastService.levelError
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
ToastService.hideToast()
|
ToastService.hideToast();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,7 +209,7 @@ PanelWindow {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: detailsColumn.height + Theme.spacingS * 2
|
height: detailsColumn.height + Theme.spacingS * 2
|
||||||
color: Qt.rgba(0, 0, 0, 0.2)
|
color: ToastService.currentDetails.length > 0 ? Qt.rgba(0, 0, 0, 0.2) : "transparent"
|
||||||
radius: Theme.cornerRadius / 2
|
radius: Theme.cornerRadius / 2
|
||||||
visible: toast.expanded && ToastService.hasDetails
|
visible: toast.expanded && ToastService.hasDetails
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
@@ -232,9 +230,9 @@ PanelWindow {
|
|||||||
switch (ToastService.currentLevel) {
|
switch (ToastService.currentLevel) {
|
||||||
case ToastService.levelError:
|
case ToastService.levelError:
|
||||||
case ToastService.levelWarn:
|
case ToastService.levelWarn:
|
||||||
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
|
return SessionData.isLightMode ? Theme.surfaceText : Theme.background;
|
||||||
default:
|
default:
|
||||||
return Theme.surfaceText
|
return Theme.surfaceText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
visible: ToastService.currentDetails.length > 0
|
visible: ToastService.currentDetails.length > 0
|
||||||
@@ -259,9 +257,9 @@ PanelWindow {
|
|||||||
switch (ToastService.currentLevel) {
|
switch (ToastService.currentLevel) {
|
||||||
case ToastService.levelError:
|
case ToastService.levelError:
|
||||||
case ToastService.levelWarn:
|
case ToastService.levelWarn:
|
||||||
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
|
return SessionData.isLightMode ? Theme.surfaceText : Theme.background;
|
||||||
default:
|
default:
|
||||||
return Theme.surfaceText
|
return Theme.surfaceText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isMonospace: true
|
isMonospace: true
|
||||||
@@ -281,9 +279,9 @@ PanelWindow {
|
|||||||
switch (ToastService.currentLevel) {
|
switch (ToastService.currentLevel) {
|
||||||
case ToastService.levelError:
|
case ToastService.levelError:
|
||||||
case ToastService.levelWarn:
|
case ToastService.levelWarn:
|
||||||
return SessionData.isLightMode ? Theme.surfaceText : Theme.background
|
return SessionData.isLightMode ? Theme.surfaceText : Theme.background;
|
||||||
default:
|
default:
|
||||||
return Theme.surfaceText
|
return Theme.surfaceText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buttonSize: Theme.iconSizeSmall + 8
|
buttonSize: Theme.iconSizeSmall + 8
|
||||||
@@ -295,9 +293,9 @@ PanelWindow {
|
|||||||
property bool showTooltip: false
|
property bool showTooltip: false
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Quickshell.execDetached(["wl-copy", ToastService.currentCommand])
|
Quickshell.execDetached(["wl-copy", ToastService.currentCommand]);
|
||||||
showTooltip = true
|
showTooltip = true;
|
||||||
tooltipTimer.start()
|
tooltipTimer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
@@ -346,7 +344,6 @@ PanelWindow {
|
|||||||
shadowOpacity: 0.3
|
shadowOpacity: 0.3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.mediumDuration
|
duration: Theme.mediumDuration
|
||||||
|
|||||||
@@ -422,7 +422,6 @@ Singleton {
|
|||||||
isLabwc = false;
|
isLabwc = false;
|
||||||
compositor = "niri";
|
compositor = "niri";
|
||||||
console.info("CompositorService: Detected Niri with socket:", niriSocket);
|
console.info("CompositorService: Detected Niri with socket:", niriSocket);
|
||||||
NiriService.generateNiriBinds();
|
|
||||||
NiriService.generateNiriBlurrule();
|
NiriService.generateNiriBlurrule();
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,59 +0,0 @@
|
|||||||
binds {
|
|
||||||
Mod+Space hotkey-overlay-title="Application Launcher" {
|
|
||||||
spawn "dms" "ipc" "call" "spotlight" "toggle";
|
|
||||||
}
|
|
||||||
|
|
||||||
Mod+V hotkey-overlay-title="Clipboard Manager" {
|
|
||||||
spawn "dms" "ipc" "call" "clipboard" "toggle";
|
|
||||||
}
|
|
||||||
|
|
||||||
Mod+M hotkey-overlay-title="Task Manager" {
|
|
||||||
spawn "dms" "ipc" "call" "processlist" "toggle";
|
|
||||||
}
|
|
||||||
|
|
||||||
Mod+Comma hotkey-overlay-title="Settings" {
|
|
||||||
spawn "dms" "ipc" "call" "settings" "toggle";
|
|
||||||
}
|
|
||||||
|
|
||||||
Mod+N hotkey-overlay-title="Notification Center" {
|
|
||||||
spawn "dms" "ipc" "call" "notifications" "toggle";
|
|
||||||
}
|
|
||||||
|
|
||||||
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
|
||||||
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
|
|
||||||
}
|
|
||||||
|
|
||||||
Mod+Shift+N hotkey-overlay-title="Notepad" {
|
|
||||||
spawn "dms" "ipc" "call" "notepad" "toggle";
|
|
||||||
}
|
|
||||||
|
|
||||||
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
|
|
||||||
spawn "dms" "ipc" "call" "lock" "lock";
|
|
||||||
}
|
|
||||||
|
|
||||||
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
|
|
||||||
spawn "dms" "ipc" "call" "processlist" "toggle";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audio
|
|
||||||
XF86AudioRaiseVolume allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "audio" "increment" "3";
|
|
||||||
}
|
|
||||||
XF86AudioLowerVolume allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "audio" "decrement" "3";
|
|
||||||
}
|
|
||||||
XF86AudioMute allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "audio" "mute";
|
|
||||||
}
|
|
||||||
XF86AudioMicMute allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "audio" "micmute";
|
|
||||||
}
|
|
||||||
|
|
||||||
// BL
|
|
||||||
XF86MonBrightnessUp allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
|
|
||||||
}
|
|
||||||
XF86MonBrightnessDown allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1150
quickshell/Widgets/KeybindItem.qml
Normal file
1150
quickshell/Widgets/KeybindItem.qml
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user