hyprland: add HyprlandManager

parent fcb380e1
package config
type ctxKey string
const HyprManagerKey ctxKey = "hyprManager"
......@@ -13,6 +13,23 @@ func CommandList() *cli.Command {
Name: "hyprland",
Hidden: config.Env.Hyprland == nil,
Usage: "Hyprland Management",
Before: func(ctx context.Context, command *cli.Command) (context.Context, error) {
manager, err := NewHyprlandManager()
if err != nil {
return ctx, fmt.Errorf("не удалось инициализировать HyprlandManager: %w", err)
}
ctx = context.WithValue(ctx, config.HyprManagerKey, manager)
return ctx, nil
},
After: func(ctx context.Context, command *cli.Command) error {
manager, err := GetHyprlandManager(ctx)
if err != nil {
return fmt.Errorf("не удалось инициализировать HyprlandManager: %w", err)
}
manager.Save()
return nil
},
Commands: []*cli.Command{
{
Name: "check",
......@@ -202,9 +219,12 @@ func ShellCompleteVarList(ctx context.Context, cmd *cli.Command) {
if cmd.NArg() > 0 {
return
}
data, _ := hyprlandVarList()
for _, t := range data {
fmt.Println(t)
manager, _ := NewHyprlandManager()
data := manager.Vars
for _, v := range data {
fmt.Println(v.Name)
}
}
......@@ -213,10 +233,11 @@ func ShellCompleteModule(filter string) func(ctx context.Context, cmd *cli.Comma
if cmd.NArg() > 0 {
return
}
manager, _ := NewHyprlandManager()
userFlag := cmd.Bool("user")
data := hyprlandListModules(userFlag, filter)
data := manager.GetModulesList(userFlag, filter)
for _, t := range data {
fmt.Println(t)
}
......@@ -229,7 +250,9 @@ func ShellCompletePlugin(filter string) func(ctx context.Context, cmd *cli.Comma
return
}
data := hyprlandPluginList(filter)
manager, _ := NewHyprlandManager()
data := manager.GetPluginsList(filter)
for _, t := range data {
fmt.Println(t)
}
......
package hyprland
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"ximperconf/config"
"ximperconf/utils"
)
type HyprlandManager struct {
Changed bool
ConfPath string
PluginsDir string
Home string
Lines []string
UserModules []string
SystemModules []string
Vars []HyprVar
Plugins []string
LoadedPlugins []HyprPlugin
}
type HyprModule struct {
Name string
Status string
Path string
ConfPath string
LineNumber int
Available bool
}
type HyprVar struct {
Name string
Value string
LineNumber int
}
type HyprPlugin struct {
Name string `json:"name"`
Author string `json:"author"`
Handle string `json:"handle"`
Version string `json:"version"`
Description string `json:"description"`
}
type HyprConfigError struct {
File string
Line int
Text string
}
func NewHyprlandManager() (*HyprlandManager, error) {
data, err := os.ReadFile(config.Env.Hyprland.Config)
if err != nil {
return nil, err
}
home, _ := os.UserHomeDir()
rawLines := strings.Split(string(data), "\n")
lines := make([]string, 0, len(rawLines))
for _, line := range rawLines {
lines = append(lines, normalizeHyprlandLine(line, home))
}
manager := &HyprlandManager{
ConfPath: config.Env.Hyprland.Config,
PluginsDir: config.Env.Hyprland.PluginsDir,
Lines: lines,
Home: home,
}
manager.Vars = manager.GetVarList()
manager.UserModules = manager.scanModulesDir(true)
manager.SystemModules = manager.scanModulesDir(false)
manager.Plugins = manager.scanPluginsDir()
manager.LoadedPlugins = manager.GetLoadedPlugins()
return manager, nil
}
func GetHyprlandManager(ctx context.Context) (*HyprlandManager, error) {
mgr, ok := ctx.Value(config.HyprManagerKey).(*HyprlandManager)
if !ok || mgr == nil {
return nil, fmt.Errorf("HyprlandManager не найден в контексте")
}
return mgr, nil
}
func (m *HyprlandManager) Save() error {
if m.Changed {
err := os.WriteFile(m.ConfPath, []byte(strings.Join(m.Lines, "\n")), 0644)
if err != nil {
return err
}
m.Changed = false
}
return nil
}
// Check
func (m *HyprlandManager) Check(configPath string) ([]HyprConfigError, error) {
if configPath == "" {
configPath = m.ConfPath
}
cmd := exec.Command(
"hyprland", "--verify-config",
"-c", configPath,
)
output, err := cmd.CombinedOutput()
if err != nil {
if _, ok := err.(*exec.ExitError); !ok {
return nil, fmt.Errorf("проверка не удалась")
}
}
return m.parseCheckOutput(output), nil
}
func (m *HyprlandManager) CheckModule(
module string,
user bool,
) ([]HyprConfigError, error) {
info := m.GetModuleInfo(module, user)
if info.Path == "" {
return nil, fmt.Errorf("модуль '%s' не найден", module)
}
tmp, err := os.CreateTemp("", "ximperconf-hypr-check-*.conf")
if err != nil {
return nil, err
}
defer os.Remove(tmp.Name())
for _, v := range m.Vars {
fmt.Fprintf(tmp, "$%s = %s\n", v.Name, v.Value)
}
fmt.Fprintf(tmp, "source = %s\n", info.ConfPath)
tmp.Close()
return m.Check(tmp.Name())
}
// Modules
func (m *HyprlandManager) GetModuleInfo(module string, user bool) HyprModule {
sysFile := m.GetModuleFile(module, user)
var ConfPath string
if user {
ConfPath = "~" + strings.TrimPrefix(sysFile, m.Home)
} else {
ConfPath = sysFile
}
Available := true
for _, skipModule := range config.HyprlandSkipModules {
if module == skipModule {
Available = false
}
}
FileExists := utils.FileExists(sysFile)
// Конфиг Hyprland отсутствует
if !utils.FileExists(config.Env.Hyprland.Config) {
if FileExists {
return HyprModule{
Name: module,
Status: "unused",
Path: sysFile,
ConfPath: ConfPath,
Available: Available,
}
}
return HyprModule{
Name: module,
Status: "missing",
Available: Available,
}
}
foundActive := false
foundCommented := false
var LineNumber int
for i, line := range m.Lines {
path, commented, ok := m.parseModuleLine(line)
if !ok || path != ConfPath {
continue
}
if commented {
foundCommented = true
LineNumber = i
continue
}
foundActive = true
LineNumber = i
break
}
// подключен
if foundActive {
// файла нет
if !FileExists {
return HyprModule{
Name: module,
Status: "missing",
LineNumber: LineNumber,
Available: Available,
}
}
// файл есть
return HyprModule{
Name: module,
Status: "enabled",
Path: sysFile,
ConfPath: ConfPath,
LineNumber: LineNumber,
Available: Available,
}
}
// закомментирован
if foundCommented {
// файла нет
Path := sysFile
if !FileExists {
Path = ""
}
// файл есть
return HyprModule{
Name: module,
Status: "disabled",
Path: Path,
ConfPath: ConfPath,
LineNumber: LineNumber,
Available: Available,
}
}
// файл есть, но строка отсутствует
if FileExists {
return HyprModule{
Name: module,
Status: "unused",
Path: sysFile,
ConfPath: ConfPath,
Available: Available,
}
}
// Файла нет и упоминаний нет
return HyprModule{
Name: module,
Status: "unknown",
Available: Available,
}
}
func (m *HyprlandManager) GetModuleFile(module string, user bool) string {
if user {
return filepath.Join(config.Env.Hyprland.UserModulesDir, module+".conf")
}
return filepath.Join(config.Env.Hyprland.SystemModulesDir, module+".conf")
}
func (m *HyprlandManager) GetModuleDir(user bool) string {
if user {
return config.Env.Hyprland.UserModulesDir
}
return config.Env.Hyprland.SystemModulesDir
}
func (m *HyprlandManager) GetModulesList(user bool, filter string) []string {
var modules []string
var res []string
if user {
modules = m.UserModules
} else {
modules = m.SystemModules
}
for _, module := range modules {
info := m.GetModuleInfo(module, user)
if !info.Available {
continue
}
if filter == "" || filter == "all" || info.Status == filter {
res = append(res, module)
}
}
return res
}
func (m *HyprlandManager) SetModule(action, module string, user, onlyNew bool) (string, error) {
info := m.GetModuleInfo(module, user)
if !info.Available {
return "", fmt.Errorf("недопустимое название для модуля")
}
if info.Status == "unknown" {
return "", fmt.Errorf("модуль '%s' не найден: %s", module, info.Path)
}
switch action {
case "enable":
// нет файла
if info.Path == "" || info.Status == "missing" {
return "", fmt.Errorf("нельзя включить данный модуль")
}
// уже включён
if info.Status == "enabled" {
return "", fmt.Errorf("модуль '%s' уже включён", module)
}
// был закомментирован
if info.Status == "disabled" {
if onlyNew {
return "", fmt.Errorf("модуль '%s' уже присутствует в конфиге (закомментирован) — пропущено", module)
}
m.Lines[info.LineNumber] = strings.TrimPrefix(m.Lines[info.LineNumber], "#")
m.Changed = true
return fmt.Sprintf("Модуль '%s' включён", module), nil
}
// не использовался
if info.Status == "unused" {
line := "source = " + info.ConfPath
m.Lines = append(m.Lines, line)
m.Changed = true
return fmt.Sprintf("Модуль '%s' включён", module), nil
}
return "", fmt.Errorf("модуль '%s' не изменён", module)
case "disable":
// Уже выключен
if info.Status == "disabled" || info.Status == "unused" {
return "", fmt.Errorf("модуль '%s' уже отключён", module)
}
// Включён
if info.Status == "enabled" || info.Status == "missing" {
m.Lines[info.LineNumber] = "#" + m.Lines[info.LineNumber]
m.Changed = true
return fmt.Sprintf("Модуль '%s' отключён", module), nil
}
case "remove":
if info.LineNumber <= 0 {
return "", fmt.Errorf("модуль '%s' не найден в конфигурации", module)
}
m.removeLine(info.LineNumber)
return fmt.Sprintf("Модуль '%s' удалён", module), nil
}
return "", nil
}
// Plugins
func (m *HyprlandManager) GetLoadedPlugins() []HyprPlugin {
out, err := exec.Command("hyprctl", "plugin", "list", "-j").Output()
if err != nil {
return nil
}
var plugins []HyprPlugin
json.Unmarshal(out, &plugins)
return plugins
}
func (m *HyprlandManager) GetPluginStatus(name string) string {
for _, p := range m.LoadedPlugins {
if p.Name == name {
return "loaded"
}
}
return "unloaded"
}
func (m *HyprlandManager) GetPluginFile(name string) (string, error) {
if name == "" {
return "", fmt.Errorf("плагин не указан")
}
path := filepath.Join(m.PluginsDir, name+".so")
if !utils.FileExists(path) {
return "", fmt.Errorf("плагин не найден")
}
return path, nil
}
func (m *HyprlandManager) GetPluginsList(filter string) []string {
plugins := m.Plugins
var list []string
for _, plugin := range plugins {
status := m.GetPluginStatus(plugin)
if filter == "" || filter == "all" || filter == status {
list = append(list, plugin)
}
}
return list
}
func (m *HyprlandManager) SetPlugin(action string, name string) (string, error) {
if name == "" {
return "", fmt.Errorf("плагин не указан")
}
path, err := m.GetPluginFile(name)
if err != nil {
return "", err
}
status := m.GetPluginStatus(name)
switch action {
case "load":
if status == "loaded" {
return "", fmt.Errorf("плагин уже загружен")
}
time.Sleep(1 * time.Second)
cmd := exec.Command("hyprctl", "plugin", "load", path)
if out, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("%v (%s)", err, out)
}
return fmt.Sprintf("Плагин '%s' загружен", name), nil
case "unload":
if status == "unloaded" {
return "", fmt.Errorf("плагин не загружен")
}
time.Sleep(1 * time.Second)
cmd := exec.Command("hyprctl", "plugin", "unload", path)
if out, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("%v (%s)", err, out)
}
return fmt.Sprintf("Плагин '%s' выгружен", name), nil
}
return "", nil
}
// Vars
func (m *HyprlandManager) GetVarList() []HyprVar {
var vars []HyprVar
for i, line := range m.Lines {
name, value, ok := m.parseVarLine(line)
if !ok {
continue
}
vars = append(vars, HyprVar{name, value, i})
}
return vars
}
func (m *HyprlandManager) GetVar(name string) string {
for _, v := range m.Vars {
if v.Name == name {
return v.Value
}
}
return ""
}
func (m *HyprlandManager) SetVar(name, value string) (string, error) {
if name == "" {
return "", fmt.Errorf("укажите имя переменной")
}
if value == "" {
return "", fmt.Errorf("укажите значение переменной")
}
for _, v := range m.Vars {
if v.Name == name {
if v.Value == value {
return "", fmt.Errorf("переменная '%s' уже '%s'", v.Name, v.Value)
}
m.Lines[v.LineNumber] = fmt.Sprintf("$%s = %s", name, value)
m.Changed = true
return fmt.Sprintf("Переменная '%s' обновлена: %s", name, value), nil
}
}
insertAt := -1
for i, line := range m.Lines {
if strings.Contains(line, "ПЕРЕМЕННЫЕ") && strings.Contains(line, "VARS") {
insertAt = i + 1
break
}
}
newLine := fmt.Sprintf("$%s = %s", name, value)
if insertAt >= 0 {
m.Lines = append(
m.Lines[:insertAt],
append([]string{newLine}, m.Lines[insertAt:]...)...,
)
m.Changed = true
return fmt.Sprintf("Переменная '%s' установлена: %s", name, value), nil
}
m.Lines = append(m.Lines,
"",
"#---------- ПЕРЕМЕННЫЕ ---- VARS",
newLine,
)
m.Changed = true
return fmt.Sprintf("Блок VARS создан, переменная '%s' установлена: %s", name, value), nil
}
func (m *HyprlandManager) UnsetVar(name string) error {
if name == "" {
return fmt.Errorf("укажите имя переменной")
}
for _, v := range m.Vars {
if v.Name == name {
m.removeLine(v.LineNumber)
return nil
}
}
return fmt.Errorf("переменная %s не найдена", name)
}
// ====================
func (m *HyprlandManager) parseModuleLine(line string) (path string, commented bool, ok bool) {
if strings.HasPrefix(line, "#") {
commented = true
line = strings.TrimSpace(strings.TrimPrefix(line, "#"))
}
if !strings.HasPrefix(line, "source") {
return "", commented, false
}
line = strings.TrimSpace(strings.TrimPrefix(line, "source"))
if !strings.HasPrefix(line, "=") {
return "", commented, false
}
path = strings.TrimSpace(strings.TrimPrefix(line, "="))
if path == "" {
return "", commented, false
}
return path, commented, true
}
func (m *HyprlandManager) parseVarLine(line string) (name, value string, ok bool) {
line = strings.TrimSpace(line)
// должна начинаться с $
if !strings.HasPrefix(line, "$") {
return "", "", false
}
left, right, found := strings.Cut(line, "=")
if !found {
return "", "", false
}
name = strings.TrimSpace(strings.TrimPrefix(left, "$"))
if name == "" {
return "", "", false
}
// значение: всё после =, комментарий убираем
right = strings.TrimSpace(right)
if idx := strings.Index(right, "#"); idx != -1 {
right = strings.TrimSpace(right[:idx])
}
value = right // может быть пустым — это ОК
return name, value, true
}
func (m *HyprlandManager) parseCheckOutput(out []byte) []HyprConfigError {
lines := strings.Split(string(out), "\n")
var res []HyprConfigError
start := false
for _, line := range lines {
if strings.Contains(line, "======== Config parsing result:") {
start = true
continue
}
if !start {
continue
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
if strings.Contains(line, ": Config error in file ") {
continue
}
prefix, msg, ok := strings.Cut(line, ": ")
if !ok {
continue
}
var file string
var lineNum int
if _, err := fmt.Sscanf(
prefix,
"Config error in file %s at line %d",
&file, &lineNum,
); err != nil {
continue
}
res = append(res, HyprConfigError{
File: file,
Line: lineNum,
Text: msg,
})
}
return res
}
func (m *HyprlandManager) removeLine(LineNumber int) {
m.Lines = append(m.Lines[:LineNumber], m.Lines[LineNumber+1:]...)
m.Changed = true
}
func (m *HyprlandManager) scanModulesDir(user bool) []string {
dir := m.GetModuleDir(user)
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}
var modules []string
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".conf") {
continue
}
modules = append(modules, strings.TrimSuffix(e.Name(), ".conf"))
}
return modules
}
func (m *HyprlandManager) scanPluginsDir() []string {
var list []string
entries, err := os.ReadDir(m.PluginsDir)
if err != nil {
return list
}
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".so") {
continue
}
name := strings.TrimSuffix(e.Name(), ".so")
list = append(list, name)
}
return list
}
func normalizeHyprlandLine(line, home string) string {
line = strings.TrimSpace(line)
if line == "" {
return ""
}
// проверяем, закомментирована ли строка
commented := strings.HasPrefix(line, "#")
if commented {
line = strings.TrimSpace(strings.TrimPrefix(line, "#"))
}
// пробелы вокруг "="
if strings.Contains(line, "=") {
parts := strings.SplitN(line, "=", 2)
left := strings.TrimSpace(parts[0])
right := strings.TrimSpace(parts[1])
line = left + " = " + right
}
// HOME → ~
if home != "" {
line = strings.ReplaceAll(line, home, "~")
}
if commented {
return "#" + line
}
return line
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment