shell: add panel management commands

parent d6d0309d
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"ximperconf/locale" "ximperconf/locale"
"ximperconf/preset" "ximperconf/preset"
"ximperconf/repo" "ximperconf/repo"
"ximperconf/shell"
"ximperconf/system" "ximperconf/system"
"ximperconf/ui" "ximperconf/ui"
...@@ -32,6 +33,7 @@ func main() { ...@@ -32,6 +33,7 @@ func main() {
repo.CommandList(), repo.CommandList(),
preset.CommandList(), preset.CommandList(),
hyprland.CommandList(), hyprland.CommandList(),
shell.CommandList(),
system.CommandList(), system.CommandList(),
{ {
Name: "help", Name: "help",
......
hyprland/actions.go hyprland/actions.go
hyprland/binds.go
hyprland/commands.go hyprland/commands.go
hyprland/keyboard-actions.go hyprland/keyboard-actions.go
hyprland/manager.go hyprland/manager.go
...@@ -11,6 +12,9 @@ preset/helpers.go ...@@ -11,6 +12,9 @@ preset/helpers.go
preset/result.go preset/result.go
repo/actions.go repo/actions.go
repo/commands.go repo/commands.go
shell/commands.go
shell/panel/actions.go
shell/panel/commands.go
system/commands.go system/commands.go
system/grub-actions.go system/grub-actions.go
ui/help.go ui/help.go
package shell
import (
"ximperconf/locale"
"ximperconf/shell/panel"
"github.com/urfave/cli/v3"
)
func CommandList() *cli.Command {
return &cli.Command{
Name: "shell",
Usage: locale.T("Ximper Shell Management"),
Commands: []*cli.Command{
panel.CommandList(),
},
}
}
package panel
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"ximperconf/config"
"ximperconf/locale"
"ximperconf/ui"
"github.com/fatih/color"
"github.com/urfave/cli/v3"
)
const panelBinary = "ximper-shell-panel"
type runtimeStatus struct {
Running bool `json:"running"`
SafeMode bool `json:"safe_mode"`
}
type panelStatus struct {
Running bool `json:"running"`
PID *int `json:"pid"`
RuntimeDir string `json:"runtime_dir"`
SafeMode bool `json:"safe_mode"`
}
type infoOutput struct {
Config Config `json:"config"`
Runtime runtimeStatus `json:"runtime"`
}
func InfoCommand(ctx context.Context, cmd *cli.Command) error {
cfg, err := loadConfig()
if err != nil {
return err
}
status, err := getPanelStatus()
if err != nil {
return err
}
output := infoOutput{
Config: cfg,
Runtime: runtimeStatus{
Running: status.Running,
SafeMode: status.SafeMode,
},
}
if config.IsJSON(cmd) {
return ui.PrintJSON(output)
}
printPanelInfo(cfg, output.Runtime)
return nil
}
func SetCommand(ctx context.Context, cmd *cli.Command) error {
key := cmd.Args().Get(0)
if key == "" || cmd.NArg() < 2 {
return errors.New(locale.T("specify option and value"))
}
cfg, err := loadConfig()
if err != nil {
return err
}
values := argValues(cmd, 1)
switch key {
case "position":
cfg.Position = values[0]
case "type":
cfg.Type = values[0]
case "modules-left":
modules, err := parseAndValidateModuleList(values)
if err != nil {
return err
}
cfg.ModulesLeft = modules
case "modules-center":
modules, err := parseAndValidateModuleList(values)
if err != nil {
return err
}
cfg.ModulesCenter = modules
case "modules-right":
modules, err := parseAndValidateModuleList(values)
if err != nil {
return err
}
cfg.ModulesRight = modules
default:
return fmt.Errorf(locale.T("unsupported panel option: %s"), key)
}
if err := saveConfig(cfg); err != nil {
return err
}
status, err := getPanelStatus()
if err != nil {
return err
}
if status.SafeMode {
color.Yellow(locale.T("Panel config updated, but safe mode is enabled; restart skipped"))
return nil
}
if err := runPanelCommand("restart"); err != nil {
return err
}
color.Green(locale.T("Panel config updated"))
return nil
}
func ListModulesCommand(ctx context.Context, cmd *cli.Command) error {
args := []string{"list-modules"}
if cmd.Bool("all") {
args = append(args, "--all")
}
if config.IsJSON(cmd) {
args = append(args, "-o", "json")
}
return runPanelCommand(args...)
}
func StartCommand(ctx context.Context, cmd *cli.Command) error {
return runPanelCommand("start")
}
func StopCommand(ctx context.Context, cmd *cli.Command) error {
return runPanelCommand("stop")
}
func ToggleCommand(ctx context.Context, cmd *cli.Command) error {
return runPanelCommand("toggle")
}
func RestartCommand(ctx context.Context, cmd *cli.Command) error {
return runPanelCommand("restart")
}
func getPanelStatus() (panelStatus, error) {
out, err := runPanelCommandOutput("status", "-o", "json")
if err != nil {
return panelStatus{}, err
}
var status panelStatus
if err := json.Unmarshal(out, &status); err != nil {
return panelStatus{}, fmt.Errorf(locale.T("parse panel status: %w"), err)
}
return status, nil
}
func validateModuleNames(names []string) error {
available, err := availablePanelModules()
if err != nil {
return err
}
var unknown []string
for _, name := range names {
if _, ok := available[name]; !ok {
unknown = append(unknown, name)
}
}
if len(unknown) > 0 {
return fmt.Errorf(locale.T("unknown panel module: %s"), strings.Join(unknown, ", "))
}
return nil
}
func parseAndValidateModuleList(values []string) ([]string, error) {
modules := parseModuleList(values)
if err := validateModuleNames(modules); err != nil {
return nil, err
}
return modules, nil
}
func availablePanelModules() (map[string]struct{}, error) {
names := make(map[string]struct{})
for _, args := range [][]string{
{"list-modules", "-o", "json"},
{"list-modules", "--all", "-o", "json"},
} {
out, err := runPanelCommandOutput(args...)
if err != nil {
return nil, err
}
var list []string
if err := json.Unmarshal(out, &list); err != nil {
return nil, fmt.Errorf(locale.T("parse panel modules: %w"), err)
}
for _, name := range list {
names[name] = struct{}{}
}
}
return names, nil
}
func runPanelCommand(args ...string) error {
path, err := exec.LookPath(panelBinary)
if err != nil {
return fmt.Errorf(locale.T("failed to find %s"), panelBinary)
}
c := exec.Command(path, args...)
c.Stdout = os.Stdout
c.Stderr = os.Stderr
c.Stdin = os.Stdin
return c.Run()
}
func runPanelCommandOutput(args ...string) ([]byte, error) {
path, err := exec.LookPath(panelBinary)
if err != nil {
return nil, fmt.Errorf(locale.T("failed to find %s"), panelBinary)
}
c := exec.Command(path, args...)
c.Stderr = os.Stderr
c.Stdin = os.Stdin
return c.Output()
}
func argValues(cmd *cli.Command, start int) []string {
values := make([]string, 0, cmd.NArg()-start)
for i := start; i < cmd.NArg(); i++ {
values = append(values, cmd.Args().Get(i))
}
return values
}
func printPanelInfo(cfg Config, runtime runtimeStatus) {
blue := color.New(color.FgBlue).SprintFunc()
fmt.Printf("%s:\n", blue(locale.T("Config")))
fmt.Printf(" %s: %s\n", blue(locale.T("Position")), cfg.Position)
fmt.Printf(" %s: %s\n", blue(locale.T("Type")), cfg.Type)
fmt.Printf(" %s:\n", blue(locale.T("Modules")))
fmt.Printf(" %s: %s\n", blue(locale.T("Left")), strings.Join(cfg.ModulesLeft, " "))
fmt.Printf(" %s: %s\n", blue(locale.T("Center")), strings.Join(cfg.ModulesCenter, " "))
fmt.Printf(" %s: %s\n", blue(locale.T("Right")), strings.Join(cfg.ModulesRight, " "))
fmt.Printf("\n%s:\n", blue(locale.T("Status")))
fmt.Printf(" %s: %s\n", blue(locale.T("Running")), coloredYesNo(runtime.Running))
fmt.Printf(" %s: %s\n", blue(locale.T("Safe mode")), coloredSafeMode(runtime.SafeMode))
}
func coloredYesNo(value bool) string {
if value {
return color.GreenString(locale.T("yes"))
}
return color.RedString(locale.T("no"))
}
func coloredSafeMode(value bool) string {
if value {
return color.YellowString(locale.T("yes"))
}
return color.GreenString(locale.T("no"))
}
package panel
import (
"ximperconf/config"
"ximperconf/locale"
"github.com/urfave/cli/v3"
)
func CommandList() *cli.Command {
return &cli.Command{
Name: "panel",
Usage: locale.T("Panel management"),
Commands: []*cli.Command{
{
Name: "info",
Usage: locale.T("Panel information"),
Flags: []cli.Flag{config.FormatFlag},
Action: InfoCommand,
},
{
Name: "set",
Usage: locale.T("Set panel option"),
ArgsUsage: "option value",
Action: SetCommand,
},
{
Name: "list-modules",
Usage: locale.T("List available panel modules"),
Flags: []cli.Flag{
config.FormatFlag,
&cli.BoolFlag{
Name: "all",
Usage: locale.T("show full Waybar module names"),
},
},
Action: ListModulesCommand,
},
{
Name: "start",
Usage: locale.T("Start panel"),
Action: StartCommand,
},
{
Name: "stop",
Usage: locale.T("Stop panel"),
Action: StopCommand,
},
{
Name: "toggle",
Usage: locale.T("Toggle panel"),
Action: ToggleCommand,
},
{
Name: "restart",
Usage: locale.T("Restart panel"),
Action: RestartCommand,
},
},
}
}
package panel
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
type Config struct {
Position string `json:"position"`
Type string `json:"type"`
ModulesLeft []string `json:"modules_left"`
ModulesCenter []string `json:"modules_center"`
ModulesRight []string `json:"modules_right"`
}
func defaultConfig() Config {
return Config{
Position: "top",
Type: "panel",
ModulesLeft: []string{"image#menu"},
ModulesCenter: []string{"workspaces"},
ModulesRight: []string{"group/volume", "network", "clock"},
}
}
func configPath() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config", "ximper-shell", "panel", "config.json")
}
func loadConfig() (Config, error) {
cfg := defaultConfig()
data, err := os.ReadFile(configPath())
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return cfg, nil
}
return cfg, err
}
if len(bytes.TrimSpace(data)) == 0 {
return cfg, nil
}
if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, fmt.Errorf("parse panel config: %w", err)
}
return cfg, cfg.validate()
}
func saveConfig(cfg Config) error {
if err := cfg.validate(); err != nil {
return err
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
path := configPath()
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
return os.WriteFile(path, append(data, '\n'), 0644)
}
func (c Config) validate() error {
switch c.Position {
case "top", "bottom", "left", "right":
default:
return fmt.Errorf("unsupported position %q", c.Position)
}
switch c.Type {
case "panel", "floating", "islands":
default:
return fmt.Errorf("unsupported panel type %q", c.Type)
}
for _, name := range append(append([]string{}, c.ModulesLeft...), append(c.ModulesCenter, c.ModulesRight...)...) {
if strings.TrimSpace(name) == "" {
return errors.New("module name cannot be empty")
}
}
return nil
}
func parseModuleList(parts []string) []string {
raw := strings.Join(parts, " ")
raw = strings.ReplaceAll(raw, ",", " ")
return strings.Fields(raw)
}
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