Initial commit
This commit is contained in:
commit
23bb7a61ba
|
@ -0,0 +1,2 @@
|
|||
# Openbsd core dumps
|
||||
*.core
|
|
@ -0,0 +1,47 @@
|
|||
package cmdlets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Usage struct {
|
||||
Name string
|
||||
Short string
|
||||
Long []string
|
||||
Flags map[string]string
|
||||
Examples []string
|
||||
}
|
||||
|
||||
func (u Usage) String() string {
|
||||
str := strings.Builder{}
|
||||
str.WriteString(
|
||||
fmt.Sprintf("%s SUBCOMMAND\n\n", strings.ToUpper(u.Name)),
|
||||
)
|
||||
str.WriteString(fmt.Sprintf("\t%s\t%s\n\n", u.Name, u.Short))
|
||||
for _, line := range u.Long {
|
||||
str.WriteString(fmt.Sprintf("\t%s\n", line))
|
||||
}
|
||||
str.WriteRune('\n')
|
||||
if len(u.Flags) != 0 {
|
||||
for flag, desc := range u.Flags {
|
||||
str.WriteString(fmt.Sprintf("\t%s\n\t\t%s\n", flag, desc))
|
||||
}
|
||||
str.WriteRune('\n')
|
||||
}
|
||||
if len(u.Examples) != 0 {
|
||||
str.WriteString("EXAMPLES\n\n")
|
||||
for _, example := range u.Examples {
|
||||
str.WriteString(fmt.Sprintf("\t%s\n", example))
|
||||
}
|
||||
str.WriteRune('\n')
|
||||
}
|
||||
|
||||
return str.String()
|
||||
}
|
||||
|
||||
type Commandlet interface {
|
||||
Name() string
|
||||
Usage() Usage
|
||||
Invoke(args ...string) error
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"sectorinf.com/emilis/hlctl/cmdlets"
|
||||
"sectorinf.com/emilis/hlctl/config"
|
||||
"sectorinf.com/emilis/hlctl/ctllog"
|
||||
"sectorinf.com/emilis/hlctl/groupctl"
|
||||
"sectorinf.com/emilis/hlctl/hlcl"
|
||||
"sectorinf.com/emilis/hlctl/notifyctl"
|
||||
)
|
||||
|
||||
var log = ctllog.Logger{}.New("group", ctllog.ColorRGB{}.FromHex("27ee7e"))
|
||||
|
||||
type command struct{}
|
||||
|
||||
var Command command
|
||||
|
||||
func (command) Name() string {
|
||||
return "group"
|
||||
}
|
||||
|
||||
func (command) Usage() cmdlets.Usage {
|
||||
return cmdlets.Usage{
|
||||
Name: Command.Name(),
|
||||
Short: "Controls tag grouping and behavior",
|
||||
Long: []string{
|
||||
"`group` is responsible for controlling how groups behave",
|
||||
"in hlwm, and switching between them",
|
||||
},
|
||||
Flags: map[string]string{
|
||||
"+": "Jump to next group",
|
||||
"-": "Jump to previous group",
|
||||
"[number]": "Jump to [number] group",
|
||||
"move [number]": "Move the currently focused window to group [number]",
|
||||
},
|
||||
Examples: []string{
|
||||
"hcctl group +",
|
||||
"hcctl group -",
|
||||
"hcctl group 2",
|
||||
"hcctl group move 2",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (command) Invoke(args ...string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("group requires arguments")
|
||||
}
|
||||
arg := args[0]
|
||||
if arg == "move" && len(args) < 2 {
|
||||
return errors.New("group move requires an argument")
|
||||
}
|
||||
var active uint8
|
||||
currentActiveCfg, err := config.Get(config.ActiveGroup)
|
||||
if err != nil {
|
||||
log.Warnf(
|
||||
"error getting active group, defaulting to 1: %s",
|
||||
err.Error(),
|
||||
)
|
||||
active = 1
|
||||
} else {
|
||||
val, err := currentActiveCfg.Uint()
|
||||
mustUint(config.ActiveGroup, err)
|
||||
active = uint8(val)
|
||||
}
|
||||
log.Print("Loading num of tags per group")
|
||||
numCfg, err := config.Get(config.TagsPerGroup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", config.TagsPerGroup, err)
|
||||
}
|
||||
num, err := numCfg.Uint()
|
||||
mustUint(config.TagsPerGroup, err)
|
||||
log.Print(num, "tags per group")
|
||||
focus, err := config.GetFocusedTag()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get focused tag: %w", err)
|
||||
}
|
||||
log.Print("Currently in", focus, "tag")
|
||||
|
||||
maxCfg, err := config.Get(config.GroupCount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", config.GroupCount, err)
|
||||
}
|
||||
max, err := maxCfg.Uint()
|
||||
mustUint(config.GroupCount, err)
|
||||
|
||||
var (
|
||||
newActive uint8
|
||||
)
|
||||
|
||||
switch arg {
|
||||
case "+":
|
||||
newActive = shiftGroup(false, active, uint8(max))
|
||||
case "-":
|
||||
newActive = shiftGroup(true, active, uint8(max))
|
||||
case "move":
|
||||
a, err := strconv.ParseUint(arg, 10, 8)
|
||||
target := uint8(a)
|
||||
if err != nil || target < 1 || target > max {
|
||||
return fmt.Errorf("out of range, use [1:%d]", max)
|
||||
}
|
||||
return groupctl.MoveTo(uint8(a))
|
||||
default:
|
||||
a, err := strconv.ParseUint(arg, 10, 8)
|
||||
target := uint8(a)
|
||||
if err != nil || target < 1 || target > max {
|
||||
return fmt.Errorf("out of range, use [1:%d]", max)
|
||||
}
|
||||
newActive = uint8(a)
|
||||
}
|
||||
|
||||
if active != newActive {
|
||||
log.Printf("Group %d -> %d", active, newActive)
|
||||
config.Set(config.ActiveGroup, strconv.Itoa(int(newActive)))
|
||||
// shift the tag index
|
||||
if err := groupctl.Update(); err != nil {
|
||||
return fmt.Errorf(
|
||||
"updating groups [%d -> %d]: %w",
|
||||
active,
|
||||
newActive,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if err := groupctl.SetKeybinds(newActive, uint8(num)); err != nil {
|
||||
return fmt.Errorf("SetKeybinds: %w", err)
|
||||
}
|
||||
|
||||
fg, _ := config.Get(config.ColorText)
|
||||
bg, _ := config.Get(config.ColorNormal)
|
||||
font, _ := config.Get(config.Font)
|
||||
return notifyctl.Display(
|
||||
fmt.Sprintf("Group %d", newActive),
|
||||
2,
|
||||
fg.String(),
|
||||
bg.String(),
|
||||
font.String(),
|
||||
)
|
||||
}
|
||||
|
||||
func moveTo(currentTag, currentGroup, newGroup, num uint8) error {
|
||||
relTag := (currentTag - ((currentGroup - 1) * num))
|
||||
newTag := ((newGroup - 1) * num) + relTag
|
||||
return hlcl.MoveIndex(uint8(newTag))
|
||||
}
|
||||
|
||||
func mustUint(cfg string, err error) {
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf(
|
||||
"%s must be set as uint: %s",
|
||||
cfg,
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
func shiftGroup(backwards bool, active, num uint8) uint8 {
|
||||
// Maintain the logic of the fish scripts,
|
||||
// 0 == 1, act as if groups are 1 indexed
|
||||
if backwards && active <= 1 {
|
||||
active = num
|
||||
} else if backwards {
|
||||
active--
|
||||
} else if active >= num {
|
||||
active = 1
|
||||
} else {
|
||||
active++
|
||||
}
|
||||
return uint8(active)
|
||||
}
|
|
@ -0,0 +1,323 @@
|
|||
package init
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sectorinf.com/emilis/hlctl/cmdlets"
|
||||
"sectorinf.com/emilis/hlctl/config"
|
||||
"sectorinf.com/emilis/hlctl/ctllog"
|
||||
"sectorinf.com/emilis/hlctl/groupctl"
|
||||
"sectorinf.com/emilis/hlctl/hlcl"
|
||||
"sectorinf.com/emilis/hlctl/hlcl/cmd"
|
||||
"sectorinf.com/emilis/hlctl/panelctl"
|
||||
"sectorinf.com/emilis/hlctl/svcctl"
|
||||
)
|
||||
|
||||
const (
|
||||
Return = "Return"
|
||||
Shift = "Shift"
|
||||
Tab = "Tab"
|
||||
Left = "Left"
|
||||
Right = "Right"
|
||||
Up = "Up"
|
||||
Down = "Down"
|
||||
Space = "space"
|
||||
Control = "Control"
|
||||
Backtick = "grave"
|
||||
)
|
||||
|
||||
var log = ctllog.Logger{}.New("init", ctllog.Green)
|
||||
|
||||
type command struct{}
|
||||
|
||||
var Command command
|
||||
|
||||
func (command) Name() string {
|
||||
return "init"
|
||||
}
|
||||
|
||||
func (command) Usage() cmdlets.Usage {
|
||||
return cmdlets.Usage{
|
||||
Name: Command.Name(),
|
||||
Short: "Initialize herbstluftwm, should be run from " +
|
||||
"the autostart script",
|
||||
Long: []string{
|
||||
"`init` starts the configuration of herbstluftwm with the",
|
||||
"default configuration, setting up keybinds, hlctl groups,",
|
||||
"and other commands.",
|
||||
},
|
||||
Flags: map[string]string{},
|
||||
Examples: []string{
|
||||
"hcctl init",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func logError(err error) {
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
type Keybind struct {
|
||||
Keys []string
|
||||
Command cmd.Command
|
||||
Args []string
|
||||
}
|
||||
|
||||
func (k Keybind) Set() {
|
||||
err := hlcl.Keybind(k.Keys, k.Command, k.Args...)
|
||||
if err != nil {
|
||||
log.Errorf(
|
||||
"Keybind [%s]: %s", strings.Join(k.Args, "-"), err,
|
||||
)
|
||||
} else {
|
||||
binds := strings.Join(k.Keys, "-")
|
||||
command := strings.Join(
|
||||
append([]string{k.Command.String()}, k.Args...),
|
||||
" ",
|
||||
)
|
||||
log.Printf("%s -> [%s]", binds, command)
|
||||
}
|
||||
}
|
||||
|
||||
func Keybinds(cfg config.HlwmConfig) []Keybind {
|
||||
// convenience for setting []string
|
||||
c := func(v ...string) []string {
|
||||
return v
|
||||
}
|
||||
key := func(keys []string, cm cmd.Command, args ...string) Keybind {
|
||||
return Keybind{
|
||||
Keys: keys,
|
||||
Command: cm,
|
||||
Args: args,
|
||||
}
|
||||
}
|
||||
Mod := cfg.Mod
|
||||
step := "+0.05"
|
||||
base := []Keybind{
|
||||
key(c(Mod, Shift, "r"), cmd.Reload),
|
||||
key(c(Mod, Shift, "c"), cmd.Close),
|
||||
key(c(Mod, Return), cmd.Spawn, "dmenu_run"),
|
||||
key(c(Mod, "s"), cmd.Spawn, "flameshot", "gui"),
|
||||
key(c(Mod, cfg.TermSpawnKey), cmd.Spawn, cfg.Term),
|
||||
|
||||
// Focus
|
||||
key(c(Mod, Left), cmd.Focus, "left"),
|
||||
key(c(Mod, Right), cmd.Focus, "right"),
|
||||
key(c(Mod, Up), cmd.Focus, "up"),
|
||||
key(c(Mod, Down), cmd.Focus, "down"),
|
||||
key(c(Mod, Tab), cmd.Cycle),
|
||||
key(c(Mod, "i"), cmd.JumpTo, "urgent"),
|
||||
|
||||
// Move frames
|
||||
key(c(Mod, Shift, Left), cmd.Shift, "left"),
|
||||
key(c(Mod, Shift, Right), cmd.Shift, "right"),
|
||||
key(c(Mod, Shift, Up), cmd.Shift, "up"),
|
||||
key(c(Mod, Shift, Down), cmd.Shift, "down"),
|
||||
|
||||
// Frames
|
||||
key(c(Mod, "u"), cmd.Split, "bottom", "0.7"),
|
||||
key(c(Mod, "o"), cmd.Split, "right", "0.5"),
|
||||
key(c(Mod, "r"), cmd.Remove),
|
||||
key(c(Mod, "f"), cmd.Fullscreen, "toggle"),
|
||||
key(c(Mod, Space), cmd.CycleLayout),
|
||||
|
||||
// Resizing
|
||||
key(c(Mod, Control, Left), cmd.Resize, "left", step),
|
||||
key(c(Mod, Control, Right), cmd.Resize, "right", step),
|
||||
key(c(Mod, Control, Up), cmd.Resize, "up", step),
|
||||
key(c(Mod, Control, Down), cmd.Resize, "down", step),
|
||||
|
||||
// Commands
|
||||
key(c(Mod, Backtick), cmd.Spawn, "hlctl", "group", "+"),
|
||||
}
|
||||
|
||||
for i := 0; i < int(cfg.Groups.Groups); i++ {
|
||||
base = append(
|
||||
base,
|
||||
key(c(Mod, fmt.Sprintf("F%d", i+1)), cmd.Spawn,
|
||||
"hlctl", "group", strconv.Itoa(i+1)),
|
||||
)
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
type set struct {
|
||||
name string
|
||||
value any
|
||||
}
|
||||
|
||||
func (s set) String() string {
|
||||
return fmt.Sprint(s.value)
|
||||
}
|
||||
|
||||
func Theme(cfg config.HlwmConfig) {
|
||||
s := func(name string, value any) set {
|
||||
return set{
|
||||
name: name,
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
col := cfg.Theme.Colors
|
||||
// Theme sets
|
||||
for _, row := range [...]set{
|
||||
s("frame_border_active_color", col.Active),
|
||||
s("frame_border_normal_color", "black"),
|
||||
s("frame_bg_active_color", col.Active),
|
||||
s("frame_border_width", 2),
|
||||
s("show_frame_decorations", "focused_if_multiple"),
|
||||
s("tree_style", "╾│ ├└╼─┐"),
|
||||
s("frame_bg_transparent", "on"),
|
||||
s("smart_window_surroundings", "off"), // TODO
|
||||
s("smart_frame_surroundings", "on"),
|
||||
s("frame_transparent_width", 5),
|
||||
s("frame_gap", 3),
|
||||
s("window_gap", 0),
|
||||
s("frame_padding", 0),
|
||||
s("mouse_recenter_gap", 0),
|
||||
} {
|
||||
value := row.String()
|
||||
if err := hlcl.Set(row.name, value); err != nil {
|
||||
log.Errorf("Set [%s] -> [%s]: %s", row.name, value, err)
|
||||
}
|
||||
}
|
||||
// Theme attrs
|
||||
for _, row := range [...]set{
|
||||
s("reset", 1),
|
||||
s("tiling.reset", 1),
|
||||
s("floating.reset", 1),
|
||||
s("title_height", 15),
|
||||
s("title_when", "multiple_tabs"),
|
||||
s("title_font", cfg.FontBold),
|
||||
s("title_depth", 3),
|
||||
s("inner_width", 0),
|
||||
s("inner_color", "black"),
|
||||
s("border_width", 1),
|
||||
s("floating.border_width", 4),
|
||||
s("floating.outer_width", 1),
|
||||
s("tiling.outer_width", 1),
|
||||
s("background_color", "black"),
|
||||
|
||||
s("active.color", col.Active),
|
||||
s("active.inner_color", col.Active),
|
||||
s("active.outer_color", col.Active),
|
||||
s("normal.color", col.Normal),
|
||||
s("normal.inner_color", col.Normal),
|
||||
s("normal.outer_color", col.Normal),
|
||||
s("urgent.color", col.Urgent),
|
||||
s("urgent.inner_color", col.Urgent),
|
||||
s("urgent.outer_color", col.Urgent),
|
||||
s("title_color", "white"),
|
||||
s("normal.title_color", col.Text),
|
||||
} {
|
||||
attr := fmt.Sprintf("theme.%s", row.name)
|
||||
v := row.String()
|
||||
log.Printf("Setting [%s] -> [%s]", attr, v)
|
||||
if err := hlcl.SetAttr(attr, v); err != nil {
|
||||
log.Errorf("Attr [%s] -> [%s]: %s", attr, v, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SetRules() {
|
||||
sFmt := "windowtype~\"_NET_WM_WINDOW_TYPE_%s\""
|
||||
logError(hlcl.Rule("focus=on"))
|
||||
logError(hlcl.Rule("floatplacement=smart"))
|
||||
logError(hlcl.Rule("fixedsize", "floating=on"))
|
||||
logError(hlcl.Rule(
|
||||
fmt.Sprintf(sFmt, "(DIALOG|UTILITY|SPLASH)"),
|
||||
"floating=on",
|
||||
))
|
||||
|
||||
logError(hlcl.Rule(
|
||||
fmt.Sprintf(sFmt, "(NOTIFICATION|DOCK|DESKTOP)"),
|
||||
"manage=off",
|
||||
))
|
||||
}
|
||||
|
||||
func AddTags(groupCount, tagsPerGroup uint8) error {
|
||||
for group := 0; group < int(groupCount); group++ {
|
||||
for tag := 0; tag < int(tagsPerGroup); tag++ {
|
||||
err := hlcl.AddTag(fmt.Sprintf("%d|%d", group+1, tag+1))
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"Adding tag [%d] for group [%d]: %s",
|
||||
tag+1, group+1, err,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
defaultAttr, err := hlcl.GetAttr("tags.by-name.default.index")
|
||||
if err != nil {
|
||||
// no default
|
||||
log.Print("No 'default' tag, continuing...")
|
||||
return nil
|
||||
}
|
||||
focus, err := config.GetFocusedTag()
|
||||
if err != nil {
|
||||
return fmt.Errorf("(removing default) getting current tag: %s", err)
|
||||
}
|
||||
def, err := strconv.ParseUint(defaultAttr, 10, 8)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Bug? default index not uint: %s", err)
|
||||
}
|
||||
if uint8(def) == focus {
|
||||
if err := hlcl.UseIndex(focus + 1); err != nil {
|
||||
log.Warnf("Tried switching away from default: %s", err)
|
||||
}
|
||||
}
|
||||
if err := hlcl.MergeTag("default"); err != nil {
|
||||
return fmt.Errorf("merging default: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (command) Invoke(args ...string) error {
|
||||
log.Print("begining herbstluftwm setup via hlctl")
|
||||
log.Print("loading config")
|
||||
cfg := config.Load()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return fmt.Errorf("validating config: %s", err)
|
||||
}
|
||||
if err := cfg.SetAll(); err != nil {
|
||||
return fmt.Errorf("setting cfg values to hlwm: %s", err)
|
||||
}
|
||||
|
||||
if err := hlcl.Lock(); err != nil {
|
||||
return fmt.Errorf("failed lock: %w", err)
|
||||
}
|
||||
|
||||
exec.Command("xsetroot", "-solid", "black").Run()
|
||||
logError(hlcl.KeyUnbind())
|
||||
|
||||
log.Print("Adding tags")
|
||||
logError(AddTags(cfg.Groups.Groups, cfg.Groups.Tags))
|
||||
logError(groupctl.ResetActive())
|
||||
|
||||
for _, key := range Keybinds(cfg) {
|
||||
key.Set()
|
||||
}
|
||||
log.Print("Resetting groups")
|
||||
logError(groupctl.ResetActive())
|
||||
|
||||
log.Print("Theming...")
|
||||
Theme(cfg)
|
||||
|
||||
log.Print("Rules")
|
||||
SetRules()
|
||||
|
||||
log.Print("Unlocking")
|
||||
if err := hlcl.Unlock(); err != nil {
|
||||
return fmt.Errorf("failed unlock: %w", err)
|
||||
}
|
||||
|
||||
svcctl.RestartServices(cfg.Services)
|
||||
|
||||
log.Print("Running panels")
|
||||
return panelctl.Run()
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package notify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sectorinf.com/emilis/hlctl/cmdlets"
|
||||
"sectorinf.com/emilis/hlctl/config"
|
||||
"sectorinf.com/emilis/hlctl/ctllog"
|
||||
"sectorinf.com/emilis/hlctl/notifyctl"
|
||||
)
|
||||
|
||||
var log = ctllog.Logger{}.New("notify", ctllog.ColorRGB{}.FromHex("ff003e"))
|
||||
|
||||
type command struct{}
|
||||
|
||||
var Command command
|
||||
|
||||
func (command) Name() string {
|
||||
return "notify"
|
||||
}
|
||||
|
||||
func (command) Usage() cmdlets.Usage {
|
||||
return cmdlets.Usage{
|
||||
Name: Command.Name(),
|
||||
Short: "Draws notifications using dzen2",
|
||||
Long: []string{
|
||||
"`notify` Will create notifications using the theme colors",
|
||||
"that hlwm was set over",
|
||||
},
|
||||
Flags: map[string]string{
|
||||
"-s": "Amount of seconds to persist for, default 1",
|
||||
},
|
||||
Examples: []string{
|
||||
"notify -s 3 \"Hello world\"",
|
||||
"notify Hey what's up chicken head",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (command) Invoke(args ...string) error {
|
||||
var (
|
||||
secs string = "1"
|
||||
)
|
||||
cleanArgs := make([]string, 0, len(args))
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
if len(arg) > 1 && arg[0] == '-' {
|
||||
var next string
|
||||
if i+1 < len(args) {
|
||||
next = args[i+1]
|
||||
}
|
||||
switch arg[1] {
|
||||
case 's':
|
||||
secs = next
|
||||
i++
|
||||
default:
|
||||
cleanArgs = append(cleanArgs, arg)
|
||||
}
|
||||
} else {
|
||||
cleanArgs = append(cleanArgs, arg)
|
||||
}
|
||||
}
|
||||
if len(cleanArgs) == 0 {
|
||||
return errors.New("no message")
|
||||
}
|
||||
message := strings.Join(cleanArgs, " ")
|
||||
cfg := config.Load()
|
||||
secsNum, err := strconv.ParseUint(secs, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("seconds[%s]: %w", secs, err)
|
||||
}
|
||||
|
||||
c := cfg.Theme.Colors
|
||||
return notifyctl.Display(
|
||||
message,
|
||||
uint(secsNum),
|
||||
c.Text,
|
||||
c.Normal,
|
||||
cfg.Font,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package save
|
||||
|
||||
import (
|
||||
"sectorinf.com/emilis/hlctl/cmdlets"
|
||||
"sectorinf.com/emilis/hlctl/config"
|
||||
"sectorinf.com/emilis/hlctl/ctllog"
|
||||
)
|
||||
|
||||
var log = ctllog.Logger{}.New("save", ctllog.Green)
|
||||
|
||||
type command struct{}
|
||||
|
||||
var Command command
|
||||
|
||||
func (command) Name() string {
|
||||
return "save"
|
||||
}
|
||||
|
||||
func (command) Usage() cmdlets.Usage {
|
||||
return cmdlets.Usage{
|
||||
Name: Command.Name(),
|
||||
Short: "Save the currently loaded configuration to file",
|
||||
Long: []string{
|
||||
"`save` simply saves the configurable state of hlctl",
|
||||
"to a configuration file located at",
|
||||
"$HOME/.config/herbstluftwm/hlctl.toml",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (command) Invoke(args ...string) error {
|
||||
return config.Collect().Save()
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"sectorinf.com/emilis/hlctl/hlcl"
|
||||
"sectorinf.com/emilis/hlctl/hlcl/cmd"
|
||||
)
|
||||
|
||||
const (
|
||||
Term = "Term"
|
||||
Font = "Font"
|
||||
FontBold = "FontBold"
|
||||
ModKey = "ModKey"
|
||||
TermSpawnKey = "TermSpawnKey"
|
||||
GroupCount = "GroupCount"
|
||||
TagsPerGroup = "TagsPerGroup"
|
||||
ActiveGroup = "ActiveGroup"
|
||||
Services = "Services"
|
||||
|
||||
// Colors
|
||||
ColorActive = "ColorActive"
|
||||
ColorNormal = "ColorNormal"
|
||||
ColorUrgent = "ColorUrgent"
|
||||
ColorText = "ColorText"
|
||||
)
|
||||
|
||||
const (
|
||||
settingFmt = "settings.my_%s"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidType = errors.New("invalid type")
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
raw string
|
||||
valueType string
|
||||
}
|
||||
|
||||
func (s Setting) String() string {
|
||||
return s.raw
|
||||
}
|
||||
|
||||
func (s Setting) Uint() (uint8, error) {
|
||||
if s.valueType != "uint" {
|
||||
return 0, fmt.Errorf("%s: %w", s.valueType, ErrInvalidType)
|
||||
}
|
||||
val, err := strconv.ParseUint(s.raw, 10, 8)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf(
|
||||
"hc uint setting[%s] failed to parse: %s",
|
||||
s.raw,
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
return uint8(val), nil
|
||||
}
|
||||
|
||||
func GetFocusedTag() (uint8, error) {
|
||||
attr, err := hlcl.GetAttr("tags.focus.index")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
val, err := strconv.ParseUint(attr, 10, 8)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing [%s] to uint8: %w", attr, err)
|
||||
}
|
||||
return uint8(val), nil
|
||||
}
|
||||
|
||||
func Get(name string) (Setting, error) {
|
||||
attrPath := fmt.Sprintf(settingFmt, name)
|
||||
attr, err := hlcl.GetAttr(attrPath)
|
||||
if err != nil {
|
||||
return Setting{}, err
|
||||
}
|
||||
attrType, err := hlcl.AttrType(attrPath)
|
||||
if err != nil {
|
||||
// Default to string, silently
|
||||
attrType = "string"
|
||||
}
|
||||
return Setting{
|
||||
raw: attr,
|
||||
valueType: attrType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Set(name string, value string) error {
|
||||
return hlcl.SetAttr(fmt.Sprintf(settingFmt, name), value)
|
||||
}
|
||||
|
||||
func New(name string, value string, attrType cmd.AttributeType) error {
|
||||
if _, err := Get(name); err == nil {
|
||||
// Already exists, just set value
|
||||
return Set(name, value)
|
||||
}
|
||||
return hlcl.NewAttr(fmt.Sprintf(settingFmt, name), value, attrType)
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"sectorinf.com/emilis/hlctl/ctllog"
|
||||
"sectorinf.com/emilis/hlctl/hlcl/cmd"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgPath = filepath.Join(
|
||||
os.Getenv("HOME"),
|
||||
".config",
|
||||
"herbstluftwm",
|
||||
"hlctl.toml",
|
||||
)
|
||||
|
||||
log = ctllog.Logger{}.New("config", ctllog.ColorRGB{}.FromHex("ff5aff"))
|
||||
)
|
||||
|
||||
type (
|
||||
HlwmConfig struct {
|
||||
Font string
|
||||
FontBold string
|
||||
Mod string
|
||||
TermSpawnKey string
|
||||
Term string
|
||||
Groups GroupConfig
|
||||
Theme Theme
|
||||
Services ServicesConfig
|
||||
}
|
||||
|
||||
ServicesConfig struct {
|
||||
Services []string
|
||||
}
|
||||
|
||||
GroupConfig struct {
|
||||
Groups uint8
|
||||
Tags uint8
|
||||
}
|
||||
|
||||
Theme struct {
|
||||
Colors Colors
|
||||
}
|
||||
|
||||
Colors struct {
|
||||
Active string
|
||||
Normal string
|
||||
Urgent string
|
||||
Text string
|
||||
}
|
||||
)
|
||||
|
||||
func (h HlwmConfig) Save() error {
|
||||
cfg, err := os.Create(cfgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening cfg path: [%s]: %w", cfgPath, err)
|
||||
}
|
||||
defer cfg.Close()
|
||||
err = toml.NewEncoder(cfg).Encode(h)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding config to toml: %w", err)
|
||||
}
|
||||
return cfg.Sync()
|
||||
}
|
||||
|
||||
func (h HlwmConfig) Validate() error {
|
||||
if h.Font == "" ||
|
||||
h.FontBold == "" ||
|
||||
h.Mod == "" ||
|
||||
h.TermSpawnKey == "" ||
|
||||
h.Term == "" ||
|
||||
h.Groups.Groups == 0 ||
|
||||
h.Groups.Tags == 0 ||
|
||||
len(h.Theme.Colors.Active) < 6 ||
|
||||
len(h.Theme.Colors.Normal) < 6 ||
|
||||
len(h.Theme.Colors.Urgent) < 6 ||
|
||||
len(h.Theme.Colors.Text) < 6 {
|
||||
return errors.New("some configuration options are missing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect the config from loaded attributes. Or get default
|
||||
// if that fails.
|
||||
func Collect() HlwmConfig {
|
||||
getOrNothing := func(settingName string, value *string) {
|
||||
v, err := Get(settingName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
*value = v.String()
|
||||
}
|
||||
getUints := func(settingName string, value *uint8) {
|
||||
v, err := Get(settingName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
val, err := v.Uint()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
*value = uint8(val)
|
||||
}
|
||||
cfg := Default
|
||||
getOrNothing(Font, &cfg.Font)
|
||||
getOrNothing(FontBold, &cfg.FontBold)
|
||||
getOrNothing(ModKey, &cfg.Mod)
|
||||
getOrNothing(TermSpawnKey, &cfg.TermSpawnKey)
|
||||
getOrNothing(Term, &cfg.Term)
|
||||
getUints(GroupCount, &cfg.Groups.Groups)
|
||||
getUints(TagsPerGroup, &cfg.Groups.Tags)
|
||||
getOrNothing(ColorActive, &cfg.Theme.Colors.Active)
|
||||
getOrNothing(ColorNormal, &cfg.Theme.Colors.Normal)
|
||||
getOrNothing(ColorUrgent, &cfg.Theme.Colors.Urgent)
|
||||
getOrNothing(ColorText, &cfg.Theme.Colors.Text)
|
||||
svcsString, err := Get(Services)
|
||||
if err == nil {
|
||||
cfg.Services.Services = strings.Split(svcsString.String(), ";")
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (h HlwmConfig) SetAll() error {
|
||||
err := New(GroupCount, strconv.Itoa(int(h.Groups.Groups)), cmd.Uint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", GroupCount, err)
|
||||
}
|
||||
|
||||
err = New(TagsPerGroup, strconv.Itoa(int(h.Groups.Tags)), cmd.Uint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", TagsPerGroup, err)
|
||||
}
|
||||
|
||||
sets := map[string]string{
|
||||
Font: h.Font,
|
||||
FontBold: h.FontBold,
|
||||
ModKey: h.Mod,
|
||||
TermSpawnKey: h.TermSpawnKey,
|
||||
Term: h.Term,
|
||||
ColorActive: h.Theme.Colors.Active,
|
||||
ColorNormal: h.Theme.Colors.Normal,
|
||||
ColorUrgent: h.Theme.Colors.Urgent,
|
||||
ColorText: h.Theme.Colors.Text,
|
||||
Services: strings.Join(h.Services.Services, ";"),
|
||||
}
|
||||
for cfg, val := range sets {
|
||||
if err := New(cfg, val, cmd.String); err != nil {
|
||||
return fmt.Errorf("%s[%s]: %w", cfg, val, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Load() HlwmConfig {
|
||||
cfg := HlwmConfig{}
|
||||
if _, err := toml.DecodeFile(cfgPath, &cfg); err != nil {
|
||||
log.Warnf(
|
||||
"could not read file [%s] for config, using default",
|
||||
cfgPath,
|
||||
)
|
||||
return Default
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
var (
|
||||
Default = HlwmConfig{
|
||||
Font: "-*-fixed-medium-*-*-*-12-*-*-*-*-*-*-*",
|
||||
FontBold: "-*-fixed-bold-*-*-*-12-*-*-*-*-*-*-*",
|
||||
Mod: "Mod4",
|
||||
TermSpawnKey: "Home",
|
||||
Term: "alacritty",
|
||||
Groups: GroupConfig{
|
||||
Groups: 3,
|
||||
Tags: 5,
|
||||
},
|
||||
Theme: Theme{
|
||||
Colors: Colors{
|
||||
Active: "#800080",
|
||||
Normal: "#330033",
|
||||
Urgent: "#7811A1",
|
||||
Text: "#898989",
|
||||
},
|
||||
},
|
||||
Services: ServicesConfig{
|
||||
Services: []string{"fcitx5"},
|
||||
},
|
||||
}
|
||||
)
|
|
@ -0,0 +1,102 @@
|
|||
package ctllog
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// \x1b[38;2;255;100;0mTRUECOLOR\x1b[0m
|
||||
// ESC[ 38;2;⟨r⟩;⟨g⟩;⟨b⟩ m Select RGB foreground color
|
||||
|
||||
const (
|
||||
escape byte = 0x1B
|
||||
csi byte = '['
|
||||
separator byte = ';'
|
||||
csiEnd byte = 'm'
|
||||
fgColor byte = '3'
|
||||
bgColor byte = '4'
|
||||
trueColor byte = '8'
|
||||
rgb byte = '2'
|
||||
reset byte = '0'
|
||||
)
|
||||
|
||||
var (
|
||||
trueColorFMT = string([]byte{
|
||||
escape,
|
||||
csi,
|
||||
'%', 'c',
|
||||
trueColor,
|
||||
separator,
|
||||
rgb,
|
||||
separator,
|
||||
'%', 'd',
|
||||
separator,
|
||||
'%', 'd',
|
||||
separator,
|
||||
'%', 'd',
|
||||
csiEnd,
|
||||
'%', 's',
|
||||
escape,
|
||||
csi,
|
||||
reset,
|
||||
csiEnd,
|
||||
})
|
||||
)
|
||||
|
||||
type Variant byte
|
||||
|
||||
const (
|
||||
Foreground = Variant(fgColor)
|
||||
Background = Variant(bgColor)
|
||||
)
|
||||
|
||||
type ColorRGB struct {
|
||||
r uint8
|
||||
g uint8
|
||||
b uint8
|
||||
}
|
||||
|
||||
func (ColorRGB) FromHex(hexString string) ColorRGB {
|
||||
if len(hexString) < 6 {
|
||||
return White
|
||||
}
|
||||
if hexString[0] == '#' {
|
||||
hexString = hexString[1:]
|
||||
}
|
||||
if len(hexString) != 6 {
|
||||
return White
|
||||
}
|
||||
|
||||
res, err := hex.DecodeString(hexString)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return White
|
||||
}
|
||||
|
||||
return ColorRGB{
|
||||
r: res[0],
|
||||
g: res[1],
|
||||
b: res[2],
|
||||
}
|
||||
}
|
||||
|
||||
func getStr(str string, typeByte Variant, color ColorRGB) string {
|
||||
return fmt.Sprintf(
|
||||
trueColorFMT,
|
||||
typeByte,
|
||||
color.r,
|
||||
color.g,
|
||||
color.b,
|
||||
str,
|
||||
)
|
||||
}
|
||||
|
||||
var (
|
||||
Black ColorRGB
|
||||
White = ColorRGB{255, 255, 255}
|
||||
Green = ColorRGB{0, 255, 0}
|
||||
Red = ColorRGB{255, 0, 0}
|
||||
Blue = ColorRGB{0, 0, 255}
|
||||
Purple = ColorRGB{255, 0, 255}
|
||||
Yellow = ColorRGB{255, 255, 0}
|
||||
)
|
|
@ -0,0 +1,22 @@
|
|||
package ctllog
|
||||
|
||||
import (
|
||||
"testing"
|
||||
// "sectorinf.com/emilis/hlctl/ctllog"
|
||||
)
|
||||
|
||||
func TestColorFromHex(t *testing.T) {
|
||||
testCases := map[string]ColorRGB{
|
||||
"000000": {},
|
||||
"0c2238": {12, 34, 56},
|
||||
}
|
||||
|
||||
for hexColor, expected := range testCases {
|
||||
t.Run(hexColor, func(t *testing.T) {
|
||||
actual := ColorRGB{}.FromHex(hexColor)
|
||||
if expected != actual {
|
||||
t.Fatalf("expected %+v got %+v", expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package ctllog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
coloredName string
|
||||
}
|
||||
|
||||
func (Logger) New(name string, color ColorRGB) Logger {
|
||||
coloredName := getStr("[ "+name+" ]", Foreground, color)
|
||||
return Logger{
|
||||
coloredName: coloredName,
|
||||
}
|
||||
}
|
||||
|
||||
func (l Logger) print(str string) {
|
||||
lines := strings.Split(str, "\n")
|
||||
for i, line := range lines {
|
||||
lines[i] = fmt.Sprintf("%s: %s", l.coloredName, line)
|
||||
}
|
||||
str = strings.Join(lines, "\n")
|
||||
fmt.Fprintln(os.Stderr, str)
|
||||
}
|
||||
|
||||
func (l Logger) colored(color ColorRGB, args ...any) {
|
||||
l.print(getStr(fmt.Sprint(args...), Foreground, color))
|
||||
}
|
||||
|
||||
func (l Logger) coloredf(color ColorRGB, format string, args ...any) {
|
||||
l.print(getStr(fmt.Sprintf(format, args...), Foreground, color))
|
||||
}
|
||||
|
||||
func (l Logger) fatal(str string) {
|
||||
l.print(getStr(str, Background, Red))
|
||||
debug.PrintStack()
|
||||
os.Exit(16)
|
||||
}
|
||||
|
||||
func (l Logger) Print(args ...any) {
|
||||
l.print(fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func (l Logger) Printf(format string, args ...any) {
|
||||
l.print(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (l Logger) Warn(args ...any) {
|
||||
l.colored(Yellow, args...)
|
||||
}
|
||||
|
||||
func (l Logger) Warnf(format string, args ...any) {
|
||||
l.coloredf(Yellow, format, args...)
|
||||
}
|
||||
|
||||
func (l Logger) Error(args ...any) {
|
||||
l.colored(Red, args...)
|
||||
}
|
||||
|
||||
func (l Logger) Errorf(format string, args ...any) {
|
||||
l.coloredf(Red, format, args...)
|
||||
}
|
||||
|
||||
func (l Logger) Fatal(args ...any) {
|
||||
l.fatal(fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func (l Logger) Fatalf(format string, args ...any) {
|
||||
l.fatal(fmt.Sprintf(format, args...))
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
module sectorinf.com/emilis/hlctl
|
||||
|
||||
go 1.19
|
||||
|
||||
require github.com/BurntSushi/toml v1.2.1
|
|
@ -0,0 +1,2 @@
|
|||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
|
@ -0,0 +1,106 @@
|
|||
package groupctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"sectorinf.com/emilis/hlctl/config"
|
||||
"sectorinf.com/emilis/hlctl/ctllog"
|
||||
"sectorinf.com/emilis/hlctl/hlcl"
|
||||
"sectorinf.com/emilis/hlctl/hlcl/cmd"
|
||||
)
|
||||
|
||||
var log = ctllog.Logger{}.New("group", ctllog.ColorRGB{}.FromHex("27ee7e"))
|
||||
|
||||
func ResetActive() error {
|
||||
err := config.New(config.ActiveGroup, "1", cmd.Uint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("new active group: %w", err)
|
||||
}
|
||||
|
||||
return Update()
|
||||
}
|
||||
|
||||
// Update switches to the correct tag based on the active group
|
||||
func Update() error {
|
||||
activeCfg, err := config.Get(config.ActiveGroup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", config.ActiveGroup, err)
|
||||
}
|
||||
active, err := activeCfg.Uint()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", config.ActiveGroup, err)
|
||||
}
|
||||
focus, err := config.GetFocusedTag()
|
||||
if err != nil {
|
||||
return fmt.Errorf("focus: %w", err)
|
||||
}
|
||||
numCfg, err := config.Get(config.TagsPerGroup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get %s: %w", config.TagsPerGroup, err)
|
||||
}
|
||||
num, err := numCfg.Uint()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", config.TagsPerGroup, err)
|
||||
}
|
||||
relativeTag := focus % uint8(num)
|
||||
absTag := ((active - 1) * num) + relativeTag
|
||||
if focus != absTag {
|
||||
log.Printf("Tag %d -> %d", focus, absTag)
|
||||
}
|
||||
if err := hlcl.UseIndex(absTag); err != nil {
|
||||
return fmt.Errorf("UseIndex[%d]: %w", absTag, err)
|
||||
}
|
||||
|
||||
return SetKeybinds(active, num)
|
||||
}
|
||||
|
||||
func MoveTo(group uint8) error {
|
||||
numCfg, err := config.Get(config.TagsPerGroup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get %s: %w", config.TagsPerGroup, err)
|
||||
}
|
||||
num, err := numCfg.Uint()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", config.TagsPerGroup, err)
|
||||
}
|
||||
|
||||
focus, err := config.GetFocusedTag()
|
||||
if err != nil {
|
||||
return fmt.Errorf("focus: %w", err)
|
||||
}
|
||||
relative := focus % num
|
||||
newTag := ((group - 1) * num) + relative
|
||||
log.Printf("Moving window to tag -> %d", newTag)
|
||||
return hlcl.MoveIndex(newTag)
|
||||
}
|
||||
|
||||
func SetKeybinds(group uint8, num uint8) error {
|
||||
offset := int((group - 1) * num)
|
||||
mod, err := config.Get(config.ModKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", config.ModKey, err)
|
||||
}
|
||||
log.Printf("Setting %d keybinds for group %d", num, group)
|
||||
for i := 0; i < int(num); i++ {
|
||||
actualTag := strconv.Itoa(offset + i)
|
||||
tag := strconv.Itoa(i + 1)
|
||||
err = hlcl.Keybind(
|
||||
[]string{mod.String(), tag},
|
||||
cmd.UseIndex,
|
||||
actualTag,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Setting use_index for [%d]: %w", i+1, err)
|
||||
}
|
||||
err = hlcl.Keybind(
|
||||
[]string{mod.String(), "Shift", tag},
|
||||
cmd.MoveIndex,
|
||||
actualTag,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Setting move_index for [%d]: %w", i+1, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Command string
|
||||
|
||||
func (c Command) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
func (c Command) Pretty() string {
|
||||
parts := strings.Split(string(c), "_")
|
||||
for i, part := range parts {
|
||||
// Set the first character to be upper case
|
||||
if len(part) == 0 {
|
||||
continue
|
||||
} else if len(part) == 1 {
|
||||
parts[i] = strings.ToUpper(part)
|
||||
}
|
||||
var first rune
|
||||
for _, r := range part {
|
||||
first = r
|
||||
break
|
||||
}
|
||||
|
||||
parts[i] = fmt.Sprintf("%c%s", unicode.ToUpper(first), part[1:])
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
const (
|
||||
Reload Command = "reload"
|
||||
Close Command = "close"
|
||||
Spawn Command = "spawn"
|
||||
ListMonitors Command = "list_monitors"
|
||||
Lock Command = "lock"
|
||||
Unlock Command = "unlock"
|
||||
|
||||
// Attr family
|
||||
GetAttr Command = "get_attr"
|
||||
SetAttr Command = "set_attr"
|
||||
Attr Command = "attr"
|
||||
NewAttr Command = "new_attr"
|
||||
AttrType Command = "attr_type"
|
||||
RemoveAttr Command = "remove_attr"
|
||||
|
||||
Set Command = "set"
|
||||
EmitHook Command = "emit_hook"
|
||||
Rule Command = "rule"
|
||||
Unrule Command = "unrule"
|
||||
Substitute Command = "substitute"
|
||||
|
||||
// Controls
|
||||
Keybind Command = "keybind"
|
||||
Keyunbind Command = "keyunbind"
|
||||
Mousebind Command = "mousebind"
|
||||
Mouseunbind Command = "mouseunbind"
|
||||
UseIndex Command = "use_index"
|
||||
MoveIndex Command = "move_index"
|
||||
JumpTo Command = "jumpto"
|
||||
|
||||
// Views/Frames
|
||||
AddTag Command = "add"
|
||||
MergeTag Command = "merge_tag"
|
||||
Cycle Command = "cycle"
|
||||
Focus Command = "focus"
|
||||
Shift Command = "shift"
|
||||
Split Command = "split"
|
||||
Remove Command = "remove"
|
||||
Fullscreen Command = "fullscreen"
|
||||
CycleLayout Command = "cycle_layout"
|
||||
Resize Command = "resize"
|
||||
)
|
||||
|
||||
type MouseButton string
|
||||
|
||||
func (m MouseButton) String() string {
|
||||
return string(m)
|
||||
}
|
||||
|
||||
const (
|
||||
Mouse1 MouseButton = "Mouse1"
|
||||
Mouse2 MouseButton = "Mouse2"
|
||||
Mouse3 MouseButton = "Mouse3"
|
||||
)
|
||||
|
||||
type AttributeType string
|
||||
|
||||
func (a AttributeType) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
const (
|
||||
String AttributeType = "string"
|
||||
Bool AttributeType = "bool"
|
||||
Color AttributeType = "color"
|
||||
Int AttributeType = "int"
|
||||
Uint AttributeType = "uint"
|
||||
)
|
|
@ -0,0 +1,190 @@
|
|||
// package hlcl contains logic for interacting with the
|
||||
// herbstclient
|
||||
package hlcl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sectorinf.com/emilis/hlctl/ctllog"
|
||||
"sectorinf.com/emilis/hlctl/hlcl/cmd"
|
||||
)
|
||||
|
||||
var log = ctllog.Logger{}.New("hlcl", ctllog.Purple)
|
||||
|
||||
func runGeneric(command string, stdin string, args ...string) (string, error) {
|
||||
cmd := exec.Command(command, args...)
|
||||
// log.Printf("Running command [%s] with args: %v", command, args)
|
||||
var stdout, stderr = bytes.Buffer{}, bytes.Buffer{}
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Stdout = &stdout
|
||||
if stdin != "" {
|
||||
pipe, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("stdin pipe: %w", err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return "", fmt.Errorf("start: %w", err)
|
||||
}
|
||||
io.WriteString(pipe, stdin+"\n")
|
||||
_, err = pipe.Write([]byte(stdin))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("write stdin: %w", err)
|
||||
}
|
||||
pipe.Close()
|
||||
} else {
|
||||
if err := cmd.Start(); err != nil {
|
||||
return "", fmt.Errorf("start: %w", err)
|
||||
}
|
||||
}
|
||||
if err := cmd.Wait(); err != nil {
|
||||
status, ok := err.(*exec.ExitError)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%s exec error: %w", command, err)
|
||||
}
|
||||
exitCode := status.ExitCode()
|
||||
return "", fmt.Errorf(
|
||||
"%s error (%d): %s",
|
||||
command,
|
||||
exitCode,
|
||||
strings.TrimSpace(stderr.String()),
|
||||
)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
}
|
||||
|
||||
func run(command cmd.Command, args ...string) (string, error) {
|
||||
args = append([]string{command.String()}, args...)
|
||||
return runGeneric("herbstclient", "", args...)
|
||||
}
|
||||
|
||||
// query executes a command and pretty formats and error message
|
||||
// based on the command's Pretty(), and returns the output
|
||||
func query(c cmd.Command, args ...string) (string, error) {
|
||||
value, err := run(c, args...)
|
||||
if err != nil {
|
||||
return value, fmt.Errorf(
|
||||
"%s(%s): %w",
|
||||
c.Pretty(),
|
||||
strings.Join(args, ", "),
|
||||
err,
|
||||
)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// execute: wrapper for [query], discarding the stdout result
|
||||
func execute(c cmd.Command, args ...string) error {
|
||||
_, err := query(c, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func NewAttr(path string, value string, aType cmd.AttributeType) error {
|
||||
return execute(cmd.NewAttr, aType.String(), path, value)
|
||||
}
|
||||
|
||||
func SetAttr(path string, value string) error {
|
||||
return execute(cmd.SetAttr, path, value)
|
||||
}
|
||||
|
||||
func GetAttr(path string) (string, error) {
|
||||
return query(cmd.GetAttr, path)
|
||||
}
|
||||
|
||||
func AttrType(path string) (string, error) {
|
||||
return query(cmd.AttrType, path)
|
||||
}
|
||||
|
||||
func KeyUnbind() error {
|
||||
return execute(cmd.Keyunbind, "--all")
|
||||
}
|
||||
|
||||
func MouseUnbind() error {
|
||||
return execute(cmd.Mouseunbind, "--all")
|
||||
}
|
||||
|
||||
func ListMonitors() ([]Screen, error) {
|
||||
out, err := query(cmd.ListMonitors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outs := strings.Split(out, "\n")
|
||||
scrs := make([]Screen, len(outs))
|
||||
for i, line := range outs {
|
||||
scrs[i] = Screen{}.FromString(line)
|
||||
}
|
||||
return scrs, nil
|
||||
}
|
||||
|
||||
func bindCMD(
|
||||
bindCommand cmd.Command,
|
||||
keys []string,
|
||||
command cmd.Command,
|
||||
args ...string,
|
||||
) error {
|
||||
bindKeys := strings.Join(keys, "-")
|
||||
args = append([]string{bindKeys, command.String()}, args...)
|
||||
return execute(bindCommand, args...)
|
||||
}
|
||||
|
||||
func Keybind(keys []string, command cmd.Command, args ...string) error {
|
||||
return bindCMD(cmd.Keybind, keys, command, args...)
|
||||
}
|
||||
|
||||
func Mousebind(keys []string, command cmd.Command, args ...string) error {
|
||||
return bindCMD(cmd.Mousebind, keys, command, args...)
|
||||
}
|
||||
|
||||
func Set(name, value string) error {
|
||||
return execute(cmd.Set, name, value)
|
||||
}
|
||||
|
||||
func UseIndex(index uint8) error {
|
||||
return execute(cmd.UseIndex, strconv.Itoa(int(index)))
|
||||
}
|
||||
|
||||
func MergeTag(name string) error {
|
||||
return execute(cmd.MergeTag, name)
|
||||
}
|
||||
|
||||
func MoveIndex(index uint8) error {
|
||||
return execute(cmd.MoveIndex, strconv.Itoa(int(index)))
|
||||
}
|
||||
|
||||
func TextWidth(font, message string) (string, error) {
|
||||
return runGeneric("textwidth", "", font, message)
|
||||
}
|
||||
|
||||
func Rule(name string, args ...string) error {
|
||||
return execute(cmd.Rule, append([]string{name}, args...)...)
|
||||
}
|
||||
|
||||
func Unrule() error {
|
||||
return execute(cmd.Unrule, "-F")
|
||||
}
|
||||
|
||||
func SilentSpawn(command string, args ...string) error {
|
||||
return execute(cmd.Spawn, append([]string{command}, args...)...)
|
||||
}
|
||||
|
||||
func Lock() error {
|
||||
return execute(cmd.Lock)
|
||||
}
|
||||
|
||||
func Unlock() error {
|
||||
return execute(cmd.Unlock)
|
||||
}
|
||||
|
||||
func AddTag(name string) error {
|
||||
return execute(cmd.AddTag, name)
|
||||
}
|
||||
|
||||
func Dzen2(message string, args ...string) error {
|
||||
_, err := runGeneric("dzen2", message, args...)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package hlcl
|
||||
|
||||
import "strings"
|
||||
|
||||
type Screen struct {
|
||||
ID string
|
||||
Resolution string
|
||||
WithTag string
|
||||
Focused bool
|
||||
}
|
||||
|
||||
func (Screen) FromString(v string) Screen {
|
||||
parts := strings.Split(v, " ")
|
||||
if len(parts) < 5 {
|
||||
return Screen{
|
||||
ID: v[:1],
|
||||
}
|
||||
}
|
||||
for i, part := range parts {
|
||||
parts[i] = strings.TrimSpace(
|
||||
strings.TrimRight(
|
||||
strings.Trim(part, "\""), ":"))
|
||||
}
|
||||
return Screen{
|
||||
ID: parts[0],
|
||||
Resolution: parts[1],
|
||||
// Maybe later
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"sectorinf.com/emilis/hlctl/cmdlets"
|
||||
"sectorinf.com/emilis/hlctl/cmdlets/group"
|
||||
initcmd "sectorinf.com/emilis/hlctl/cmdlets/init"
|
||||
"sectorinf.com/emilis/hlctl/cmdlets/notify"
|
||||
"sectorinf.com/emilis/hlctl/cmdlets/save"
|
||||
"sectorinf.com/emilis/hlctl/ctllog"
|
||||
)
|
||||
|
||||
var log = ctllog.Logger{}.New("hlctl", ctllog.Green)
|
||||
|
||||
var cmds = []cmdlets.Commandlet{
|
||||
initcmd.Command,
|
||||
group.Command,
|
||||
notify.Command,
|
||||
save.Command,
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
func getCmdlet(name string) (cmdlets.Commandlet, error) {
|
||||
for _, c := range cmds {
|
||||
if c.Name() == name {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("%s: %w", name, ErrNotFound)
|
||||
}
|
||||
|
||||
func help() {
|
||||
commands := make([]string, len(cmds))
|
||||
for i, c := range cmds {
|
||||
commands[i] = c.Name()
|
||||
}
|
||||
fmt.Fprint(os.Stderr, "USAGE: hlctl [command] [arguments]\n")
|
||||
fmt.Fprintf(os.Stderr, "\nAvailable commands: %v\n", commands)
|
||||
fmt.Fprint(os.Stderr, "\nSee hlctl help [command] for details\n")
|
||||
}
|
||||
|
||||
func main() {
|
||||
// omit the program name
|
||||
args := os.Args[1:]
|
||||
if len(args) == 0 {
|
||||
help()
|
||||
os.Exit(1)
|
||||
}
|
||||
command := strings.ToLower(args[0])
|
||||
if command == "help" {
|
||||
if len(args) == 1 {
|
||||
help()
|
||||
return
|
||||
}
|
||||
name := args[1]
|
||||
c, err := getCmdlet(name)
|
||||
if err != nil {
|
||||
help()
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(c.Usage())
|
||||
return
|
||||
}
|
||||
c, err := getCmdlet(command)
|
||||
if err != nil {
|
||||
help()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := c.Invoke(args[1:]...); err != nil {
|
||||
log.Fatalf(" %s > %s", c.Name(), err.Error())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package notifyctl
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"sectorinf.com/emilis/hlctl/ctllog"
|
||||
"sectorinf.com/emilis/hlctl/hlcl"
|
||||
)
|
||||
|
||||
var log = ctllog.Logger{}.New("notify", ctllog.ColorRGB{}.FromHex("ff003e"))
|
||||
|
||||
func Display(message string, duration uint, fg, bg, font string) error {
|
||||
width, err := hlcl.TextWidth(font, message)
|
||||
if err != nil {
|
||||
log.Errorf("%s; defaulting to width 100", err.Error())
|
||||
width = "100"
|
||||
}
|
||||
widthNum, err := strconv.ParseUint(width, 10, 8)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return hlcl.Dzen2(
|
||||
message,
|
||||
"-p", strconv.Itoa(int(duration)),
|
||||
"-fg", fg,
|
||||
"-bg", bg,
|
||||
"-fn", font,
|
||||
"-ta", "c",
|
||||
"-w", strconv.Itoa(int(widthNum)+30),
|
||||
"-h", "16",
|
||||
)
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package panelctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"sectorinf.com/emilis/hlctl/hlcl"
|
||||
)
|
||||
|
||||
func Run() error {
|
||||
screens, err := hlcl.ListMonitors()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
panel := func(mon string) error {
|
||||
return exec.Command(
|
||||
"bash",
|
||||
fmt.Sprintf(
|
||||
"%s/.config/herbstluftwm/panel.sh",
|
||||
os.Getenv("HOME"),
|
||||
),
|
||||
mon,
|
||||
).Run()
|
||||
}
|
||||
for i := 1; i < len(screens); i++ {
|
||||
id := screens[i].ID
|
||||
go func() {
|
||||
if err := panel(id); err != nil {
|
||||
log.Fatalf("Monitor [%s] panel: %s", id, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return panel(screens[0].ID)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package svcctl
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"sectorinf.com/emilis/hlctl/config"
|
||||
"sectorinf.com/emilis/hlctl/ctllog"
|
||||
"sectorinf.com/emilis/hlctl/hlcl"
|
||||
)
|
||||
|
||||
var log = ctllog.Logger{}.New("config", ctllog.ColorRGB{}.FromHex("2e8647"))
|
||||
|
||||
func RestartServices(cfg config.ServicesConfig) {
|
||||
for _, svc := range cfg.Services {
|
||||
log.Printf("Restarting [%s]", svc)
|
||||
if err := exec.Command("pkill", "-9", svc).Run(); err != nil {
|
||||
log.Errorf("Killing [%s]: %s", svc, err)
|
||||
}
|
||||
if err := hlcl.SilentSpawn(svc); err != nil {
|
||||
log.Errorf("Starting [%s]: %s", svc, err)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue