Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
X
ximperconf
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Ximper Linux
ximperconf
Commits
18c53439
Verified
Commit
18c53439
authored
Mar 08, 2026
by
Kirill Unitsaev
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
hyprland: add binds command to display keybindings
parent
0888c0d2
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
428 additions
and
7 deletions
+428
-7
binds.go
hyprland/binds.go
+410
-0
commands.go
hyprland/commands.go
+13
-0
json.go
ui/json.go
+5
-7
No files found.
hyprland/binds.go
0 → 100644
View file @
18c53439
package
hyprland
import
(
"context"
"encoding/json"
"fmt"
"os/exec"
"strings"
"unicode"
"unicode/utf8"
"ximperconf/config"
"ximperconf/locale"
"ximperconf/ui"
"github.com/fatih/color"
"github.com/urfave/cli/v3"
)
type
HyprBind
struct
{
Mouse
bool
`json:"mouse"`
HasDescription
bool
`json:"has_description"`
Modmask
int
`json:"modmask"`
Submap
string
`json:"submap"`
Key
string
`json:"key"`
Description
string
`json:"description"`
Dispatcher
string
`json:"dispatcher"`
Arg
string
`json:"arg"`
}
type
bindEntry
struct
{
Submap
string
Key
string
Action
string
Dispatcher
string
Arg
string
IsDescription
bool
}
type
BindJSON
struct
{
Submap
string
`json:"submap"`
Key
string
`json:"key"`
Dispatcher
string
`json:"dispatcher"`
Arg
string
`json:"arg"`
Description
string
`json:"description"`
}
func
(
m
*
HyprlandManager
)
GetBinds
()
([]
HyprBind
,
error
)
{
out
,
err
:=
exec
.
Command
(
"hyprctl"
,
"binds"
,
"-j"
)
.
Output
()
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
locale
.
T
(
"failed to get binds: %s"
),
err
)
}
var
binds
[]
HyprBind
if
err
:=
json
.
Unmarshal
(
out
,
&
binds
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
locale
.
T
(
"failed to parse binds: %s"
),
err
)
}
return
binds
,
nil
}
const
(
modShift
=
1
modCtrl
=
4
modAlt
=
8
modSuper
=
64
)
func
formatModmask
(
mask
int
)
string
{
var
mods
[]
string
if
mask
&
modSuper
!=
0
{
mods
=
append
(
mods
,
"SUPER"
)
}
if
mask
&
modAlt
!=
0
{
mods
=
append
(
mods
,
"ALT"
)
}
if
mask
&
modCtrl
!=
0
{
mods
=
append
(
mods
,
"CTRL"
)
}
if
mask
&
modShift
!=
0
{
mods
=
append
(
mods
,
"SHIFT"
)
}
return
strings
.
Join
(
mods
,
" + "
)
}
var
mouseButtonNames
=
map
[
string
]
string
{
"mouse:272"
:
"ЛКМ"
,
"mouse:273"
:
"ПКМ"
,
"mouse:274"
:
"СКМ"
,
}
func
formatBindKey
(
mods
string
,
key
string
)
string
{
if
name
,
ok
:=
mouseButtonNames
[
key
];
ok
{
key
=
name
}
else
{
key
=
strings
.
ToUpper
(
key
)
}
if
mods
==
""
{
return
key
}
return
mods
+
" + "
+
key
}
func
formatAction
(
b
HyprBind
)
(
string
,
bool
)
{
if
b
.
HasDescription
&&
b
.
Description
!=
""
{
return
b
.
Description
,
true
}
if
b
.
Arg
!=
""
{
return
b
.
Dispatcher
+
" "
+
b
.
Arg
,
false
}
return
b
.
Dispatcher
,
false
}
type
groupKey
struct
{
Modmask
int
Submap
string
Dispatcher
string
}
func
isDigitKey
(
key
string
)
bool
{
return
len
(
key
)
==
1
&&
unicode
.
IsDigit
(
rune
(
key
[
0
]))
}
func
replaceFirstDigitSeq
(
s
string
,
digit
string
)
string
{
idx
:=
strings
.
Index
(
s
,
digit
)
if
idx
<
0
{
return
s
}
start
:=
idx
for
start
>
0
&&
unicode
.
IsDigit
(
rune
(
s
[
start
-
1
]))
{
start
--
}
end
:=
idx
+
len
(
digit
)
for
end
<
len
(
s
)
&&
unicode
.
IsDigit
(
rune
(
s
[
end
]))
{
end
++
}
return
s
[
:
start
]
+
"<NUMBER>"
+
s
[
end
:
]
}
func
bindToEntry
(
b
HyprBind
)
bindEntry
{
mods
:=
formatModmask
(
b
.
Modmask
)
action
,
isDesc
:=
formatAction
(
b
)
return
bindEntry
{
Submap
:
b
.
Submap
,
Key
:
formatBindKey
(
mods
,
b
.
Key
),
Action
:
action
,
Dispatcher
:
b
.
Dispatcher
,
Arg
:
b
.
Arg
,
IsDescription
:
isDesc
,
}
}
func
isXF86Key
(
key
string
)
bool
{
return
strings
.
HasPrefix
(
strings
.
ToUpper
(
key
),
"XF86"
)
}
func
deduplicateBinds
(
binds
[]
HyprBind
)
[]
HyprBind
{
type
dedupKey
struct
{
Modmask
int
Submap
string
Key
string
Description
string
}
seen
:=
make
(
map
[
dedupKey
]
bool
)
var
result
[]
HyprBind
for
_
,
b
:=
range
binds
{
if
!
b
.
HasDescription
||
b
.
Description
==
""
{
result
=
append
(
result
,
b
)
continue
}
dk
:=
dedupKey
{
b
.
Modmask
,
b
.
Submap
,
b
.
Key
,
b
.
Description
}
if
!
seen
[
dk
]
{
seen
[
dk
]
=
true
result
=
append
(
result
,
b
)
}
}
return
result
}
func
groupBinds
(
binds
[]
HyprBind
,
showAll
bool
)
[]
bindEntry
{
binds
=
deduplicateBinds
(
binds
)
groups
:=
make
(
map
[
groupKey
][]
HyprBind
)
var
order
[]
groupKey
var
nonDigit
[]
HyprBind
for
_
,
b
:=
range
binds
{
if
!
showAll
&&
isXF86Key
(
b
.
Key
)
{
continue
}
if
!
isDigitKey
(
b
.
Key
)
{
nonDigit
=
append
(
nonDigit
,
b
)
continue
}
gk
:=
groupKey
{
b
.
Modmask
,
b
.
Submap
,
b
.
Dispatcher
}
if
_
,
ok
:=
groups
[
gk
];
!
ok
{
order
=
append
(
order
,
gk
)
}
groups
[
gk
]
=
append
(
groups
[
gk
],
b
)
}
var
entries
[]
bindEntry
for
_
,
gk
:=
range
order
{
group
:=
groups
[
gk
]
if
len
(
group
)
>=
3
{
mods
:=
formatModmask
(
gk
.
Modmask
)
key
:=
formatBindKey
(
mods
,
"<NUMBER>"
)
var
action
string
isDesc
:=
false
for
_
,
b
:=
range
group
{
if
b
.
HasDescription
&&
b
.
Description
!=
""
{
action
=
replaceFirstDigitSeq
(
b
.
Description
,
b
.
Key
)
isDesc
=
true
break
}
}
if
action
==
""
{
sample
:=
group
[
0
]
if
sample
.
Arg
!=
""
{
action
=
sample
.
Dispatcher
+
" "
+
replaceFirstDigitSeq
(
sample
.
Arg
,
sample
.
Key
)
}
else
{
action
=
sample
.
Dispatcher
}
}
entries
=
append
(
entries
,
bindEntry
{
Submap
:
gk
.
Submap
,
Key
:
key
,
Action
:
action
,
Dispatcher
:
gk
.
Dispatcher
,
Arg
:
"<NUMBER>"
,
IsDescription
:
isDesc
,
})
}
else
{
for
_
,
b
:=
range
group
{
entries
=
append
(
entries
,
bindToEntry
(
b
))
}
}
}
for
_
,
b
:=
range
nonDigit
{
entries
=
append
(
entries
,
bindToEntry
(
b
))
}
return
mergeByAction
(
entries
)
}
func
mergeByAction
(
entries
[]
bindEntry
)
[]
bindEntry
{
type
actionKey
struct
{
Submap
string
Dispatcher
string
Arg
string
}
seen
:=
make
(
map
[
actionKey
]
int
)
var
result
[]
bindEntry
for
_
,
e
:=
range
entries
{
ak
:=
actionKey
{
e
.
Submap
,
e
.
Dispatcher
,
e
.
Arg
}
if
idx
,
ok
:=
seen
[
ak
];
ok
{
result
[
idx
]
.
Key
+=
" / "
+
e
.
Key
if
e
.
IsDescription
&&
!
result
[
idx
]
.
IsDescription
{
result
[
idx
]
.
Action
=
e
.
Action
result
[
idx
]
.
IsDescription
=
true
}
}
else
{
seen
[
ak
]
=
len
(
result
)
result
=
append
(
result
,
e
)
}
}
return
result
}
// Форматирование с цветами
var
(
cMod
=
color
.
New
(
color
.
FgCyan
)
.
SprintFunc
()
cKey
=
color
.
New
(
color
.
Bold
)
.
SprintFunc
()
cPlus
=
color
.
New
(
color
.
Faint
)
.
SprintFunc
()
cDesc
=
color
.
New
(
color
.
FgGreen
)
.
SprintFunc
()
cDisp
=
color
.
New
(
color
.
FgYellow
)
.
SprintFunc
()
cHeader
=
color
.
New
(
color
.
FgCyan
,
color
.
Bold
)
.
SprintFunc
()
cLine
=
color
.
New
(
color
.
Faint
)
.
SprintFunc
()
)
func
colorizeKey
(
key
string
)
string
{
// Обработка объединённых ключей через " / "
variants
:=
strings
.
Split
(
key
,
" / "
)
colored
:=
make
([]
string
,
len
(
variants
))
for
i
,
v
:=
range
variants
{
colored
[
i
]
=
colorizeSingleKey
(
v
)
}
return
strings
.
Join
(
colored
,
cPlus
(
" / "
))
}
var
modNames
=
map
[
string
]
bool
{
"SUPER"
:
true
,
"ALT"
:
true
,
"CTRL"
:
true
,
"SHIFT"
:
true
}
func
colorizeSingleKey
(
key
string
)
string
{
parts
:=
strings
.
Split
(
key
,
" + "
)
colored
:=
make
([]
string
,
len
(
parts
))
for
i
,
p
:=
range
parts
{
if
modNames
[
p
]
{
colored
[
i
]
=
cMod
(
p
)
}
else
{
colored
[
i
]
=
cKey
(
p
)
}
}
return
strings
.
Join
(
colored
,
cPlus
(
" + "
))
}
func
colorizeAction
(
action
string
,
isDesc
bool
)
string
{
if
isDesc
{
return
cDesc
(
action
)
}
parts
:=
strings
.
SplitN
(
action
,
" "
,
2
)
disp
:=
cDisp
(
parts
[
0
])
if
len
(
parts
)
>
1
{
return
disp
+
" "
+
parts
[
1
]
}
return
disp
}
func
formatHeader
(
title
string
)
string
{
line
:=
"── "
+
cHeader
(
title
)
+
" "
pad
:=
50
-
4
-
utf8
.
RuneCountInString
(
title
)
if
pad
>
0
{
line
+=
cLine
(
strings
.
Repeat
(
"─"
,
pad
))
}
return
line
}
func
HyprlandBindsCommand
(
ctx
context
.
Context
,
cmd
*
cli
.
Command
)
error
{
manager
,
err
:=
GetHyprlandManager
(
ctx
)
if
err
!=
nil
{
return
err
}
binds
,
err
:=
manager
.
GetBinds
()
if
err
!=
nil
{
return
err
}
entries
:=
groupBinds
(
binds
,
cmd
.
Bool
(
"all"
))
if
config
.
IsJSON
(
cmd
)
{
jsonItems
:=
make
([]
BindJSON
,
len
(
entries
))
for
i
,
e
:=
range
entries
{
jsonItems
[
i
]
=
BindJSON
{
Submap
:
e
.
Submap
,
Key
:
e
.
Key
,
Dispatcher
:
e
.
Dispatcher
,
Arg
:
e
.
Arg
,
}
if
e
.
IsDescription
{
jsonItems
[
i
]
.
Description
=
e
.
Action
}
}
return
ui
.
PrintJSON
(
jsonItems
)
}
// Группируем по submap
type
submapData
struct
{
name
string
entries
[]
bindEntry
}
var
submaps
[]
submapData
submapIdx
:=
make
(
map
[
string
]
int
)
for
_
,
e
:=
range
entries
{
if
idx
,
ok
:=
submapIdx
[
e
.
Submap
];
ok
{
submaps
[
idx
]
.
entries
=
append
(
submaps
[
idx
]
.
entries
,
e
)
}
else
{
submapIdx
[
e
.
Submap
]
=
len
(
submaps
)
submaps
=
append
(
submaps
,
submapData
{
name
:
e
.
Submap
,
entries
:
[]
bindEntry
{
e
}})
}
}
for
i
,
sg
:=
range
submaps
{
if
i
>
0
{
fmt
.
Println
()
}
title
:=
"DEFAULT"
if
sg
.
name
!=
""
{
title
=
strings
.
ToUpper
(
sg
.
name
)
}
fmt
.
Println
(
formatHeader
(
title
))
maxKeyLen
:=
0
for
_
,
e
:=
range
sg
.
entries
{
if
n
:=
utf8
.
RuneCountInString
(
e
.
Key
);
n
>
maxKeyLen
{
maxKeyLen
=
n
}
}
for
_
,
e
:=
range
sg
.
entries
{
padding
:=
maxKeyLen
-
utf8
.
RuneCountInString
(
e
.
Key
)
fmt
.
Printf
(
" %s%*s %s
\n
"
,
colorizeKey
(
e
.
Key
),
padding
,
""
,
colorizeAction
(
e
.
Action
,
e
.
IsDescription
),
)
}
}
return
nil
}
hyprland/commands.go
View file @
18c53439
...
...
@@ -49,6 +49,19 @@ func CommandList() *cli.Command {
Action
:
HyprlandFixConfigCommand
,
},
{
Name
:
"binds"
,
Usage
:
locale
.
T
(
"Show keybindings"
),
Flags
:
[]
cli
.
Flag
{
&
cli
.
BoolFlag
{
Name
:
"all"
,
Usage
:
locale
.
T
(
"Show all binds including hardware keys (XF86)"
),
Aliases
:
[]
string
{
"a"
},
},
config
.
FormatFlag
,
},
Action
:
HyprlandBindsCommand
,
},
{
Name
:
"sync-xkb-layouts"
,
Usage
:
locale
.
T
(
"Sync layouts with xkb"
),
Action
:
HyprlandSyncSystemLayouts
,
...
...
ui/json.go
View file @
18c53439
...
...
@@ -2,7 +2,7 @@ package ui
import
(
"encoding/json"
"
fmt
"
"
os
"
)
type
JSONItem
struct
{
...
...
@@ -25,10 +25,8 @@ func TreeItemsToJSON(items []TreeItem) []JSONItem {
}
func
PrintJSON
(
data
interface
{})
error
{
output
,
err
:=
json
.
MarshalIndent
(
data
,
""
,
" "
)
if
err
!=
nil
{
return
err
}
fmt
.
Println
(
string
(
output
))
return
nil
enc
:=
json
.
NewEncoder
(
os
.
Stdout
)
enc
.
SetIndent
(
""
,
" "
)
enc
.
SetEscapeHTML
(
false
)
return
enc
.
Encode
(
data
)
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment