Skip to content

Commit cdb6b44

Browse files
Xaytonlucarin91
andauthored
New commands to set keyboard layout (#663)
Co-authored-by: Luca Rinaldi <lucarin@protonmail.com>
1 parent 60472f8 commit cdb6b44

File tree

4 files changed

+199
-0
lines changed

4 files changed

+199
-0
lines changed

cmd/arduino-app-cli/board/board.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ func NewBoardCmd() *cobra.Command {
7070
fsCmd.AddCommand(newDisableNetworkModeCmd())
7171
fsCmd.AddCommand(newNetworkModeStatusCmd())
7272

73+
fsCmd.AddCommand(listKeyboardLayouts())
74+
fsCmd.AddCommand(getKeyboardLayout())
75+
fsCmd.AddCommand(setKeyboardLayout())
76+
7377
return fsCmd
7478
}
7579

@@ -247,3 +251,85 @@ func newNetworkModeStatusCmd() *cobra.Command {
247251
},
248252
}
249253
}
254+
255+
func getKeyboardLayout() *cobra.Command {
256+
return &cobra.Command{
257+
Use: "get-keyboard-layout",
258+
Short: "Returns the current system keyboard layout code",
259+
Args: cobra.ExactArgs(0),
260+
RunE: func(cmd *cobra.Command, args []string) error {
261+
conn := cmd.Context().Value(remoteConnKey).(remote.RemoteConn)
262+
263+
layoutCode, err := board.GetKeyboardLayout(cmd.Context(), conn)
264+
if err != nil {
265+
return fmt.Errorf("failed: %w", err)
266+
}
267+
feedback.Printf("Layout: %s", layoutCode)
268+
269+
return nil
270+
},
271+
}
272+
}
273+
274+
func setKeyboardLayout() *cobra.Command {
275+
return &cobra.Command{
276+
Use: "set-keyboard-layout <layout>",
277+
Short: "Saves and applies the current system keyboard layout",
278+
Args: cobra.ExactArgs(1),
279+
RunE: func(cmd *cobra.Command, args []string) error {
280+
conn := cmd.Context().Value(remoteConnKey).(remote.RemoteConn)
281+
layoutCode := args[0]
282+
283+
err := validateKeyboardLayoutCode(conn, layoutCode)
284+
if err != nil {
285+
return fmt.Errorf("failed: %w", err)
286+
}
287+
288+
err = board.SetKeyboardLayout(cmd.Context(), conn, layoutCode)
289+
if err != nil {
290+
return fmt.Errorf("failed: %w", err)
291+
}
292+
293+
feedback.Printf("New layout applied: %s", layoutCode)
294+
return nil
295+
},
296+
}
297+
}
298+
299+
func validateKeyboardLayoutCode(conn remote.RemoteConn, layoutCode string) error {
300+
// Make sure the input layout code is in the list of valid ones
301+
layouts, err := board.ListKeyboardLayouts(conn)
302+
if err != nil {
303+
return fmt.Errorf("failed to fetch valid layouts: %w", err)
304+
}
305+
306+
for _, layout := range layouts {
307+
if layout.LayoutId == layoutCode {
308+
return nil
309+
}
310+
}
311+
312+
return fmt.Errorf("invalid layout code: %s", layoutCode)
313+
}
314+
315+
func listKeyboardLayouts() *cobra.Command {
316+
return &cobra.Command{
317+
Use: "list-keyboard-layouts",
318+
Short: "Returns the list of valid keyboard layouts, with a description",
319+
Args: cobra.ExactArgs(0),
320+
RunE: func(cmd *cobra.Command, args []string) error {
321+
conn := cmd.Context().Value(remoteConnKey).(remote.RemoteConn)
322+
323+
layouts, err := board.ListKeyboardLayouts(conn)
324+
if err != nil {
325+
return fmt.Errorf("failed: %w", err)
326+
}
327+
328+
for _, layout := range layouts {
329+
feedback.Printf("%s, %s", layout.LayoutId, layout.Description)
330+
}
331+
332+
return nil
333+
},
334+
}
335+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
arduino ALL=(ALL) NOPASSWD: /usr/local/bin/arduino-set-keyboard-layout
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/sh
2+
3+
LAYOUT="$1"
4+
if [ -z "$LAYOUT" ]; then
5+
echo "No layout provided"
6+
exit 1
7+
fi
8+
9+
# Update the "XKBLAYOUT" line in /etc/default/keyboard
10+
sed -i "s/^XKBLAYOUT=\"[^\"]*\"/XKBLAYOUT=\"$LAYOUT\"/" /etc/default/keyboard
11+
12+
# Then apply the changes
13+
udevadm trigger --subsystem-match=input --action=change

pkg/board/board_system.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package board
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/bcmi-labs/orchestrator/pkg/board/remote"
10+
)
11+
12+
type KeyboardLayout struct {
13+
LayoutId string
14+
Description string
15+
}
16+
17+
func GetKeyboardLayout(ctx context.Context, conn remote.RemoteConn) (string, error) {
18+
cmd := conn.GetCmd("localectl", "status")
19+
output, err := cmd.Output(ctx)
20+
if err != nil {
21+
return "", fmt.Errorf("failed to get the keyboard layout: %w", err)
22+
}
23+
24+
lines := strings.Split(string(output), "\n")
25+
26+
// Loop through each line of the output to find "X11 Layout"
27+
for _, line := range lines {
28+
line = strings.TrimSpace(line)
29+
if strings.HasPrefix(line, "X11 Layout:") {
30+
parts := strings.SplitN(line, ":", 2)
31+
if len(parts) != 2 {
32+
return "", fmt.Errorf("failed to get the keyboard layout, parsing error")
33+
}
34+
35+
layout := strings.TrimSpace(parts[1])
36+
return layout, nil
37+
}
38+
}
39+
40+
return "", fmt.Errorf("failed to get the keyboard layout, layout not found")
41+
}
42+
43+
func SetKeyboardLayout(ctx context.Context, conn remote.RemoteConn, layoutCode string) error {
44+
err := conn.GetCmd("sudo", "/usr/local/bin/arduino-set-keyboard-layout", layoutCode).Run(ctx)
45+
if err != nil {
46+
return fmt.Errorf("failed to start command: %w", err)
47+
}
48+
49+
return nil
50+
}
51+
52+
func ListKeyboardLayouts(conn remote.RemoteConn) ([]KeyboardLayout, error) {
53+
// The file contains multiple things, including the list of valid keyboard layouts.
54+
r, err := conn.ReadFile("/usr/share/X11/xkb/rules/base.lst")
55+
if err != nil {
56+
return nil, fmt.Errorf("failed opening the keyboard layouts file: %w", err)
57+
}
58+
defer r.Close()
59+
60+
var layouts []KeyboardLayout
61+
62+
scanner := bufio.NewScanner(r)
63+
insideLayoutSection := false
64+
65+
for scanner.Scan() {
66+
line := scanner.Text()
67+
68+
if strings.HasPrefix(line, "! layout") {
69+
insideLayoutSection = true
70+
continue
71+
}
72+
73+
if !insideLayoutSection {
74+
continue
75+
}
76+
77+
// If the line is empty or starts with "!", it's the end of the layout section
78+
if line == "" || strings.HasPrefix(line, "!") {
79+
break
80+
}
81+
82+
// Split the line into layout code and description
83+
parts := strings.Fields(line)
84+
if len(parts) >= 2 {
85+
layout := KeyboardLayout{
86+
LayoutId: parts[0],
87+
Description: strings.Join(parts[1:], " "),
88+
}
89+
layouts = append(layouts, layout)
90+
}
91+
}
92+
93+
// Check for scanner errors
94+
if err := scanner.Err(); err != nil {
95+
return nil, fmt.Errorf("error reading input: %v", err)
96+
}
97+
98+
return layouts, nil
99+
}

0 commit comments

Comments
 (0)