diff --git a/dot_config/hypr/utils/cycle_kb_layout b/dot_config/hypr/utils/cycle_kb_layout index f712491..59bf48f 100755 Binary files a/dot_config/hypr/utils/cycle_kb_layout and b/dot_config/hypr/utils/cycle_kb_layout differ diff --git a/dot_config/hypr/utils/src/cycle_kb_layout.go b/dot_config/hypr/utils/src/cycle_kb_layout.go index c699450..ab0a6e9 100644 --- a/dot_config/hypr/utils/src/cycle_kb_layout.go +++ b/dot_config/hypr/utils/src/cycle_kb_layout.go @@ -11,6 +11,7 @@ import ( const valueToChange = "kb_layout" +// readLayouts reads the available keyboard layouts from a file. func readLayouts(layoutFile string) ([]string, error) { file, err := os.Open(layoutFile) if err != nil { @@ -31,16 +32,24 @@ func readLayouts(layoutFile string) ([]string, error) { return nil, err } - // To prevent index out of range error in toggleLine function - layouts = append(layouts, layouts[0]) // Cycle back to the first layout + // To prevent index out of range error in toggleLine function, + // we add the first layout to the end to create a cycle. + if len(layouts) > 0 { + layouts = append(layouts, layouts[0]) + } else { + return nil, fmt.Errorf("no layouts found in %s", layoutFile) + } + return layouts, nil } +// isTargetLine checks if a line contains the configuration key we want to change. func isTargetLine(line string) bool { return strings.Contains(line, valueToChange) } +// readCurrentConfig reads all lines from the configuration file. func readCurrentConfig(configFile string) ([]string, error) { file, err := os.Open(configFile) if err != nil { @@ -54,13 +63,10 @@ func readCurrentConfig(configFile string) ([]string, error) { lines = append(lines, scanner.Text()) } - if err := scanner.Err(); err != nil { - return nil, err - } - - return lines, nil + return lines, scanner.Err() } +// writeNewConfig writes the updated lines back to the configuration file. func writeNewConfig(lines []string, configFile string) error { file, err := os.Create(configFile) if err != nil { @@ -68,39 +74,55 @@ func writeNewConfig(lines []string, configFile string) error { } defer file.Close() + writer := bufio.NewWriter(file) for _, line := range lines { - _, err := file.WriteString(line + "\n") - if err != nil { + if _, err := writer.WriteString(line + "\n"); err != nil { return err } } - return nil + return writer.Flush() } +// toggleLine finds the current layout in a line, and replaces it with the next one from the list. func toggleLine(line string, layouts []string) string { var builder strings.Builder + // Preserve indentation indentLength := len(line) - len(strings.TrimLeft(line, " \t")) - builder.Grow(len(line)) // Preallocate enough space to avoid multiple allocations - builder.WriteString(strings.Repeat(" ", indentLength)) + // Separate code from comments codeAndComments := strings.SplitN(line, "#", 2) code := strings.TrimSpace(codeAndComments[0]) tokens := strings.Fields(code) currentLayout := tokens[len(tokens)-1] - index := (strings.Index(strings.Join(layouts, " "), currentLayout)/len(currentLayout) + 1) % len(layouts) - tokens[len(tokens)-1] = layouts[index] + + // Find the index of the current layout and get the next one + currentIndex := -1 + for i, layout := range layouts { + if layout == currentLayout { + currentIndex = i + break + } + } + + // The last element is a duplicate of the first, so (currentIndex+1) is always safe. + if currentIndex != -1 { + tokens[len(tokens)-1] = layouts[currentIndex+1] + } + builder.WriteString(strings.Join(tokens, " ")) + // Re-append the comment if it exists if len(codeAndComments) > 1 { builder.WriteString(" # ") - builder.WriteString(codeAndComments[1]) + builder.WriteString(strings.TrimSpace(codeAndComments[1])) } return builder.String() } +// cycleKbLayout orchestrates the entire process of reading, modifying, and writing the config. func cycleKbLayout(configFile, layoutFile string) error { layouts, err := readLayouts(layoutFile) if err != nil { @@ -123,26 +145,60 @@ func cycleKbLayout(configFile, layoutFile string) error { return writeNewConfig(newLines, configFile) } +// expandPath resolves file paths by handling the '~' shorthand for the home directory +// and converting relative paths to absolute paths based on the executable's location. +func expandPath(path string) (string, error) { + // --- Step 1: Handle tilde expansion --- + if strings.HasPrefix(path, "~") { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("could not get user home directory: %w", err) + } + // Replace "~" with the actual home directory path. + // filepath.Join correctly handles path separators. + path = filepath.Join(homeDir, path[1:]) + } + + // --- Step 2: Handle relative paths --- + // If the path is still not absolute after potential tilde expansion, + // join it with the executable's directory. + if !filepath.IsAbs(path) { + exeDir, err := filepath.Abs(filepath.Dir(os.Args[0])) + if err != nil { + return "", fmt.Errorf("could not get executable directory: %w", err) + } + path = filepath.Join(exeDir, path) + } + + return path, nil +} + + // Define flags for the input files -var configFile = flag.String("c", "inputs.conf", "Path to the configuration file") -var layoutFile = flag.String("l", "kb_layouts.txt", "Path to the keyboard layout file") +var configFile = flag.String("c", "inputs.conf", "Path to the configuration file (e.g., ~/.config/hypr/inputs.conf)") +var layoutFile = flag.String("l", "kb_layouts.txt", "Path to the keyboard layout file (e.g., ~/.config/hypr/kb_layouts.txt)") func main() { // Parse command-line flags flag.Parse() - // Determine the absolute paths based on provided flags - hyprDir, err := filepath.Abs(filepath.Dir(os.Args[0])) + // Resolve the full, absolute path for the config file, handling '~' + configFilePath, err := expandPath(*configFile) if err != nil { - fmt.Println("Error getting directory: %v", err) + fmt.Printf("Error processing config file path: %v\n", err) + return } - configFilePath := filepath.Join(hyprDir, *configFile) - layoutFilePath := filepath.Join(hyprDir, *layoutFile) + // Resolve the full, absolute path for the layout file, handling '~' + layoutFilePath, err := expandPath(*layoutFile) + if err != nil { + fmt.Printf("Error processing layout file path: %v\n", err) + return + } - // Call the cycleKbLayout function with the paths from the flags + // Call the main logic with the fully resolved paths if err := cycleKbLayout(configFilePath, layoutFilePath); err != nil { - fmt.Println("Error cycling keyboard layout: %v", err) + fmt.Printf("Error cycling keyboard layout: %v\n", err) } } diff --git a/dot_config/hypr/utils/src/go.mod b/dot_config/hypr/utils/src/go.mod index 627ad48..d424aa9 100644 --- a/dot_config/hypr/utils/src/go.mod +++ b/dot_config/hypr/utils/src/go.mod @@ -1,3 +1,3 @@ module cycle_kb_layout -go 1.21.8 +go 1.24.4