diff --git a/config.json b/config.json index b2ec947..346d340 100644 --- a/config.json +++ b/config.json @@ -14,6 +14,12 @@ "CSVFile": "gleise_zahlen.csv", "Default_Variants": ["hoch"], "Default_Language": "dt" + }, + { + "Name": "Module", + "Path": "module", + "CSVFile": "module_dt.csv", + "Default_Language": "dt" } ] } diff --git a/go.mod b/go.mod index 29275ef..0a342b5 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,12 @@ module main go 1.24.1 -require github.com/posener/complete v1.2.3 +require ( + github.com/peterh/liner v1.2.2 + golang.org/x/text v0.23.0 +) require ( - github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-multierror v1.0.0 // indirect + github.com/mattn/go-runewidth v0.0.3 // indirect + golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect ) diff --git a/go.sum b/go.sum index e6ca257..9219db1 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,8 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= +github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= diff --git a/main.go b/main.go index e135a99..6db0d75 100644 --- a/main.go +++ b/main.go @@ -7,22 +7,26 @@ import ( "io/ioutil" "log" "os" + "os/exec" "strings" + + "github.com/peterh/liner" + "golang.org/x/text/unicode/norm" ) // Config represents the structure of the config.json file. type Config struct { - BaseDir string `json:"Base_Dir"` - Modules []Module `json:"Modules"` + BaseDir string `json:"Base_Dir"` + Modules []Module `json:"Modules"` } // Module represents a module entry in the config.json file. type Module struct { - Name string `json:"Name"` - Path string `json:"Path"` - CSVFile string `json:"CSVFile"` - DefaultLanguage string `json:"Default_Language"` - DefaultVariants []string `json:"Default_Variants"` + Name string `json:"Name"` + Path string `json:"Path"` + CSVFile string `json:"CSVFile"` + DefaultLanguage string `json:"Default_Language"` + DefaultVariants []string `json:"Default_Variants"` } func main() { @@ -40,39 +44,217 @@ func main() { log.Fatalf("Error parsing JSON: %v", err) } - // Print the display texts of all modules as a comma-separated list - fmt.Println("Display Texts of Modules:") - for _, module := range config.Modules { - displayTexts := []string{} - csvFilePath := config.BaseDir + "/" + module.CSVFile + fmt.Println("Blechelse - waiting for your orders") + interactiveMode(config) +} - // Open and read the CSV file - csvFile, err := os.Open(csvFilePath) +// Interactive mode with tab autocomplete and playing audio +func interactiveMode(config Config) { + line := liner.NewLiner() + defer line.Close() + + line.SetCtrlCAborts(true) + displayTexts := collectAllDisplayTexts(config) + + // Setting up the autocomplete feature + line.SetCompleter(func(line string) (c []string) { + // Split the input line into parts based on commas + words := strings.Split(line, ",") + lastWord := words[len(words)-1] + + // Autocomplete only the last word + for _, text := range displayTexts { + if strings.HasPrefix(strings.ToLower(text), strings.ToLower(lastWord)) { + c = append(c, text) + } + } + return + }) + + var currentInput []string // Store entered words + for { + // Print the current input prompt above the line + printPrompt(currentInput) + + // Read user input + inputText, err := line.Prompt("Enter text (or type 'exit' to quit, 'play' to trigger playback): ") if err != nil { - log.Fatalf("Error opening CSV file %s: %v", csvFilePath, err) - } - defer csvFile.Close() - - reader := csv.NewReader(csvFile) - records, err := reader.ReadAll() - if err != nil { - log.Fatalf("Error reading CSV file %s: %v", csvFilePath, err) + fmt.Println("Error reading input:", err) + continue } - // Ignore the first line - if len(records) > 1 { - records = records[1:] + inputText = strings.TrimSpace(inputText) + + // Exit condition + if inputText == "exit" { + fmt.Println("Goodbye!") + break } - for _, record := range records { - displayTexts = append(displayTexts, record[1]) // Assuming the display text is in the second column (index 1) + // If "play" is typed, trigger audio playback + if inputText == "play" { + if len(currentInput) > 0 { + // Concatenate the current input for display and play each part individually + concatenatedPrompt := strings.Join(currentInput, " ") + fmt.Printf("Playing: %s\n", concatenatedPrompt) + // Play each part individually in the order they were added + autocompleteAndPlay(config, currentInput) + currentInput = nil // Clear the input after playing + } else { + fmt.Println("No input to play.") + } + } else if len(inputText) > 0 { + // If it's not "play", it's a part of the input, so add to the current input + currentInput = append(currentInput, inputText) } - - fmt.Printf("- Module Name: %s (%s)\n", module.Name, commaSeparated(displayTexts)) } } -// Helper function to join a slice of strings with commas -func commaSeparated(slice []string) string { - return fmt.Sprintf("[%s]", strings.Join(slice, ", ")) +// Print the current accumulated prompt above the input line +func printPrompt(currentInput []string) { + fmt.Println("Current prompt:") + for i, word := range currentInput { + fmt.Printf("%d. %s\n", i+1, word) + } +} + +// Collect all display texts for autocomplete +func collectAllDisplayTexts(config Config) []string { + var displayTexts []string + for _, module := range config.Modules { + // Unpack both return values (displayTexts and fileNames) + moduleDisplayTexts, _ := readDisplayTextsAndFileNames(config.BaseDir + "/" + module.CSVFile) + displayTexts = append(displayTexts, moduleDisplayTexts...) + } + return displayTexts +} + +// Function to collect all display texts and play matching files +func autocompleteAndPlay(config Config, inputText []string) { + var matchingFiles []string // Store matching files in the order of inputText + var moduleNames []string // Store corresponding module names for each matched file + + // Normalize each input word (e.g., convert umlauts to standard form) + for _, part := range inputText { + normalizedInputText := norm.NFC.String(part) + + // Collect matching filenames from the second column (display text) and first column (filenames) + for _, module := range config.Modules { + displayTexts, fileNames := readDisplayTextsAndFileNames(config.BaseDir + "/" + module.CSVFile) + for i, text := range displayTexts { + // Normalize display text before comparing and remove any trailing spaces + normalizedText := norm.NFC.String(strings.TrimSpace(text)) + + // Explicitly compare both normalized strings (case-sensitive) + if normalizedText == normalizedInputText { + matchingFiles = append(matchingFiles, fileNames[i]) + moduleNames = append(moduleNames, module.Name) // Track the module that matched + } + } + } + } + + // If no matching filenames found, prompt again + if len(matchingFiles) == 0 { + fmt.Println("No matching display text found.") + return + } + + // Debugging: print matched files and their corresponding modules + fmt.Println("Matched files:") + for i, fileName := range matchingFiles { + fmt.Printf(" - File: %s, Module: %s\n", fileName, moduleNames[i]) + } + + // Play matching audio files in the exact order they were entered + for i, fileName := range matchingFiles { + // Get the module name associated with this matched file + moduleName := moduleNames[i] + + // Construct the correct audio path using the module information + audioPath := constructAudioPath(config, fileName, moduleName) + + // Debugging: print the constructed audio path + fmt.Printf("Attempting to play audio from path: %s\n", audioPath) + + // Check if the file exists before trying to play it + if _, err := os.Stat(audioPath); os.IsNotExist(err) { + fmt.Printf("File does not exist: %s\n", audioPath) + continue + } + + // Play the audio file + err := playAudioFiles(audioPath) + if err != nil { + fmt.Printf("Error playing audio: %v\n", err) + } + } +} + +// Function to construct the audio file path with language and variant considerations +func constructAudioPath(config Config, fileName, moduleName string) string { + var audioPath string + var languagePart, variantPart string + + // Find the module for which we need to construct the path + var selectedModule Module + for _, module := range config.Modules { + if module.Name == moduleName { + selectedModule = module + break + } + } + + // If a language is set in the selected module, use it + if len(selectedModule.DefaultLanguage) > 0 { + languagePart = selectedModule.DefaultLanguage + "/" + } + + // If a variant is set in the selected module, use it + if len(selectedModule.DefaultVariants) > 0 { + variantPart = selectedModule.DefaultVariants[0] + "/" + } + + // Construct the path + audioPath = fmt.Sprintf("%s/%s%s/%s%s", config.BaseDir, languagePart, selectedModule.Path, variantPart, fileName) + return audioPath +} + +// Function to read display texts (second column) and filenames (first column) from a CSV file +func readDisplayTextsAndFileNames(filePath string) ([]string, []string) { + file, err := os.Open(filePath) + if err != nil { + log.Fatalf("Error opening CSV file: %v", err) + } + defer file.Close() + + reader := csv.NewReader(file) + records, err := reader.ReadAll() + if err != nil { + log.Fatalf("Error reading CSV records: %v", err) + } + + var displayTexts, fileNames []string + for _, record := range records { + if len(record) > 1 { + fileNames = append(fileNames, record[0]) // First column is the filename + displayTexts = append(displayTexts, record[1]) // Second column is the display text + } + } + return displayTexts, fileNames +} + +// Function to play audio files based on the file name +func playAudioFiles(filePath string) error { + // Debugging: print the file path being played + fmt.Printf("Playing audio from: %s\n", filePath) + + cmd := exec.Command("ffplay", "-nodisp", "-autoexit", filePath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return fmt.Errorf("Error playing audio file: %v", err) + } + return nil }