mirror of
https://github.com/anotherhadi/settuings.git
synced 2026-10-05 16:18:24 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f75c7b6c0 | ||
|
|
0f7738be68 | ||
|
|
fc35732dbf | ||
|
|
6e4f8b889c | ||
|
|
df530f39b0 |
@@ -37,6 +37,7 @@ SetTUIngs is a keyboard-driven TUI for the everyday Linux system settings you'd
|
|||||||
- **Bluetooth**: Discover, pair, connect, trust, and forget devices, and power the adapter on/off.
|
- **Bluetooth**: Discover, pair, connect, trust, and forget devices, and power the adapter on/off.
|
||||||
- **Audio**: Adjust output/input volume, mute, and pick the default speaker or mic; preview a device with a test tone or a live input-level meter; control the volume of individual apps (browser tab, Spotify, etc.) independently.
|
- **Audio**: Adjust output/input volume, mute, and pick the default speaker or mic; preview a device with a test tone or a live input-level meter; control the volume of individual apps (browser tab, Spotify, etc.) independently.
|
||||||
- **Power**: Switch power profiles (performance, balanced, power-saver) and check battery charge, health, and time remaining at a glance.
|
- **Power**: Switch power profiles (performance, balanced, power-saver) and check battery charge, health, and time remaining at a glance.
|
||||||
|
- **Inputs**: Change the keyboard layout and adjust mouse pointer speed, via `hyprctl` (Hyprland only).
|
||||||
- **Vim-like Navigation**: The entire interface is keyboard-driven with Vim-inspired shortcuts. Use `h/j/k/l` to move, `gg`/`G` to jump to the top/bottom, `/` to search, `q` to close panels, and more. All keybindings are fully customizable via the config file.
|
- **Vim-like Navigation**: The entire interface is keyboard-driven with Vim-inspired shortcuts. Use `h/j/k/l` to move, `gg`/`G` to jump to the top/bottom, `/` to search, `q` to close panels, and more. All keybindings are fully customizable via the config file.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -91,7 +92,7 @@ Flags:
|
|||||||
--add-default-config copy the default config file to the config path and exit
|
--add-default-config copy the default config file to the config path and exit
|
||||||
-c, --config string path to config file
|
-c, --config string path to config file
|
||||||
-h, --help help for settuings
|
-h, --help help for settuings
|
||||||
-p, --page string page to open at launch (About, Network, Bluetooth, Audio, Power)
|
-p, --page string page to open at launch (About, Network, Bluetooth, Audio, Power, Inputs)
|
||||||
-v, --version version for settuings
|
-v, --version version for settuings
|
||||||
```
|
```
|
||||||
<!-- endexec -->
|
<!-- endexec -->
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ require (
|
|||||||
charm.land/lipgloss/v2 v2.0.3
|
charm.land/lipgloss/v2 v2.0.3
|
||||||
github.com/anotherhadi/ilovetui v0.1.6
|
github.com/anotherhadi/ilovetui v0.1.6
|
||||||
github.com/charmbracelet/x/ansi v0.11.7
|
github.com/charmbracelet/x/ansi v0.11.7
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2
|
||||||
github.com/sirupsen/logrus v1.9.4
|
github.com/sirupsen/logrus v1.9.4
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5
|
|||||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ func CheckAvailable() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Installed reports whether wpctl is present in PATH, without probing
|
||||||
|
// PipeWire. Cheap enough to call when deciding whether to show the Audio
|
||||||
|
// page in the sidebar.
|
||||||
|
func Installed() bool {
|
||||||
|
_, err := exec.LookPath("wpctl")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func run(args ...string) (string, error) {
|
func run(args ...string) (string, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package bluetooth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
agentPath = dbus.ObjectPath("/org/anotherhadi/settuings/agent")
|
||||||
|
agentCapability = "DisplayYesNo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// agent implements the BlueZ org.bluez.Agent1 interface, auto-accepting
|
||||||
|
// every pairing prompt. `bluetoothctl pair` run non-interactively never
|
||||||
|
// registers an agent, so BlueZ has nobody to ask for PIN/passkey
|
||||||
|
// confirmation and any device that isn't "Just Works" fails with
|
||||||
|
// AuthenticationFailed. Registering this agent as the system default
|
||||||
|
// fixes that even though pairing itself still happens in a separate
|
||||||
|
// bluetoothctl process: BlueZ routes auth requests to whichever agent is
|
||||||
|
// registered as default, not to whoever initiated the pairing.
|
||||||
|
type agent struct{}
|
||||||
|
|
||||||
|
func (agent) Release() *dbus.Error { return nil }
|
||||||
|
|
||||||
|
func (agent) RequestPinCode(_ dbus.ObjectPath) (string, *dbus.Error) {
|
||||||
|
return "", dbus.NewError("org.bluez.Error.Rejected", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (agent) DisplayPinCode(_ dbus.ObjectPath, _ string) *dbus.Error { return nil }
|
||||||
|
|
||||||
|
func (agent) RequestPasskey(_ dbus.ObjectPath) (uint32, *dbus.Error) {
|
||||||
|
return 0, dbus.NewError("org.bluez.Error.Rejected", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (agent) DisplayPasskey(_ dbus.ObjectPath, _ uint32, _ uint16) *dbus.Error { return nil }
|
||||||
|
|
||||||
|
func (agent) RequestConfirmation(_ dbus.ObjectPath, _ uint32) *dbus.Error { return nil }
|
||||||
|
|
||||||
|
func (agent) RequestAuthorization(_ dbus.ObjectPath) *dbus.Error { return nil }
|
||||||
|
|
||||||
|
func (agent) AuthorizeService(_ dbus.ObjectPath, _ string) *dbus.Error { return nil }
|
||||||
|
|
||||||
|
func (agent) Cancel() *dbus.Error { return nil }
|
||||||
|
|
||||||
|
var (
|
||||||
|
agentOnce sync.Once
|
||||||
|
agentErr error
|
||||||
|
// agentConn is kept alive for the process lifetime: BlueZ calls back
|
||||||
|
// into our agent over this connection while pairing is in progress.
|
||||||
|
agentConn *dbus.Conn
|
||||||
|
)
|
||||||
|
|
||||||
|
// ensureAgent registers this process as BlueZ's default pairing agent, once
|
||||||
|
// per run. Call before any Pair.
|
||||||
|
func ensureAgent() error {
|
||||||
|
agentOnce.Do(func() {
|
||||||
|
conn, err := dbus.ConnectSystemBus()
|
||||||
|
if err != nil {
|
||||||
|
agentErr = fmt.Errorf("connect to system bus: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.Export(agent{}, agentPath, "org.bluez.Agent1"); err != nil {
|
||||||
|
agentErr = fmt.Errorf("export agent: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager := conn.Object("org.bluez", dbus.ObjectPath("/org/bluez"))
|
||||||
|
if call := manager.Call("org.bluez.AgentManager1.RegisterAgent", 0, agentPath, agentCapability); call.Err != nil {
|
||||||
|
agentErr = fmt.Errorf("register agent: %w", call.Err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if call := manager.Call("org.bluez.AgentManager1.RequestDefaultAgent", 0, agentPath); call.Err != nil {
|
||||||
|
agentErr = fmt.Errorf("request default agent: %w", call.Err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agentConn = conn
|
||||||
|
})
|
||||||
|
return agentErr
|
||||||
|
}
|
||||||
@@ -39,6 +39,14 @@ func CheckAvailable() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Installed reports whether bluetoothctl is present in PATH, without
|
||||||
|
// probing for a controller. Cheap enough to call when deciding whether to
|
||||||
|
// show the Bluetooth page in the sidebar.
|
||||||
|
func Installed() bool {
|
||||||
|
_, err := exec.LookPath("bluetoothctl")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
// run executes bluetoothctl in single-command (non-interactive) mode.
|
// run executes bluetoothctl in single-command (non-interactive) mode.
|
||||||
// bluetoothctl writes both normal output and error messages to stdout
|
// bluetoothctl writes both normal output and error messages to stdout
|
||||||
// (colored with ANSI escapes), so errors are extracted from there rather
|
// (colored with ANSI escapes), so errors are extracted from there rather
|
||||||
|
|||||||
@@ -185,10 +185,13 @@ func Scan(seconds int) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pair initiates pairing with a device. Because non-interactive
|
// Pair initiates pairing with a device, first registering settuings as
|
||||||
// bluetoothctl has no agent to answer PIN/confirmation prompts, this only
|
// BlueZ's default agent so devices that need PIN/passkey confirmation
|
||||||
// succeeds for "just works" devices (most mice, keyboards, headsets).
|
// (not just "just works" ones) can complete pairing.
|
||||||
func Pair(address string) error {
|
func Pair(address string) error {
|
||||||
|
if err := ensureAgent(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
_, err := run(actionTimeout, "pair", address)
|
_, err := run(actionTimeout, "pair", address)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,3 +59,8 @@ keybindings:
|
|||||||
power:
|
power:
|
||||||
select: "enter"
|
select: "enter"
|
||||||
refresh: "r"
|
refresh: "r"
|
||||||
|
|
||||||
|
input:
|
||||||
|
select: "enter"
|
||||||
|
reset: "r"
|
||||||
|
filter: "/"
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ type PowerKeys struct {
|
|||||||
Refresh string `mapstructure:"refresh"`
|
Refresh string `mapstructure:"refresh"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InputKeys struct {
|
||||||
|
Select string `mapstructure:"select"`
|
||||||
|
Reset string `mapstructure:"reset"`
|
||||||
|
Filter string `mapstructure:"filter"`
|
||||||
|
}
|
||||||
|
|
||||||
type Keybindings struct {
|
type Keybindings struct {
|
||||||
Global GlobalKeys `mapstructure:"global"`
|
Global GlobalKeys `mapstructure:"global"`
|
||||||
Docs DocsKeys `mapstructure:"docs"`
|
Docs DocsKeys `mapstructure:"docs"`
|
||||||
@@ -66,4 +72,5 @@ type Keybindings struct {
|
|||||||
Bluetooth BluetoothKeys `mapstructure:"bluetooth"`
|
Bluetooth BluetoothKeys `mapstructure:"bluetooth"`
|
||||||
Audio AudioKeys `mapstructure:"audio"`
|
Audio AudioKeys `mapstructure:"audio"`
|
||||||
Power PowerKeys `mapstructure:"power"`
|
Power PowerKeys `mapstructure:"power"`
|
||||||
|
Input InputKeys `mapstructure:"input"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type Icons struct {
|
|||||||
Bluetooth string
|
Bluetooth string
|
||||||
Audio string
|
Audio string
|
||||||
Power string
|
Power string
|
||||||
|
Inputs string
|
||||||
|
|
||||||
Wifi string
|
Wifi string
|
||||||
Ethernet string
|
Ethernet string
|
||||||
@@ -39,6 +40,7 @@ func Init(cfg *config.Config) {
|
|||||||
Bluetooth: " ",
|
Bluetooth: " ",
|
||||||
Audio: " ",
|
Audio: " ",
|
||||||
Power: " ",
|
Power: " ",
|
||||||
|
Inputs: " ",
|
||||||
|
|
||||||
Wifi: " ",
|
Wifi: " ",
|
||||||
Ethernet: " ",
|
Ethernet: " ",
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("hyprctl not found in PATH")
|
||||||
|
|
||||||
|
const defaultTimeout = 15 * time.Second
|
||||||
|
|
||||||
|
func CheckAvailable() error {
|
||||||
|
if _, err := exec.LookPath("hyprctl"); err != nil {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
_, err := run("version")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Installed reports whether hyprctl is present in PATH, without probing
|
||||||
|
// Hyprland. Cheap enough to call when deciding whether to show the Inputs
|
||||||
|
// page in the sidebar.
|
||||||
|
func Installed() bool {
|
||||||
|
_, err := exec.LookPath("hyprctl")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reset() error {
|
||||||
|
_, err := run("reload")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(args ...string) (string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "hyprctl", args...)
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
if errors.Is(err, exec.ErrNotFound) {
|
||||||
|
return "", ErrNotFound
|
||||||
|
}
|
||||||
|
msg := strings.TrimSpace(stderr.String())
|
||||||
|
if msg == "" {
|
||||||
|
msg = strings.TrimSpace(stdout.String())
|
||||||
|
}
|
||||||
|
if msg == "" {
|
||||||
|
msg = err.Error()
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("hyprctl %s: %s", strings.Join(args, " "), msg)
|
||||||
|
}
|
||||||
|
return stdout.String(), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Layout struct {
|
||||||
|
Code string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
var Layouts = []Layout{
|
||||||
|
{"al", "Albanian"},
|
||||||
|
{"et", "Amharic"},
|
||||||
|
{"am", "Armenian"},
|
||||||
|
{"ara", "Arabic"},
|
||||||
|
{"eg", "Arabic (Egypt)"},
|
||||||
|
{"iq", "Arabic (Iraq)"},
|
||||||
|
{"ma", "Arabic (Morocco)"},
|
||||||
|
{"sy", "Arabic (Syria)"},
|
||||||
|
{"az", "Azerbaijani"},
|
||||||
|
{"ml", "Bambara"},
|
||||||
|
{"bd", "Bangla"},
|
||||||
|
{"by", "Belarusian"},
|
||||||
|
{"be", "Belgian"},
|
||||||
|
{"dz", "Berber (Algeria, Latin)"},
|
||||||
|
{"ba", "Bosnian"},
|
||||||
|
{"brai", "Braille"},
|
||||||
|
{"bg", "Bulgarian"},
|
||||||
|
{"mm", "Burmese"},
|
||||||
|
{"cn", "Chinese"},
|
||||||
|
{"hr", "Croatian"},
|
||||||
|
{"cz", "Czech"},
|
||||||
|
{"dk", "Danish"},
|
||||||
|
{"af", "Dari"},
|
||||||
|
{"mv", "Dhivehi"},
|
||||||
|
{"nl", "Dutch"},
|
||||||
|
{"bt", "Dzongkha"},
|
||||||
|
{"au", "English (Australia)"},
|
||||||
|
{"cm", "English (Cameroon)"},
|
||||||
|
{"gh", "English (Ghana)"},
|
||||||
|
{"nz", "English (New Zealand)"},
|
||||||
|
{"ng", "English (Nigeria)"},
|
||||||
|
{"za", "English (South Africa)"},
|
||||||
|
{"gb", "English (UK)"},
|
||||||
|
{"us", "English (US)"},
|
||||||
|
{"epo", "Esperanto"},
|
||||||
|
{"ee", "Estonian"},
|
||||||
|
{"fo", "Faroese"},
|
||||||
|
{"ph", "Filipino"},
|
||||||
|
{"fi", "Finnish"},
|
||||||
|
{"fr", "French"},
|
||||||
|
{"ca", "French (Canada)"},
|
||||||
|
{"cd", "French (Democratic Republic of the Congo)"},
|
||||||
|
{"tg", "French (Togo)"},
|
||||||
|
{"ge", "Georgian"},
|
||||||
|
{"de", "German"},
|
||||||
|
{"at", "German (Austria)"},
|
||||||
|
{"ch", "German (Switzerland)"},
|
||||||
|
{"gr", "Greek"},
|
||||||
|
{"il", "Hebrew"},
|
||||||
|
{"hu", "Hungarian"},
|
||||||
|
{"is", "Icelandic"},
|
||||||
|
{"in", "Indian"},
|
||||||
|
{"id", "Indonesian (Latin)"},
|
||||||
|
{"ie", "Irish"},
|
||||||
|
{"it", "Italian"},
|
||||||
|
{"jp", "Japanese"},
|
||||||
|
{"kz", "Kazakh"},
|
||||||
|
{"kh", "Khmer (Cambodia)"},
|
||||||
|
{"kr", "Korean"},
|
||||||
|
{"kg", "Kyrgyz"},
|
||||||
|
{"la", "Lao"},
|
||||||
|
{"lv", "Latvian"},
|
||||||
|
{"lt", "Lithuanian"},
|
||||||
|
{"mk", "Macedonian"},
|
||||||
|
{"my", "Malay (Jawi, Arabic Keyboard)"},
|
||||||
|
{"mt", "Maltese"},
|
||||||
|
{"md", "Moldavian"},
|
||||||
|
{"mn", "Mongolian"},
|
||||||
|
{"me", "Montenegrin"},
|
||||||
|
{"np", "Nepali"},
|
||||||
|
{"gn", "N'Ko (AZERTY)"},
|
||||||
|
{"no", "Norwegian"},
|
||||||
|
{"ir", "Persian"},
|
||||||
|
{"pl", "Polish"},
|
||||||
|
{"pt", "Portuguese"},
|
||||||
|
{"br", "Portuguese (Brazil)"},
|
||||||
|
{"ro", "Romanian"},
|
||||||
|
{"ru", "Russian"},
|
||||||
|
{"rs", "Serbian"},
|
||||||
|
{"lk", "Sinhala (phonetic)"},
|
||||||
|
{"sk", "Slovak"},
|
||||||
|
{"si", "Slovenian"},
|
||||||
|
{"es", "Spanish"},
|
||||||
|
{"latam", "Spanish (Latin American)"},
|
||||||
|
{"ke", "Swahili (Kenya)"},
|
||||||
|
{"tz", "Swahili (Tanzania)"},
|
||||||
|
{"se", "Swedish"},
|
||||||
|
{"tw", "Taiwanese"},
|
||||||
|
{"tj", "Tajik"},
|
||||||
|
{"th", "Thai"},
|
||||||
|
{"bw", "Tswana"},
|
||||||
|
{"tm", "Turkmen"},
|
||||||
|
{"tr", "Turkish"},
|
||||||
|
{"ua", "Ukrainian"},
|
||||||
|
{"pk", "Urdu (Pakistan)"},
|
||||||
|
{"uz", "Uzbek"},
|
||||||
|
{"vn", "Vietnamese"},
|
||||||
|
{"sn", "Wolof"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func LayoutName(code string) string {
|
||||||
|
for _, l := range Layouts {
|
||||||
|
if l.Code == code {
|
||||||
|
return l.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOptionResp is the shape of `hyprctl getoption <name> -j`.
|
||||||
|
type getOptionResp struct {
|
||||||
|
Str string `json:"str"`
|
||||||
|
Float float64 `json:"float"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CurrentLayout() (string, error) {
|
||||||
|
out, err := run("getoption", "input:kb_layout", "-j")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var resp getOptionResp
|
||||||
|
if err := json.Unmarshal([]byte(out), &resp); err != nil {
|
||||||
|
return "", fmt.Errorf("parse getoption input:kb_layout: %w", err)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(resp.Str), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetLayout(code string) error {
|
||||||
|
_, err := run("keyword", "input:kb_layout", code)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MinSensitivity = -1.0
|
||||||
|
MaxSensitivity = 1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
func CurrentSensitivity() (float64, error) {
|
||||||
|
out, err := run("getoption", "input:sensitivity", "-j")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
var resp getOptionResp
|
||||||
|
if err := json.Unmarshal([]byte(out), &resp); err != nil {
|
||||||
|
return 0, fmt.Errorf("parse getoption input:sensitivity: %w", err)
|
||||||
|
}
|
||||||
|
return resp.Float, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetSensitivity(v float64) error {
|
||||||
|
switch {
|
||||||
|
case v < MinSensitivity:
|
||||||
|
v = MinSensitivity
|
||||||
|
case v > MaxSensitivity:
|
||||||
|
v = MaxSensitivity
|
||||||
|
}
|
||||||
|
_, err := run("keyword", "input:sensitivity", fmt.Sprintf("%.2f", v))
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"charm.land/bubbles/v2/key"
|
||||||
|
"github.com/anotherhadi/settuings/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InputKeyMap struct {
|
||||||
|
Select key.Binding
|
||||||
|
Reset key.Binding
|
||||||
|
Filter key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInputKeyMap(cfg config.InputKeys) InputKeyMap {
|
||||||
|
return InputKeyMap{
|
||||||
|
Select: binding(cfg.Select, "apply layout"),
|
||||||
|
Reset: binding(cfg.Reset, "reset to config"),
|
||||||
|
Filter: binding(cfg.Filter, "filter"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i InputKeyMap) Bindings() []key.Binding {
|
||||||
|
return []key.Binding{i.Select, i.Reset, i.Filter}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ type KeyMap struct {
|
|||||||
Bluetooth BluetoothKeyMap
|
Bluetooth BluetoothKeyMap
|
||||||
Audio AudioKeyMap
|
Audio AudioKeyMap
|
||||||
Power PowerKeyMap
|
Power PowerKeyMap
|
||||||
|
Input InputKeyMap
|
||||||
}
|
}
|
||||||
|
|
||||||
var Keys *KeyMap
|
var Keys *KeyMap
|
||||||
@@ -27,6 +28,7 @@ func Init(cfg *config.Config) {
|
|||||||
Bluetooth: newBluetoothKeyMap(kb.Bluetooth),
|
Bluetooth: newBluetoothKeyMap(kb.Bluetooth),
|
||||||
Audio: newAudioKeyMap(kb.Audio),
|
Audio: newAudioKeyMap(kb.Audio),
|
||||||
Power: newPowerKeyMap(kb.Power),
|
Power: newPowerKeyMap(kb.Power),
|
||||||
|
Input: newInputKeyMap(kb.Input),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ func CheckAvailable() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Installed reports whether nmcli is present in PATH, without probing
|
||||||
|
// NetworkManager. Cheap enough to call when deciding whether to show the
|
||||||
|
// Network page in the sidebar.
|
||||||
|
func Installed() bool {
|
||||||
|
_, err := exec.LookPath("nmcli")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func run(args ...string) (string, error) {
|
func run(args ...string) (string, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ func CheckAvailable() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Installed reports whether powerprofilesctl is present in PATH, without
|
||||||
|
// probing power-profiles-daemon. Cheap enough to call when deciding whether
|
||||||
|
// to show the Power page in the sidebar.
|
||||||
|
func Installed() bool {
|
||||||
|
_, err := exec.LookPath("powerprofilesctl")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListProfiles returns the available profiles in the order reported by
|
// ListProfiles returns the available profiles in the order reported by
|
||||||
// powerprofilesctl, each flagged with whether it's currently active.
|
// powerprofilesctl, each flagged with whether it's currently active.
|
||||||
func ListProfiles() ([]Profile, error) {
|
func ListProfiles() ([]Profile, error) {
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ import (
|
|||||||
audioUI "github.com/anotherhadi/settuings/internal/ui/audio"
|
audioUI "github.com/anotherhadi/settuings/internal/ui/audio"
|
||||||
bluetoothUI "github.com/anotherhadi/settuings/internal/ui/bluetooth"
|
bluetoothUI "github.com/anotherhadi/settuings/internal/ui/bluetooth"
|
||||||
notificationsUI "github.com/anotherhadi/settuings/internal/ui/components/notifications"
|
notificationsUI "github.com/anotherhadi/settuings/internal/ui/components/notifications"
|
||||||
|
inputUI "github.com/anotherhadi/settuings/internal/ui/input"
|
||||||
networkUI "github.com/anotherhadi/settuings/internal/ui/network"
|
networkUI "github.com/anotherhadi/settuings/internal/ui/network"
|
||||||
powerUI "github.com/anotherhadi/settuings/internal/ui/power"
|
powerUI "github.com/anotherhadi/settuings/internal/ui/power"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// buildPageShortcuts numbers the visible pages 1..N in sidebar order, so
|
// buildPageShortcuts numbers the visible pages 1..N in sidebar order, so
|
||||||
// hiding a page never leaves a gap in the shortcuts. It must run after
|
// hiding a page (or a missing CLI tool) never leaves a gap in the
|
||||||
// config.Load, hence a function called from New rather than a package var.
|
// shortcuts.
|
||||||
func buildPageShortcuts() map[string]page {
|
func buildPageShortcuts(visible []pageEntry) map[string]page {
|
||||||
visible := visiblePages()
|
|
||||||
m := make(map[string]page, len(visible))
|
m := make(map[string]page, len(visible))
|
||||||
for i, e := range visible {
|
for i, e := range visible {
|
||||||
m[strconv.Itoa(i+1)] = e.id
|
m[strconv.Itoa(i+1)] = e.id
|
||||||
@@ -33,6 +33,7 @@ func buildPageShortcuts() map[string]page {
|
|||||||
type Model struct {
|
type Model struct {
|
||||||
page page
|
page page
|
||||||
pageShortcuts map[string]page
|
pageShortcuts map[string]page
|
||||||
|
visiblePages []pageEntry
|
||||||
logPath string
|
logPath string
|
||||||
fatalErr error
|
fatalErr error
|
||||||
logFileErr error
|
logFileErr error
|
||||||
@@ -45,6 +46,7 @@ type Model struct {
|
|||||||
bluetooth bluetoothUI.Model
|
bluetooth bluetoothUI.Model
|
||||||
audio audioUI.Model
|
audio audioUI.Model
|
||||||
power powerUI.Model
|
power powerUI.Model
|
||||||
|
input inputUI.Model
|
||||||
notifications notificationsUI.Model
|
notifications notificationsUI.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,15 +67,18 @@ func New(initialPage string) (Model, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.Global
|
cfg := config.Global
|
||||||
|
visible := visiblePages()
|
||||||
|
|
||||||
m := Model{
|
m := Model{
|
||||||
page: startPage,
|
page: startPage,
|
||||||
pageShortcuts: buildPageShortcuts(),
|
pageShortcuts: buildPageShortcuts(visible),
|
||||||
|
visiblePages: visible,
|
||||||
about: aboutUI.New(),
|
about: aboutUI.New(),
|
||||||
network: networkUI.New(),
|
network: networkUI.New(),
|
||||||
bluetooth: bluetoothUI.New(),
|
bluetooth: bluetoothUI.New(),
|
||||||
audio: audioUI.New(),
|
audio: audioUI.New(),
|
||||||
power: powerUI.New(),
|
power: powerUI.New(),
|
||||||
|
input: inputUI.New(),
|
||||||
notifications: notificationsUI.New(),
|
notifications: notificationsUI.New(),
|
||||||
sidebarState: sidebarState(cfg.TUI.DefaultSidebarState),
|
sidebarState: sidebarState(cfg.TUI.DefaultSidebarState),
|
||||||
logPath: logPath,
|
logPath: logPath,
|
||||||
@@ -92,7 +97,7 @@ func New(initialPage string) (Model, error) {
|
|||||||
func (m Model) FatalErr() error { return m.fatalErr }
|
func (m Model) FatalErr() error { return m.fatalErr }
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
cmds := []tea.Cmd{m.about.Init(), m.network.Init(), m.bluetooth.Init(), m.audio.Init(), m.power.Init()}
|
cmds := []tea.Cmd{m.about.Init(), m.network.Init(), m.bluetooth.Init(), m.audio.Init(), m.power.Init(), m.input.Init()}
|
||||||
if m.page != pageAbout {
|
if m.page != pageAbout {
|
||||||
cmds = append(cmds, m.activatePage(m.page))
|
cmds = append(cmds, m.activatePage(m.page))
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-20
@@ -4,11 +4,17 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"github.com/anotherhadi/settuings/internal/audio"
|
||||||
|
"github.com/anotherhadi/settuings/internal/bluetooth"
|
||||||
"github.com/anotherhadi/settuings/internal/config"
|
"github.com/anotherhadi/settuings/internal/config"
|
||||||
"github.com/anotherhadi/settuings/internal/icons"
|
"github.com/anotherhadi/settuings/internal/icons"
|
||||||
|
"github.com/anotherhadi/settuings/internal/input"
|
||||||
|
"github.com/anotherhadi/settuings/internal/network"
|
||||||
|
"github.com/anotherhadi/settuings/internal/power"
|
||||||
aboutUI "github.com/anotherhadi/settuings/internal/ui/about"
|
aboutUI "github.com/anotherhadi/settuings/internal/ui/about"
|
||||||
audioUI "github.com/anotherhadi/settuings/internal/ui/audio"
|
audioUI "github.com/anotherhadi/settuings/internal/ui/audio"
|
||||||
bluetoothUI "github.com/anotherhadi/settuings/internal/ui/bluetooth"
|
bluetoothUI "github.com/anotherhadi/settuings/internal/ui/bluetooth"
|
||||||
|
inputUI "github.com/anotherhadi/settuings/internal/ui/input"
|
||||||
networkUI "github.com/anotherhadi/settuings/internal/ui/network"
|
networkUI "github.com/anotherhadi/settuings/internal/ui/network"
|
||||||
powerUI "github.com/anotherhadi/settuings/internal/ui/power"
|
powerUI "github.com/anotherhadi/settuings/internal/ui/power"
|
||||||
)
|
)
|
||||||
@@ -21,6 +27,7 @@ const (
|
|||||||
pageBluetooth page = "Bluetooth"
|
pageBluetooth page = "Bluetooth"
|
||||||
pageAudio page = "Audio"
|
pageAudio page = "Audio"
|
||||||
pagePower page = "Power"
|
pagePower page = "Power"
|
||||||
|
pageInputs page = "Inputs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// pageEntry describes a page and all its integration hooks.
|
// pageEntry describes a page and all its integration hooks.
|
||||||
@@ -28,6 +35,12 @@ type pageEntry struct {
|
|||||||
id page
|
id page
|
||||||
icon func() string
|
icon func() string
|
||||||
|
|
||||||
|
// available reports whether this page's underlying CLI tool is
|
||||||
|
// installed. nil = always available (e.g. About has no dependency).
|
||||||
|
// A page that isn't available is left out of the sidebar and its
|
||||||
|
// numbered shortcuts, but stays reachable via --page.
|
||||||
|
available func() bool
|
||||||
|
|
||||||
// render returns the page's view content. nil = show "empty".
|
// render returns the page's view content. nil = show "empty".
|
||||||
render func(m *Model) string
|
render func(m *Model) string
|
||||||
// update is called when this page is active. nil = no-op.
|
// update is called when this page is active. nil = no-op.
|
||||||
@@ -64,8 +77,9 @@ var pageRegistry = []pageEntry{
|
|||||||
resize: func(m *Model, w, h int) { m.about.SetSize(w, h) },
|
resize: func(m *Model, w, h int) { m.about.SetSize(w, h) },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: pageNetwork,
|
id: pageNetwork,
|
||||||
icon: func() string { return icons.I.Network },
|
icon: func() string { return icons.I.Network },
|
||||||
|
available: network.Installed,
|
||||||
|
|
||||||
render: func(m *Model) string { return m.network.View().Content },
|
render: func(m *Model) string { return m.network.View().Content },
|
||||||
update: func(m *Model, msg tea.Msg) tea.Cmd {
|
update: func(m *Model, msg tea.Msg) tea.Cmd {
|
||||||
@@ -78,8 +92,9 @@ var pageRegistry = []pageEntry{
|
|||||||
activate: func(m *Model) tea.Cmd { return m.network.Activate() },
|
activate: func(m *Model) tea.Cmd { return m.network.Activate() },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: pageBluetooth,
|
id: pageBluetooth,
|
||||||
icon: func() string { return icons.I.Bluetooth },
|
icon: func() string { return icons.I.Bluetooth },
|
||||||
|
available: bluetooth.Installed,
|
||||||
|
|
||||||
render: func(m *Model) string { return m.bluetooth.View().Content },
|
render: func(m *Model) string { return m.bluetooth.View().Content },
|
||||||
update: func(m *Model, msg tea.Msg) tea.Cmd {
|
update: func(m *Model, msg tea.Msg) tea.Cmd {
|
||||||
@@ -92,8 +107,9 @@ var pageRegistry = []pageEntry{
|
|||||||
activate: func(m *Model) tea.Cmd { return m.bluetooth.Activate() },
|
activate: func(m *Model) tea.Cmd { return m.bluetooth.Activate() },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: pageAudio,
|
id: pageAudio,
|
||||||
icon: func() string { return icons.I.Audio },
|
icon: func() string { return icons.I.Audio },
|
||||||
|
available: audio.Installed,
|
||||||
|
|
||||||
render: func(m *Model) string { return m.audio.View().Content },
|
render: func(m *Model) string { return m.audio.View().Content },
|
||||||
update: func(m *Model, msg tea.Msg) tea.Cmd {
|
update: func(m *Model, msg tea.Msg) tea.Cmd {
|
||||||
@@ -107,8 +123,9 @@ var pageRegistry = []pageEntry{
|
|||||||
deactivate: func(m *Model) { m.audio.Deactivate() },
|
deactivate: func(m *Model) { m.audio.Deactivate() },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: pagePower,
|
id: pagePower,
|
||||||
icon: func() string { return icons.I.Power },
|
icon: func() string { return icons.I.Power },
|
||||||
|
available: power.Installed,
|
||||||
|
|
||||||
render: func(m *Model) string { return m.power.View().Content },
|
render: func(m *Model) string { return m.power.View().Content },
|
||||||
update: func(m *Model, msg tea.Msg) tea.Cmd {
|
update: func(m *Model, msg tea.Msg) tea.Cmd {
|
||||||
@@ -120,6 +137,21 @@ var pageRegistry = []pageEntry{
|
|||||||
resize: func(m *Model, w, h int) { m.power.SetSize(w, h) },
|
resize: func(m *Model, w, h int) { m.power.SetSize(w, h) },
|
||||||
activate: func(m *Model) tea.Cmd { return m.power.Activate() },
|
activate: func(m *Model) tea.Cmd { return m.power.Activate() },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: pageInputs,
|
||||||
|
icon: func() string { return icons.I.Inputs },
|
||||||
|
available: input.Installed,
|
||||||
|
|
||||||
|
render: func(m *Model) string { return m.input.View().Content },
|
||||||
|
update: func(m *Model, msg tea.Msg) tea.Cmd {
|
||||||
|
updated, cmd := m.input.Update(msg)
|
||||||
|
m.input = updated.(inputUI.Model)
|
||||||
|
return cmd
|
||||||
|
},
|
||||||
|
isEditing: func(m *Model) bool { return m.input.IsEditing() },
|
||||||
|
resize: func(m *Model, w, h int) { m.input.SetSize(w, h) },
|
||||||
|
activate: func(m *Model) tea.Cmd { return m.input.Activate() },
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageNames returns the valid page identifiers, in sidebar order, usable
|
// PageNames returns the valid page identifiers, in sidebar order, usable
|
||||||
@@ -144,30 +176,30 @@ func lookupPage(name string) (page, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// visiblePages returns pageRegistry entries not listed in the user's
|
// visiblePages returns pageRegistry entries not listed in the user's
|
||||||
// tui.hidden_pages config, preserving registry order. A page hidden this
|
// tui.hidden_pages config and whose underlying CLI tool (if any) is
|
||||||
// way is still reachable via --page: hiding only affects the sidebar and
|
// installed, preserving registry order. A page left out this way is still
|
||||||
// its numbered shortcuts.
|
// reachable via --page: this only affects the sidebar and its numbered
|
||||||
|
// shortcuts. It shells out to `exec.LookPath` per page, so callers should
|
||||||
|
// compute it once (e.g. in New) and cache the result rather than calling it
|
||||||
|
// on every render.
|
||||||
func visiblePages() []pageEntry {
|
func visiblePages() []pageEntry {
|
||||||
hiddenNames := config.Global.TUI.HiddenPages
|
hiddenNames := config.Global.TUI.HiddenPages
|
||||||
if len(hiddenNames) == 0 {
|
|
||||||
return pageRegistry
|
|
||||||
}
|
|
||||||
|
|
||||||
hidden := make(map[page]bool, len(hiddenNames))
|
hidden := make(map[page]bool, len(hiddenNames))
|
||||||
for _, name := range hiddenNames {
|
for _, name := range hiddenNames {
|
||||||
if p, ok := lookupPage(name); ok {
|
if p, ok := lookupPage(name); ok {
|
||||||
hidden[p] = true
|
hidden[p] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(hidden) == 0 {
|
|
||||||
return pageRegistry
|
|
||||||
}
|
|
||||||
|
|
||||||
visible := make([]pageEntry, 0, len(pageRegistry))
|
visible := make([]pageEntry, 0, len(pageRegistry))
|
||||||
for _, e := range pageRegistry {
|
for _, e := range pageRegistry {
|
||||||
if !hidden[e.id] {
|
if hidden[e.id] {
|
||||||
visible = append(visible, e)
|
continue
|
||||||
}
|
}
|
||||||
|
if e.available != nil && !e.available() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visible = append(visible, e)
|
||||||
}
|
}
|
||||||
return visible
|
return visible
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func (m *Model) renderSidebar() string {
|
|||||||
var items strings.Builder
|
var items strings.Builder
|
||||||
badgeUnread := lipgloss.NewStyle().Foreground(ilovetui.S.Warning).Bold(true)
|
badgeUnread := lipgloss.NewStyle().Foreground(ilovetui.S.Warning).Bold(true)
|
||||||
|
|
||||||
for i, entry := range visiblePages() {
|
for i, entry := range m.visiblePages {
|
||||||
selected := entry.id == m.page
|
selected := entry.id == m.page
|
||||||
badgeStyle, textStyle := badgeNormal, textNormal
|
badgeStyle, textStyle := badgeNormal, textNormal
|
||||||
if selected {
|
if selected {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"github.com/anotherhadi/settuings/internal/input"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tickInterval = 5 * time.Second
|
||||||
|
|
||||||
|
func tickCmd() tea.Cmd {
|
||||||
|
return tea.Tick(tickInterval, func(time.Time) tea.Msg { return tickMsg{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCmd() tea.Cmd {
|
||||||
|
return func() tea.Msg { return checkMsg{err: input.CheckAvailable()} }
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchLayoutCmd() tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
layout, err := input.CurrentLayout()
|
||||||
|
return layoutMsg{layout: layout, err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setLayoutCmd(code string) tea.Cmd {
|
||||||
|
return func() tea.Msg { return setLayoutMsg{err: input.SetLayout(code)} }
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchSensitivityCmd() tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
value, err := input.CurrentSensitivity()
|
||||||
|
return sensitivityMsg{value: value, err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSensitivityCmd(v float64) tea.Cmd {
|
||||||
|
return func() tea.Msg { return setSensitivityMsg{err: input.SetSensitivity(v)} }
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetCmd() tea.Cmd {
|
||||||
|
return func() tea.Msg { return resetMsg{err: input.Reset()} }
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/bubbles/v2/key"
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
|
"github.com/anotherhadi/settuings/internal/icons"
|
||||||
|
inputpkg "github.com/anotherhadi/settuings/internal/input"
|
||||||
|
"github.com/anotherhadi/settuings/internal/keys"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Model) handleLayout(msg layoutMsg) {
|
||||||
|
m.layoutErr = msg.err
|
||||||
|
if msg.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.currentLayout = msg.layout
|
||||||
|
if !m.cursorInit {
|
||||||
|
m.cursorInit = true
|
||||||
|
for i, l := range inputpkg.Layouts {
|
||||||
|
if l.Code == msg.layout {
|
||||||
|
m.cursor = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) handleSetLayout(msg setLayoutMsg) tea.Cmd {
|
||||||
|
m.layoutPending = false
|
||||||
|
if msg.err != nil {
|
||||||
|
m.layoutFeedback = msg.err.Error()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.layoutFeedback = ""
|
||||||
|
return fetchLayoutCmd()
|
||||||
|
}
|
||||||
|
|
||||||
|
// visibleLayouts returns the layouts matching the current filter query (by
|
||||||
|
// code or name, case-insensitive), or every layout when there's no query.
|
||||||
|
func (m Model) visibleLayouts() []inputpkg.Layout {
|
||||||
|
q := strings.ToLower(strings.TrimSpace(m.filterInput.Value()))
|
||||||
|
if q == "" {
|
||||||
|
return inputpkg.Layouts
|
||||||
|
}
|
||||||
|
matches := make([]inputpkg.Layout, 0, len(inputpkg.Layouts))
|
||||||
|
for _, l := range inputpkg.Layouts {
|
||||||
|
if strings.Contains(strings.ToLower(l.Name), q) || strings.Contains(strings.ToLower(l.Code), q) {
|
||||||
|
matches = append(matches, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
// clampCursor keeps the cursor within the currently visible (filtered)
|
||||||
|
// list, called whenever the filter query — and so the list length —
|
||||||
|
// changes.
|
||||||
|
func (m *Model) clampCursor() {
|
||||||
|
n := len(m.visibleLayouts())
|
||||||
|
switch {
|
||||||
|
case m.cursor >= n:
|
||||||
|
m.cursor = n - 1
|
||||||
|
case m.cursor < 0:
|
||||||
|
m.cursor = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) selectedLayout() *inputpkg.Layout {
|
||||||
|
list := m.visibleLayouts()
|
||||||
|
if m.cursor < 0 || m.cursor >= len(list) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &list[m.cursor]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) updateLayout(msg tea.Msg) tea.Cmd {
|
||||||
|
keyMsg, ok := msg.(tea.KeyPressMsg)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
g := keys.Keys.Global
|
||||||
|
in := keys.Keys.Input
|
||||||
|
|
||||||
|
if m.filtering {
|
||||||
|
switch {
|
||||||
|
case key.Matches(keyMsg, g.Escape):
|
||||||
|
m.filtering = false
|
||||||
|
m.filterInput.Blur()
|
||||||
|
m.filterInput.SetValue("")
|
||||||
|
m.cursor = 0
|
||||||
|
case key.Matches(keyMsg, in.Select):
|
||||||
|
m.filtering = false
|
||||||
|
m.filterInput.Blur()
|
||||||
|
m.clampCursor()
|
||||||
|
default:
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.filterInput, cmd = m.filterInput.Update(keyMsg)
|
||||||
|
m.clampCursor()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case key.Matches(keyMsg, in.Filter):
|
||||||
|
m.filtering = true
|
||||||
|
m.filterInput.Focus()
|
||||||
|
case key.Matches(keyMsg, g.Up):
|
||||||
|
if m.cursor > 0 {
|
||||||
|
m.cursor--
|
||||||
|
}
|
||||||
|
case key.Matches(keyMsg, g.Down):
|
||||||
|
if m.cursor < len(m.visibleLayouts())-1 {
|
||||||
|
m.cursor++
|
||||||
|
}
|
||||||
|
case key.Matches(keyMsg, in.Select):
|
||||||
|
return m.applySelectedLayout()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) applySelectedLayout() tea.Cmd {
|
||||||
|
sel := m.selectedLayout()
|
||||||
|
if sel == nil || sel.Code == m.currentLayout {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.layoutPending = true
|
||||||
|
m.layoutFeedback = ""
|
||||||
|
return setLayoutCmd(sel.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderLayout(width, height int) string {
|
||||||
|
title := sectionTitleStyle(m.focus == focusLayout).Render("Keyboard Layout")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case m.layoutErr != nil:
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, title, "", errorStyle().Render(m.layoutErr.Error()))
|
||||||
|
case m.currentLayout == "" && !m.cursorInit:
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, title, "", ilovetui.S.Faint.Render("Loading current layout…"))
|
||||||
|
}
|
||||||
|
|
||||||
|
current := subtleStyle().Render("Current: ") + inputpkg.LayoutName(m.currentLayout) + subtleStyle().Render(" ("+m.currentLayout+")")
|
||||||
|
lines := []string{title, "", current}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case m.filtering:
|
||||||
|
lines = append(lines, m.filterInput.View())
|
||||||
|
case m.filterInput.Value() != "":
|
||||||
|
lines = append(lines, subtleStyle().Render(fmt.Sprintf("Filter: %q — press / to change", m.filterInput.Value())))
|
||||||
|
}
|
||||||
|
lines = append(lines, "")
|
||||||
|
|
||||||
|
list := m.visibleLayouts()
|
||||||
|
|
||||||
|
available := height - len(lines) - 2
|
||||||
|
if available < 1 {
|
||||||
|
available = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(list) == 0 {
|
||||||
|
lines = append(lines, ilovetui.S.Faint.Render("No layout matches the filter."))
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, lines...)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, start, end := paginate(m.cursor, len(list), available)
|
||||||
|
|
||||||
|
for i := start; i < end; i++ {
|
||||||
|
lines = append(lines, m.renderLayoutRow(list[i], i == m.cursor))
|
||||||
|
}
|
||||||
|
|
||||||
|
footer := renderPaginatorDots(p)
|
||||||
|
switch {
|
||||||
|
case m.layoutPending:
|
||||||
|
footer = mutedStyle().Render("applying…")
|
||||||
|
case m.layoutFeedback != "":
|
||||||
|
footer = errorStyle().Render(m.layoutFeedback)
|
||||||
|
}
|
||||||
|
lines = append(lines, "", footer)
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, lines...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderLayoutRow(l inputpkg.Layout, selected bool) string {
|
||||||
|
textStyle := lipgloss.NewStyle().Foreground(ilovetui.S.Text)
|
||||||
|
if selected {
|
||||||
|
textStyle = textStyle.Bold(true)
|
||||||
|
if m.focus == focusLayout {
|
||||||
|
textStyle = textStyle.Foreground(ilovetui.S.Primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor := " "
|
||||||
|
if selected {
|
||||||
|
cursor = "> "
|
||||||
|
}
|
||||||
|
check := " "
|
||||||
|
if l.Code == m.currentLayout {
|
||||||
|
check = successStyle().Render(icons.I.Check)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s%s%s", cursor, check, textStyle.Render(l.Name+" ("+l.Code+")"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"charm.land/bubbles/v2/key"
|
||||||
|
"github.com/anotherhadi/settuings/internal/keys"
|
||||||
|
)
|
||||||
|
|
||||||
|
// inputKeyMap renders the bottom help bar, adapting to which section
|
||||||
|
// currently has focus.
|
||||||
|
type inputKeyMap struct {
|
||||||
|
m Model
|
||||||
|
width int
|
||||||
|
}
|
||||||
|
|
||||||
|
// relabel reuses an existing binding's keys under a different help
|
||||||
|
// description, same trick the audio page uses for its volume keys.
|
||||||
|
func relabel(b key.Binding, desc string) key.Binding {
|
||||||
|
return key.NewBinding(key.WithKeys(b.Keys()...), key.WithHelp(b.Help().Key, desc))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k inputKeyMap) ShortHelp() []key.Binding {
|
||||||
|
g := keys.Keys.Global
|
||||||
|
in := keys.Keys.Input
|
||||||
|
if k.m.checkErr != nil {
|
||||||
|
return []key.Binding{g.Help}
|
||||||
|
}
|
||||||
|
if k.m.filtering {
|
||||||
|
confirm := relabel(in.Select, "confirm")
|
||||||
|
cancel := relabel(g.Escape, "cancel")
|
||||||
|
return []key.Binding{confirm, cancel}
|
||||||
|
}
|
||||||
|
switch k.m.focus {
|
||||||
|
case focusMouse:
|
||||||
|
speedDown, speedUp := relabel(g.Left, "speed down"), relabel(g.Right, "speed up")
|
||||||
|
return []key.Binding{speedDown, speedUp, in.Reset, g.CycleFocus, g.Help}
|
||||||
|
default:
|
||||||
|
return []key.Binding{g.Up, g.Down, in.Select, in.Filter, in.Reset, g.CycleFocus, g.Help}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k inputKeyMap) FullHelp() [][]key.Binding {
|
||||||
|
g := keys.Keys.Global
|
||||||
|
in := keys.Keys.Input
|
||||||
|
|
||||||
|
if k.m.filtering {
|
||||||
|
confirm := relabel(in.Select, "confirm filter")
|
||||||
|
cancel := relabel(g.Escape, "cancel filter")
|
||||||
|
return keys.ChunkByWidth([]key.Binding{confirm, cancel}, k.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
var bindings []key.Binding
|
||||||
|
switch {
|
||||||
|
case k.m.checkErr != nil:
|
||||||
|
bindings = nil
|
||||||
|
case k.m.focus == focusMouse:
|
||||||
|
speedDown, speedUp := relabel(g.Left, "speed down"), relabel(g.Right, "speed up")
|
||||||
|
bindings = []key.Binding{speedDown, speedUp, in.Reset, g.CycleFocus}
|
||||||
|
default:
|
||||||
|
bindings = []key.Binding{g.Up, g.Down, in.Select, in.Filter, in.Reset, g.CycleFocus}
|
||||||
|
}
|
||||||
|
bindings = append(bindings, g.CommonBindings()...)
|
||||||
|
return keys.ChunkByWidth(bindings, k.width)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
// checkMsg carries the result of input.CheckAvailable (hyprctl).
|
||||||
|
type checkMsg struct{ err error }
|
||||||
|
|
||||||
|
// tickMsg triggers a periodic refresh of layout + sensitivity while this
|
||||||
|
// page is active, so it reflects layout/sensitivity changes made elsewhere
|
||||||
|
// (hyprland.conf reload, another tool).
|
||||||
|
type tickMsg struct{}
|
||||||
|
|
||||||
|
type layoutMsg struct {
|
||||||
|
layout string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type setLayoutMsg struct{ err error }
|
||||||
|
|
||||||
|
type sensitivityMsg struct {
|
||||||
|
value float64
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type setSensitivityMsg struct{ err error }
|
||||||
|
|
||||||
|
// resetMsg carries the result of resetting both the keyboard layout and
|
||||||
|
// mouse sensitivity to their hyprland.conf values.
|
||||||
|
type resetMsg struct{ err error }
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// Package input provides the "Inputs" TUI page: keyboard layout and mouse
|
||||||
|
// sensitivity, controlled via hyprctl, in the same spirit as the power
|
||||||
|
// page's controller-power + status split.
|
||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"charm.land/bubbles/v2/help"
|
||||||
|
"charm.land/bubbles/v2/textinput"
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// focusZone tracks which of the two sections (keyboard layout list, mouse
|
||||||
|
// sensitivity slider) currently receives Up/Down/Left/Right/Select.
|
||||||
|
type focusZone int
|
||||||
|
|
||||||
|
const (
|
||||||
|
focusLayout focusZone = iota
|
||||||
|
focusMouse
|
||||||
|
)
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
help help.Model
|
||||||
|
focus focusZone
|
||||||
|
|
||||||
|
checkErr error // reason hyprctl is unusable; nil once confirmed OK
|
||||||
|
checked bool
|
||||||
|
|
||||||
|
layoutErr error
|
||||||
|
currentLayout string
|
||||||
|
cursor int
|
||||||
|
cursorInit bool // whether cursor has been snapped to the current layout yet
|
||||||
|
layoutPending bool
|
||||||
|
layoutFeedback string
|
||||||
|
filterInput textinput.Model
|
||||||
|
filtering bool
|
||||||
|
|
||||||
|
sensitivityErr error
|
||||||
|
sensitivity float64
|
||||||
|
sensitivityKnown bool
|
||||||
|
mousePending bool
|
||||||
|
mouseFeedback string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() Model {
|
||||||
|
ti := textinput.New()
|
||||||
|
ti.Prompt = "/"
|
||||||
|
s := ti.Styles()
|
||||||
|
s.Focused.Prompt = lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||||
|
ti.SetStyles(s)
|
||||||
|
|
||||||
|
return Model{
|
||||||
|
help: ilovetui.NewHelp(),
|
||||||
|
filterInput: ti,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate is called every time the user navigates onto this page. It
|
||||||
|
// re-checks hyprctl availability and fetches the current layout + mouse
|
||||||
|
// sensitivity, then starts the periodic auto-refresh loop.
|
||||||
|
func (m *Model) Activate() tea.Cmd {
|
||||||
|
return tea.Batch(checkCmd(), fetchLayoutCmd(), fetchSensitivityCmd(), tickCmd())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) IsEditing() bool {
|
||||||
|
return m.filtering
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) SetSize(w, h int) {
|
||||||
|
m.width = w
|
||||||
|
m.height = h
|
||||||
|
m.help.SetWidth(w - 2)
|
||||||
|
m.filterInput.SetWidth(w - 4)
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/bubbles/v2/key"
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
|
inputpkg "github.com/anotherhadi/settuings/internal/input"
|
||||||
|
"github.com/anotherhadi/settuings/internal/keys"
|
||||||
|
)
|
||||||
|
|
||||||
|
const sensitivityStep = 0.05
|
||||||
|
|
||||||
|
func (m *Model) handleSensitivity(msg sensitivityMsg) {
|
||||||
|
m.sensitivityErr = msg.err
|
||||||
|
if msg.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.sensitivity = msg.value
|
||||||
|
m.sensitivityKnown = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) handleSetSensitivity(msg setSensitivityMsg) tea.Cmd {
|
||||||
|
m.mousePending = false
|
||||||
|
if msg.err != nil {
|
||||||
|
m.mouseFeedback = msg.err.Error()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.mouseFeedback = ""
|
||||||
|
return fetchSensitivityCmd()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) updateMouse(msg tea.Msg) tea.Cmd {
|
||||||
|
keyMsg, ok := msg.(tea.KeyPressMsg)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
g := keys.Keys.Global
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case key.Matches(keyMsg, g.Left):
|
||||||
|
return m.adjustSensitivity(-sensitivityStep)
|
||||||
|
case key.Matches(keyMsg, g.Right):
|
||||||
|
return m.adjustSensitivity(sensitivityStep)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjustSensitivity nudges the mouse sensitivity by delta, updating it
|
||||||
|
// locally right away so the bar responds instantly instead of waiting on
|
||||||
|
// the round trip to hyprctl and back, mirroring the audio page's volume
|
||||||
|
// adjustment.
|
||||||
|
func (m *Model) adjustSensitivity(delta float64) tea.Cmd {
|
||||||
|
if !m.sensitivityKnown {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v := m.sensitivity + delta
|
||||||
|
switch {
|
||||||
|
case v < inputpkg.MinSensitivity:
|
||||||
|
v = inputpkg.MinSensitivity
|
||||||
|
case v > inputpkg.MaxSensitivity:
|
||||||
|
v = inputpkg.MaxSensitivity
|
||||||
|
}
|
||||||
|
m.sensitivity = v
|
||||||
|
m.mouseFeedback = ""
|
||||||
|
m.mousePending = true
|
||||||
|
return setSensitivityCmd(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderMouse(width int) string {
|
||||||
|
title := sectionTitleStyle(m.focus == focusMouse).Render("Mouse Speed")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case m.sensitivityErr != nil:
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, title, "", errorStyle().Render(m.sensitivityErr.Error()))
|
||||||
|
case !m.sensitivityKnown:
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, title, "", ilovetui.S.Faint.Render("Loading mouse sensitivity…"))
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := []string{title, "", renderSensitivityBar(m.sensitivity, width)}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case m.mousePending:
|
||||||
|
lines = append(lines, "", mutedStyle().Render("applying…"))
|
||||||
|
case m.mouseFeedback != "":
|
||||||
|
lines = append(lines, "", errorStyle().Render(m.mouseFeedback))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, lines...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderSensitivityBar draws a static block-bar gauge for a sensitivity in
|
||||||
|
// [MinSensitivity, MaxSensitivity], in the same spirit as the audio page's
|
||||||
|
// volume bar and the power page's battery gauge.
|
||||||
|
func renderSensitivityBar(value float64, width int) string {
|
||||||
|
barWidth := width / 2
|
||||||
|
switch {
|
||||||
|
case barWidth < 10:
|
||||||
|
barWidth = 10
|
||||||
|
case barWidth > 30:
|
||||||
|
barWidth = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
span := inputpkg.MaxSensitivity - inputpkg.MinSensitivity
|
||||||
|
filled := int((value-inputpkg.MinSensitivity)/span*float64(barWidth) + 0.5)
|
||||||
|
switch {
|
||||||
|
case filled > barWidth:
|
||||||
|
filled = barWidth
|
||||||
|
case filled < 0:
|
||||||
|
filled = 0
|
||||||
|
}
|
||||||
|
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
|
||||||
|
gauge := lipgloss.NewStyle().Foreground(ilovetui.S.Primary).Render("[" + bar + "]")
|
||||||
|
return fmt.Sprintf("%s %+.2f", gauge, value)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"charm.land/bubbles/v2/paginator"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newPaginator() paginator.Model {
|
||||||
|
p := paginator.New()
|
||||||
|
p.Type = paginator.Dots
|
||||||
|
p.ActiveDot = lipgloss.NewStyle().Foreground(ilovetui.S.Primary).Render("•")
|
||||||
|
p.InactiveDot = lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).Render("•")
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// paginate slices a list of length total into pages of availableRows items,
|
||||||
|
// deriving the current page from cursor (the globally-indexed selection)
|
||||||
|
// rather than tracking page state separately. It returns the configured
|
||||||
|
// paginator and the [start, end) bounds of the visible slice.
|
||||||
|
func paginate(cursor, total, availableRows int) (p paginator.Model, start, end int) {
|
||||||
|
p = newPaginator()
|
||||||
|
perPage := availableRows
|
||||||
|
if perPage < 1 {
|
||||||
|
perPage = 1
|
||||||
|
}
|
||||||
|
p.PerPage = perPage
|
||||||
|
p.SetTotalPages(total)
|
||||||
|
if p.TotalPages > 0 {
|
||||||
|
p.Page = cursor / perPage
|
||||||
|
if p.Page >= p.TotalPages {
|
||||||
|
p.Page = p.TotalPages - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start, end = p.GetSliceBounds(total)
|
||||||
|
return p, start, end
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderPaginatorDots(p paginator.Model) string {
|
||||||
|
if p.TotalPages <= 1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return mutedStyle().Render(p.View())
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import tea "charm.land/bubbletea/v2"
|
||||||
|
|
||||||
|
// applyReset kicks off a `hyprctl reload`, which restores the keyboard
|
||||||
|
// layout and mouse sensitivity to their hyprland.conf values (along with
|
||||||
|
// anything else set there); both sections are marked pending since a
|
||||||
|
// reload touches them together.
|
||||||
|
func (m *Model) applyReset() tea.Cmd {
|
||||||
|
m.layoutPending = true
|
||||||
|
m.mousePending = true
|
||||||
|
m.layoutFeedback = ""
|
||||||
|
m.mouseFeedback = ""
|
||||||
|
return resetCmd()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) handleReset(msg resetMsg) tea.Cmd {
|
||||||
|
m.layoutPending = false
|
||||||
|
m.mousePending = false
|
||||||
|
if msg.err != nil {
|
||||||
|
m.layoutFeedback = msg.err.Error()
|
||||||
|
m.mouseFeedback = msg.err.Error()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tea.Batch(fetchLayoutCmd(), fetchSensitivityCmd())
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func errorStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().Foreground(ilovetui.S.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func successStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().Foreground(ilovetui.S.Success)
|
||||||
|
}
|
||||||
|
|
||||||
|
func subtleStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().Foreground(ilovetui.S.Subtle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mutedStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().Foreground(ilovetui.S.Muted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sectionTitleStyle highlights a section's title when it holds keyboard
|
||||||
|
// focus, so Tab-cycling between the layout list and the mouse slider is
|
||||||
|
// visible even before the user starts navigating within a section.
|
||||||
|
func sectionTitleStyle(focused bool) lipgloss.Style {
|
||||||
|
style := lipgloss.NewStyle().Bold(true)
|
||||||
|
if focused {
|
||||||
|
style = style.Foreground(ilovetui.S.Primary)
|
||||||
|
}
|
||||||
|
return style
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
|
"github.com/anotherhadi/settuings/internal/icons"
|
||||||
|
)
|
||||||
|
|
||||||
|
// renderTabBar draws a header matching the power page's tab bar: a single
|
||||||
|
// always-active label, since this page has no tabs of its own.
|
||||||
|
func renderTabBar(width int) string {
|
||||||
|
style := lipgloss.NewStyle().Bold(true).Foreground(ilovetui.S.Primary).Padding(0, 2).
|
||||||
|
Border(lipgloss.NormalBorder(), false, false, true, false).
|
||||||
|
BorderForeground(ilovetui.S.Primary)
|
||||||
|
|
||||||
|
label := style.Render(icons.I.Inputs + "Inputs")
|
||||||
|
|
||||||
|
filler := width - lipgloss.Width(label)
|
||||||
|
if filler > 0 {
|
||||||
|
label = lipgloss.JoinHorizontal(lipgloss.Bottom, label,
|
||||||
|
lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.NormalBorder(), false, false, true, false).
|
||||||
|
BorderForeground(ilovetui.S.Primary).
|
||||||
|
Render(strings.Repeat(" ", filler)))
|
||||||
|
}
|
||||||
|
return label
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"charm.land/bubbles/v2/key"
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"github.com/anotherhadi/settuings/internal/keys"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case checkMsg:
|
||||||
|
wasUnavailable := m.checkErr != nil
|
||||||
|
m.checked = true
|
||||||
|
m.checkErr = msg.err
|
||||||
|
if msg.err == nil && wasUnavailable {
|
||||||
|
// recovered: resume fetching
|
||||||
|
return m, tea.Batch(fetchLayoutCmd(), fetchSensitivityCmd())
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case tickMsg:
|
||||||
|
if m.checkErr != nil {
|
||||||
|
// Not manually retriable anymore (no refresh key): keep
|
||||||
|
// re-checking on the tick so the page self-heals once
|
||||||
|
// hyprctl/Hyprland becomes available again.
|
||||||
|
return m, tea.Batch(tickCmd(), checkCmd())
|
||||||
|
}
|
||||||
|
return m, tea.Batch(tickCmd(), fetchLayoutCmd(), fetchSensitivityCmd())
|
||||||
|
|
||||||
|
case layoutMsg:
|
||||||
|
m.handleLayout(msg)
|
||||||
|
return m, nil
|
||||||
|
case setLayoutMsg:
|
||||||
|
return m, m.handleSetLayout(msg)
|
||||||
|
|
||||||
|
case sensitivityMsg:
|
||||||
|
m.handleSensitivity(msg)
|
||||||
|
return m, nil
|
||||||
|
case setSensitivityMsg:
|
||||||
|
return m, m.handleSetSensitivity(msg)
|
||||||
|
|
||||||
|
case resetMsg:
|
||||||
|
return m, m.handleReset(msg)
|
||||||
|
|
||||||
|
case tea.KeyPressMsg:
|
||||||
|
if m.filtering {
|
||||||
|
// Every key goes straight to the filter box while typing (a
|
||||||
|
// query like "tab" or "r" must not also trigger CycleFocus or
|
||||||
|
// Reset), same precedence the About page's search box uses.
|
||||||
|
return m, m.updateLayout(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.Matches(msg, keys.Keys.Global.Help) {
|
||||||
|
m.help.ShowAll = !m.help.ShowAll
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.checkErr != nil {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.Matches(msg, keys.Keys.Global.CycleFocus) {
|
||||||
|
m.cycleFocus()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.Matches(msg, keys.Keys.Input.Reset) {
|
||||||
|
return m, m.applyReset()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m.focus {
|
||||||
|
case focusLayout:
|
||||||
|
return m, m.updateLayout(msg)
|
||||||
|
case focusMouse:
|
||||||
|
return m, m.updateMouse(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) cycleFocus() {
|
||||||
|
if m.focus == focusLayout {
|
||||||
|
m.focus = focusMouse
|
||||||
|
} else {
|
||||||
|
m.focus = focusLayout
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
|
"github.com/anotherhadi/settuings/internal/input"
|
||||||
|
)
|
||||||
|
|
||||||
|
func windowStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(ilovetui.S.Subtle).
|
||||||
|
Padding(1, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) View() tea.View {
|
||||||
|
statusBar := m.renderStatusBar()
|
||||||
|
innerW, innerH := m.contentBudget()
|
||||||
|
tabBar := renderTabBar(innerW)
|
||||||
|
|
||||||
|
var body string
|
||||||
|
switch {
|
||||||
|
case !m.checked:
|
||||||
|
body = ilovetui.S.Faint.Render("Checking hyprctl…")
|
||||||
|
case m.checkErr != nil:
|
||||||
|
body = m.renderUnavailable()
|
||||||
|
default:
|
||||||
|
body = m.renderBody(innerW)
|
||||||
|
}
|
||||||
|
|
||||||
|
box := lipgloss.NewStyle().Width(innerW).Height(innerH).Render(body)
|
||||||
|
content := windowStyle().Render(lipgloss.JoinVertical(lipgloss.Left, tabBar, box))
|
||||||
|
|
||||||
|
return tea.NewView(lipgloss.JoinVertical(lipgloss.Left, content, statusBar))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderStatusBar() string {
|
||||||
|
km := inputKeyMap{m: m, width: m.width}
|
||||||
|
return lipgloss.NewStyle().Padding(0, 1).Render(m.help.View(km))
|
||||||
|
}
|
||||||
|
|
||||||
|
// contentBudget returns the exact space available inside the bordered page
|
||||||
|
// box, below the tab bar and above the status bar.
|
||||||
|
func (m Model) contentBudget() (w, h int) {
|
||||||
|
frameW := windowStyle().GetHorizontalFrameSize()
|
||||||
|
frameH := windowStyle().GetVerticalFrameSize()
|
||||||
|
w = m.width - frameW
|
||||||
|
|
||||||
|
statusH := strings.Count(m.renderStatusBar(), "\n") + 1
|
||||||
|
tabBarH := lipgloss.Height(renderTabBar(w))
|
||||||
|
|
||||||
|
h = m.height - tabBarH - frameH - statusH
|
||||||
|
if h < 1 {
|
||||||
|
h = 1
|
||||||
|
}
|
||||||
|
return w, h
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderBody lays out the two sections side by side once there's enough
|
||||||
|
// width for both to breathe, and stacks them otherwise.
|
||||||
|
func (m Model) renderBody(width int) string {
|
||||||
|
const splitThreshold = 60
|
||||||
|
const gap = 3
|
||||||
|
|
||||||
|
_, height := m.contentBudget()
|
||||||
|
|
||||||
|
if width < splitThreshold {
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, m.renderLayout(width, height/2), "", m.renderMouse(width))
|
||||||
|
}
|
||||||
|
|
||||||
|
leftW := width * 3 / 5
|
||||||
|
if leftW < 30 {
|
||||||
|
leftW = 30
|
||||||
|
}
|
||||||
|
rightW := width - leftW - gap
|
||||||
|
left := lipgloss.NewStyle().Width(leftW).Render(m.renderLayout(leftW, height))
|
||||||
|
right := lipgloss.NewStyle().Width(rightW).Render(m.renderMouse(rightW))
|
||||||
|
return lipgloss.JoinHorizontal(lipgloss.Top, left, strings.Repeat(" ", gap), right)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderUnavailable() string {
|
||||||
|
title := lipgloss.NewStyle().Bold(true).Foreground(ilovetui.S.Error).Render("Inputs page unavailable")
|
||||||
|
|
||||||
|
var hint string
|
||||||
|
switch {
|
||||||
|
case errors.Is(m.checkErr, input.ErrNotFound):
|
||||||
|
hint = "hyprctl was not found in PATH. This page only works under Hyprland."
|
||||||
|
default:
|
||||||
|
hint = m.checkErr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left,
|
||||||
|
title,
|
||||||
|
"",
|
||||||
|
subtleStyle().Render(hint),
|
||||||
|
"",
|
||||||
|
mutedStyle().Render("Retrying automatically…"),
|
||||||
|
)
|
||||||
|
}
|
||||||
+7
-71
@@ -21,10 +21,6 @@ schema = 3
|
|||||||
version = 'v2.24.1'
|
version = 'v2.24.1'
|
||||||
hash = 'sha256-DufsljWRKireFuLFcnPozuF0N3UoRYGlEfNFMD+z0ng='
|
hash = 'sha256-DufsljWRKireFuLFcnPozuF0N3UoRYGlEfNFMD+z0ng='
|
||||||
|
|
||||||
[mod.'github.com/andybalholm/brotli']
|
|
||||||
version = 'v1.0.4'
|
|
||||||
hash = 'sha256-gAnPRdGP4yna4hiRIEDyBtDOVJqd7RU27wlPu96Rdf8='
|
|
||||||
|
|
||||||
[mod.'github.com/anotherhadi/ilovetui']
|
[mod.'github.com/anotherhadi/ilovetui']
|
||||||
version = 'v0.1.6'
|
version = 'v0.1.6'
|
||||||
hash = 'sha256-7E+7UFks5vM3XWCvX2joFmHGcW7qqnoox6ZPFglaLO4='
|
hash = 'sha256-7E+7UFks5vM3XWCvX2joFmHGcW7qqnoox6ZPFglaLO4='
|
||||||
@@ -77,10 +73,6 @@ schema = 3
|
|||||||
version = 'v1.12.0'
|
version = 'v1.12.0'
|
||||||
hash = 'sha256-PVX2rDCkiG0vyA1CbDi3bzLeZ2T8hcqJv3pZ5YwGzMI='
|
hash = 'sha256-PVX2rDCkiG0vyA1CbDi3bzLeZ2T8hcqJv3pZ5YwGzMI='
|
||||||
|
|
||||||
[mod.'github.com/dustin/go-humanize']
|
|
||||||
version = 'v1.0.1'
|
|
||||||
hash = 'sha256-yuvxYYngpfVkUg9yAmG99IUVmADTQA0tMbBXe0Fq0Mc='
|
|
||||||
|
|
||||||
[mod.'github.com/fsnotify/fsnotify']
|
[mod.'github.com/fsnotify/fsnotify']
|
||||||
version = 'v1.10.1'
|
version = 'v1.10.1'
|
||||||
hash = 'sha256-6LBLgsh4nKkMpgRKVsYFEaGDSU1fncBcWVSjKBdfgjU='
|
hash = 'sha256-6LBLgsh4nKkMpgRKVsYFEaGDSU1fncBcWVSjKBdfgjU='
|
||||||
@@ -89,42 +81,22 @@ schema = 3
|
|||||||
version = 'v2.5.0'
|
version = 'v2.5.0'
|
||||||
hash = 'sha256-LbrCBANBprVI84M0CWrXc7rriJL5ac5VKbh58LBTw7U='
|
hash = 'sha256-LbrCBANBprVI84M0CWrXc7rriJL5ac5VKbh58LBTw7U='
|
||||||
|
|
||||||
[mod.'github.com/golang/groupcache']
|
[mod.'github.com/godbus/dbus/v5']
|
||||||
version = 'v0.0.0-20210331224755-41bb18bfe9da'
|
version = 'v5.2.2'
|
||||||
hash = 'sha256-7Gs7CS9gEYZkbu5P4hqPGBpeGZWC64VDwraSKFF+VR0='
|
hash = 'sha256-j+LicPlXcB1R3oVFPa8cd4yVJLpBx+ca5gXmHqOJSN8='
|
||||||
|
|
||||||
[mod.'github.com/google/uuid']
|
|
||||||
version = 'v1.6.0'
|
|
||||||
hash = 'sha256-VWl9sqUzdOuhW0KzQlv0gwwUQClYkmZwSydHG2sALYw='
|
|
||||||
|
|
||||||
[mod.'github.com/gorilla/css']
|
[mod.'github.com/gorilla/css']
|
||||||
version = 'v1.0.1'
|
version = 'v1.0.1'
|
||||||
hash = 'sha256-6JwNHqlY2NpZ0pSQTyYPSpiNqjXOdFHqrUT10sv3y8A='
|
hash = 'sha256-6JwNHqlY2NpZ0pSQTyYPSpiNqjXOdFHqrUT10sv3y8A='
|
||||||
|
|
||||||
[mod.'github.com/gorilla/websocket']
|
|
||||||
version = 'v1.5.0'
|
|
||||||
hash = 'sha256-EYVgkSEMo4HaVrsWKqnsYRp8SSS8gNf7t+Elva02Ofc='
|
|
||||||
|
|
||||||
[mod.'github.com/inconshreveable/mousetrap']
|
[mod.'github.com/inconshreveable/mousetrap']
|
||||||
version = 'v1.1.0'
|
version = 'v1.1.0'
|
||||||
hash = 'sha256-XWlYH0c8IcxAwQTnIi6WYqq44nOKUylSWxWO/vi+8pE='
|
hash = 'sha256-XWlYH0c8IcxAwQTnIi6WYqq44nOKUylSWxWO/vi+8pE='
|
||||||
|
|
||||||
[mod.'github.com/klauspost/compress']
|
|
||||||
version = 'v1.17.8'
|
|
||||||
hash = 'sha256-8rgCCfHX29le8m6fyVn6gwFde5TPUHjwQqZqv9JIubs='
|
|
||||||
|
|
||||||
[mod.'github.com/lqqyt2423/go-mitmproxy']
|
|
||||||
version = 'v1.8.11'
|
|
||||||
hash = 'sha256-PoCL6TOt99JsenH4XFx+7MqIzzYnnLHYwUpEQ7kxxyg='
|
|
||||||
|
|
||||||
[mod.'github.com/lucasb-eyer/go-colorful']
|
[mod.'github.com/lucasb-eyer/go-colorful']
|
||||||
version = 'v1.4.0'
|
version = 'v1.4.0'
|
||||||
hash = 'sha256-i/3GDHKEMLCy0kc3mtyk58UWYOPmKoUVaq6QCAWXKP0='
|
hash = 'sha256-i/3GDHKEMLCy0kc3mtyk58UWYOPmKoUVaq6QCAWXKP0='
|
||||||
|
|
||||||
[mod.'github.com/mattn/go-isatty']
|
|
||||||
version = 'v0.0.22'
|
|
||||||
hash = 'sha256-6O/0jc33pKUzlzUGpH8Ekk54XgJvx6Qe7kJtbcNJAV4='
|
|
||||||
|
|
||||||
[mod.'github.com/mattn/go-runewidth']
|
[mod.'github.com/mattn/go-runewidth']
|
||||||
version = 'v0.0.23'
|
version = 'v0.0.23'
|
||||||
hash = 'sha256-SmChZ2U1aR8pW3LPhdM7KcVF5TO6VcHgRzBtUXbBWJA='
|
hash = 'sha256-SmChZ2U1aR8pW3LPhdM7KcVF5TO6VcHgRzBtUXbBWJA='
|
||||||
@@ -137,18 +109,10 @@ schema = 3
|
|||||||
version = 'v0.2.2'
|
version = 'v0.2.2'
|
||||||
hash = 'sha256-uEPpzwRJBJsQWBw6M71FDfgJuR7n55d/7IV8MO+rpwQ='
|
hash = 'sha256-uEPpzwRJBJsQWBw6M71FDfgJuR7n55d/7IV8MO+rpwQ='
|
||||||
|
|
||||||
[mod.'github.com/ncruces/go-strftime']
|
|
||||||
version = 'v1.0.0'
|
|
||||||
hash = 'sha256-GYIwYDONuv/yTE0AEugCHQbtV3oiBaco93xUNYFcVBQ='
|
|
||||||
|
|
||||||
[mod.'github.com/pelletier/go-toml/v2']
|
[mod.'github.com/pelletier/go-toml/v2']
|
||||||
version = 'v2.3.1'
|
version = 'v2.3.1'
|
||||||
hash = 'sha256-5H8+UOtPOs+Yc+8oVT/3bugCCdbq3jFMH6eOW8dadyg='
|
hash = 'sha256-5H8+UOtPOs+Yc+8oVT/3bugCCdbq3jFMH6eOW8dadyg='
|
||||||
|
|
||||||
[mod.'github.com/remyoudompheng/bigfft']
|
|
||||||
version = 'v0.0.0-20230129092748-24d4a6f8daec'
|
|
||||||
hash = 'sha256-vYmpyCE37eBYP/navhaLV4oX4/nu0Z/StAocLIFqrmM='
|
|
||||||
|
|
||||||
[mod.'github.com/rivo/uniseg']
|
[mod.'github.com/rivo/uniseg']
|
||||||
version = 'v0.4.7'
|
version = 'v0.4.7'
|
||||||
hash = 'sha256-rDcdNYH6ZD8KouyyiZCUEy8JrjOQoAkxHBhugrfHjFo='
|
hash = 'sha256-rDcdNYH6ZD8KouyyiZCUEy8JrjOQoAkxHBhugrfHjFo='
|
||||||
@@ -157,14 +121,6 @@ schema = 3
|
|||||||
version = 'v0.12.0'
|
version = 'v0.12.0'
|
||||||
hash = 'sha256-EXk9S5Z5sYyApAzCgHIugsGMbt/pHWRfHYFZH5D+5Ws='
|
hash = 'sha256-EXk9S5Z5sYyApAzCgHIugsGMbt/pHWRfHYFZH5D+5Ws='
|
||||||
|
|
||||||
[mod.'github.com/sahilm/fuzzy']
|
|
||||||
version = 'v0.1.1'
|
|
||||||
hash = 'sha256-f2VsDI6G+V2w31tSDzbZPi9EI2E7jRV6Aq8yeOorSZY='
|
|
||||||
|
|
||||||
[mod.'github.com/satori/go.uuid']
|
|
||||||
version = 'v1.2.0'
|
|
||||||
hash = 'sha256-y/lSGbnZa7mYJCs30a3LTyjfCFQSaYp8GbVR8dwtmsg='
|
|
||||||
|
|
||||||
[mod.'github.com/sirupsen/logrus']
|
[mod.'github.com/sirupsen/logrus']
|
||||||
version = 'v1.9.4'
|
version = 'v1.9.4'
|
||||||
hash = 'sha256-ltRvmtM3XTCAFwY0IesfRqYIivyXPPuvkFjL4ARh1wg='
|
hash = 'sha256-ltRvmtM3XTCAFwY0IesfRqYIivyXPPuvkFjL4ARh1wg='
|
||||||
@@ -205,14 +161,6 @@ schema = 3
|
|||||||
version = 'v1.0.6'
|
version = 'v1.0.6'
|
||||||
hash = 'sha256-+d6bZzOPE+JSFsZbQNZMCWE+n3jgcQnkPETVk47mxSY='
|
hash = 'sha256-+d6bZzOPE+JSFsZbQNZMCWE+n3jgcQnkPETVk47mxSY='
|
||||||
|
|
||||||
[mod.'github.com/yuin/gopher-lua']
|
|
||||||
version = 'v1.1.2'
|
|
||||||
hash = 'sha256-2YMCxv7RO3uqq6OTjvE0kR9nTt+n2cF+MrLtGEW68po='
|
|
||||||
|
|
||||||
[mod.'go.uber.org/atomic']
|
|
||||||
version = 'v1.11.0'
|
|
||||||
hash = 'sha256-TyYws/cSPVqYNffFX0gbDml1bD4bBGcysrUWU7mHPIY='
|
|
||||||
|
|
||||||
[mod.'go.yaml.in/yaml/v3']
|
[mod.'go.yaml.in/yaml/v3']
|
||||||
version = 'v3.0.4'
|
version = 'v3.0.4'
|
||||||
hash = 'sha256-NkGFiDPoCxbr3LFsI6OCygjjkY0rdmg5ggvVVwpyDQ4='
|
hash = 'sha256-NkGFiDPoCxbr3LFsI6OCygjjkY0rdmg5ggvVVwpyDQ4='
|
||||||
@@ -233,22 +181,10 @@ schema = 3
|
|||||||
version = 'v0.37.0'
|
version = 'v0.37.0'
|
||||||
hash = 'sha256-8XDOnlPIybcDRy89fkjG5VqtIt5Ku+LmaqYhgKl7i1E='
|
hash = 'sha256-8XDOnlPIybcDRy89fkjG5VqtIt5Ku+LmaqYhgKl7i1E='
|
||||||
|
|
||||||
|
[mod.'gopkg.in/check.v1']
|
||||||
|
version = 'v1.0.0-20201130134442-10cb98267c6c'
|
||||||
|
hash = 'sha256-VlIpM2r/OD+kkyItn6vW35dyc0rtkJufA93rjFyzncs='
|
||||||
|
|
||||||
[mod.'gopkg.in/yaml.v3']
|
[mod.'gopkg.in/yaml.v3']
|
||||||
version = 'v3.0.1'
|
version = 'v3.0.1'
|
||||||
hash = 'sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU='
|
hash = 'sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU='
|
||||||
|
|
||||||
[mod.'modernc.org/libc']
|
|
||||||
version = 'v1.72.3'
|
|
||||||
hash = 'sha256-eOCoqSnX/VzpG63nh1j3JpXRndnZZcn2cDTeutexXAI='
|
|
||||||
|
|
||||||
[mod.'modernc.org/mathutil']
|
|
||||||
version = 'v1.7.1'
|
|
||||||
hash = 'sha256-COZ5rF2GhQVR1r6a0DanJ8qwQ94JSKdQxTMWrDzE0Cc='
|
|
||||||
|
|
||||||
[mod.'modernc.org/memory']
|
|
||||||
version = 'v1.11.0'
|
|
||||||
hash = 'sha256-MkybF8vvrxXS5j7O8w3skwTo0aMo1yjWS0K440rYcHM='
|
|
||||||
|
|
||||||
[mod.'modernc.org/sqlite']
|
|
||||||
version = 'v1.50.1'
|
|
||||||
hash = 'sha256-JbGP3eerH/6mgzI0aq9SpLLwe6ld6PV/zJReG8mLQN0='
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
hooks = {
|
hooks = {
|
||||||
gofmt.enable = true;
|
gofmt.enable = true;
|
||||||
govet.enable = true;
|
govet.enable = true;
|
||||||
stylua.enable = true;
|
|
||||||
|
|
||||||
gomod2nix = {
|
gomod2nix = {
|
||||||
enable = true;
|
enable = true;
|
||||||
@@ -47,7 +46,6 @@ in
|
|||||||
govulncheck
|
govulncheck
|
||||||
python3
|
python3
|
||||||
doctoc
|
doctoc
|
||||||
stylua
|
|
||||||
trufflehog
|
trufflehog
|
||||||
gomod2nixPkgs.gomod2nix
|
gomod2nixPkgs.gomod2nix
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user