187 lines
6.0 KiB
Go
187 lines
6.0 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"log/slog"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/fenrir/internal/command"
|
|
"git.stormux.org/storm/fenrir/internal/i18n"
|
|
"git.stormux.org/storm/fenrir/internal/input"
|
|
"git.stormux.org/storm/fenrir/internal/scripts"
|
|
)
|
|
|
|
// These are the complete message IDs from Python's tutorial command and
|
|
// HelpManager. Keep named fields intact until after gettext lookup.
|
|
const tutorialEntry = "Entering tutorial mode. In this mode commands are described but not executed. Use up and down to browse actions and left and right to switch between Active actions, Unbound actions, and Plugins. Press Space to repeat the current item. To exit tutorial mode press Fenrir+F1 or Escape. Active actions."
|
|
const tutorialExit = "Exiting tutorial mode. To enter tutorial mode again press Fenrir+F1"
|
|
const tutorialReleaseFailed = "Unable to exit tutorial mode because exclusive keyboard capture could not be released. Press Fenrir+F1 or Escape to try again."
|
|
const tutorialCaptureFailed = "Warning: full keyboard capture is unavailable. Unbound keys may reach the active application."
|
|
const tutorialItem = "{command_name}, Shortcuts {command_shortcuts}, Description {command_description}"
|
|
|
|
type tutorialMode struct {
|
|
sections [3][]command.ID
|
|
section int
|
|
index int
|
|
shortcuts map[command.ID][]string
|
|
names map[command.ID]string
|
|
}
|
|
|
|
func tutorial_key_name(name string) string {
|
|
name = strings.ToLower(name)
|
|
name = strings.ReplaceAll(name, "key_kp", "keypad ")
|
|
name = strings.TrimPrefix(name, "key_")
|
|
return i18n.T(name)
|
|
}
|
|
|
|
func tutorial_shortcut(keys []string, presses int, scriptKeys []string) string {
|
|
keys = slices.Clone(keys)
|
|
var ordered []string
|
|
for _, modifier := range []string{"KEY_FENRIR", "KEY_SCRIPT", "KEY_CTRL", "KEY_SHIFT", "KEY_ALT", "KEY_META"} {
|
|
if index := slices.Index(keys, modifier); index >= 0 {
|
|
ordered = append(ordered, modifier)
|
|
keys = slices.Delete(keys, index, index+1)
|
|
}
|
|
}
|
|
keys = append(ordered, keys...)
|
|
var names []string
|
|
for _, key := range keys {
|
|
if key == "KEY_SCRIPT" {
|
|
var physical []string
|
|
for _, scriptKey := range scriptKeys {
|
|
physical = append(physical, tutorial_key_name(scriptKey))
|
|
}
|
|
names = append(names, strings.Join(physical, " / "))
|
|
} else {
|
|
names = append(names, tutorial_key_name(key))
|
|
}
|
|
}
|
|
prefix := ""
|
|
if presses > 1 {
|
|
prefix = strings.ReplaceAll(i18n.T("{} times "), "{}", strconv.Itoa(presses))
|
|
}
|
|
return prefix + strings.Join(names, ", ")
|
|
}
|
|
|
|
func new_tutorial(bindings []input.Binding, installed map[command.ID]scripts.Script, scriptKeys []string) *tutorialMode {
|
|
mode := &tutorialMode{index: -1, shortcuts: map[command.ID][]string{}, names: map[command.ID]string{}}
|
|
for _, binding := range bindings {
|
|
keys := make([]string, len(binding.Keys))
|
|
for index, key := range binding.Keys {
|
|
keys[index] = "KEY_" + key
|
|
}
|
|
for index, id := range []command.ID{binding.Command, binding.DoubleCommand, binding.TripleCommand} {
|
|
if id != "" {
|
|
mode.shortcuts[id] = append(mode.shortcuts[id], tutorial_shortcut(keys, index+1, scriptKeys))
|
|
}
|
|
}
|
|
}
|
|
for _, id := range command.IDs() {
|
|
section := 0
|
|
if len(mode.shortcuts[id]) == 0 {
|
|
section = 1
|
|
}
|
|
mode.sections[section] = append(mode.sections[section], id)
|
|
}
|
|
for id, script := range installed {
|
|
mode.names[id] = strings.ReplaceAll(strings.ToLower(script.Name), "_", " ")
|
|
mode.shortcuts[id] = []string{tutorial_shortcut(script.Keys, 1, scriptKeys)}
|
|
mode.sections[2] = append(mode.sections[2], id)
|
|
}
|
|
slices.Sort(mode.sections[2])
|
|
return mode
|
|
}
|
|
|
|
func (mode *tutorialMode) describe(id command.ID) string {
|
|
name := mode.names[id]
|
|
if name == "" {
|
|
name = strings.ReplaceAll(string(id), "_", " ")
|
|
}
|
|
shortcuts := strings.Join(mode.shortcuts[id], "; ")
|
|
if shortcuts == "" {
|
|
shortcuts = i18n.T("unbound")
|
|
}
|
|
return strings.NewReplacer("{command_name}", name, "{command_shortcuts}", shortcuts,
|
|
"{command_description}", command.Description(id)).Replace(i18n.T(tutorialItem))
|
|
}
|
|
|
|
func (mode *tutorialMode) current() string {
|
|
if mode.index < 0 || len(mode.sections[mode.section]) == 0 {
|
|
return i18n.T([]string{"Active actions", "Unbound actions", "Plugins"}[mode.section])
|
|
}
|
|
return mode.describe(mode.sections[mode.section][mode.index])
|
|
}
|
|
|
|
func (mode *tutorialMode) handle(key input.LearnKey) (string, bool) {
|
|
if key.Command == command.Help || key.Code == input.KeyEsc {
|
|
return "", true
|
|
}
|
|
if key.Bare {
|
|
switch key.Code {
|
|
case input.KeyLeft, input.KeyRight:
|
|
delta := 1
|
|
if key.Code == input.KeyLeft {
|
|
delta = -1
|
|
}
|
|
mode.section = (mode.section + delta + 3) % 3
|
|
mode.index = -1
|
|
return mode.current(), false
|
|
case input.KeyUp, input.KeyDown:
|
|
count := len(mode.sections[mode.section])
|
|
if count > 0 {
|
|
if mode.index < 0 {
|
|
mode.index = 0
|
|
if key.Code == input.KeyUp {
|
|
mode.index = count - 1
|
|
}
|
|
} else if key.Code == input.KeyUp {
|
|
mode.index = (mode.index + count - 1) % count
|
|
} else {
|
|
mode.index = (mode.index + 1) % count
|
|
}
|
|
}
|
|
return mode.current(), false
|
|
case input.KeySpace:
|
|
return mode.current(), false
|
|
}
|
|
}
|
|
var names []string
|
|
for _, code := range append(slices.Clone(key.Held), key.Code) {
|
|
names = append(names, tutorial_key_name(input.Key_name(code)))
|
|
}
|
|
text := strings.Join(names, ", ")
|
|
if key.Command != "" && key.Command != command.Unbound {
|
|
text += ". " + mode.describe(key.Command)
|
|
} else {
|
|
text += ". " + i18n.T("unbound")
|
|
}
|
|
return text, false
|
|
}
|
|
|
|
func (ui *internalUI) open_tutorial(bindings []input.Binding, installed map[command.ID]scripts.Script, scriptKeys []string) {
|
|
if ui.active {
|
|
return
|
|
}
|
|
if _, err := ui.begin(""); err != nil {
|
|
slog.Warn("tutorial capture failed", "error", err, "at", time.Now())
|
|
ui.voice.Say(i18n.T(tutorialCaptureFailed))
|
|
return
|
|
}
|
|
ui.tutorial = new_tutorial(bindings, installed, scriptKeys)
|
|
ui.voice.Say(i18n.T(tutorialEntry))
|
|
}
|
|
|
|
func (ui *internalUI) handle_tutorial(key input.LearnKey) {
|
|
text, close := ui.tutorial.handle(key)
|
|
if close {
|
|
ui.close()
|
|
return
|
|
}
|
|
ui.voice.Say(text)
|
|
}
|