Change project layout to make easier debug / tests"
This commit is contained in:
172
internal/lib/batteries.go
Normal file
172
internal/lib/batteries.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/distatus/battery"
|
||||
)
|
||||
|
||||
// UpdateBatteryInfo updates info about battery charge.
|
||||
func (c MyConfig) UpdateBatteryInfo() {
|
||||
var (
|
||||
ch int
|
||||
status string
|
||||
Batts string
|
||||
)
|
||||
|
||||
for {
|
||||
batteries, err := battery.GetAll()
|
||||
|
||||
if err != nil {
|
||||
Batts = fmt.Sprintf(
|
||||
"<span font='%s' size='%s'>%s</span><span font='%s' size='%s'> no batt</span>",
|
||||
Conf.Battery.SymbolFont,
|
||||
Conf.Battery.SymbolFontSize,
|
||||
Conf.Battery.Symbol,
|
||||
Conf.Battery.Font,
|
||||
Conf.Battery.FontSize,
|
||||
)
|
||||
} else {
|
||||
var (
|
||||
battsInfo string
|
||||
chStr string
|
||||
)
|
||||
|
||||
for i, b := range batteries {
|
||||
switch b.State.Raw {
|
||||
case battery.Charging:
|
||||
status = `▲`
|
||||
case battery.Discharging:
|
||||
status = `▼`
|
||||
case battery.Empty:
|
||||
status = `✘`
|
||||
default:
|
||||
status = `•`
|
||||
}
|
||||
|
||||
// N.B. there can be case when battery is overcharged and shows >100%. It also can indicate that
|
||||
// calibration data is out of date and battery should be re-calibrated.
|
||||
ch = int(math.Round((b.Full - b.Current) * (100 / b.Full)))
|
||||
|
||||
switch {
|
||||
case ch > 85:
|
||||
if Conf.Battery.ChargeColor.Full == "" {
|
||||
chStr = fmt.Sprintf(
|
||||
"<span color='%s' background='%s' font='%s' size='%s'>% 3d%%</span>",
|
||||
Conf.Battery.Color,
|
||||
Conf.Battery.Background,
|
||||
Conf.Battery.Font,
|
||||
Conf.Battery.FontSize,
|
||||
ch,
|
||||
)
|
||||
} else {
|
||||
chStr = fmt.Sprintf(
|
||||
`<span color='%s' background='%s' font='%s' size='%s'>% 3d%%</span>`,
|
||||
Conf.Battery.ChargeColor.Full,
|
||||
Conf.Battery.Background,
|
||||
Conf.Battery.Font,
|
||||
Conf.Battery.FontSize,
|
||||
ch,
|
||||
)
|
||||
}
|
||||
|
||||
case ch < 85 && ch > 40:
|
||||
if Conf.Battery.ChargeColor.AlmostFull == "" {
|
||||
chStr = fmt.Sprintf(
|
||||
"<span color='%s' background='%s' font='%s' size='%s'>% 3d%%</span>",
|
||||
Conf.Battery.Color,
|
||||
Conf.Battery.Background,
|
||||
Conf.Battery.Font,
|
||||
Conf.Battery.FontSize,
|
||||
ch,
|
||||
)
|
||||
} else {
|
||||
chStr = fmt.Sprintf(
|
||||
`<span color='%s' background='%s' font='%s' size='%s'>% 3d%%</span>`,
|
||||
Conf.Battery.ChargeColor.AlmostFull,
|
||||
Conf.Battery.Background,
|
||||
Conf.Battery.Font,
|
||||
Conf.Battery.FontSize,
|
||||
ch,
|
||||
)
|
||||
}
|
||||
|
||||
case ch <= 40 && ch >= 10:
|
||||
if Conf.Battery.ChargeColor.AlmostEmpty == "" {
|
||||
chStr = fmt.Sprintf(
|
||||
"<span color='%s' background='%s' font='%s' size='%s'>% 3d%%</span>",
|
||||
Conf.Battery.Color,
|
||||
Conf.Battery.Background,
|
||||
Conf.Battery.Font,
|
||||
Conf.Battery.FontSize,
|
||||
ch,
|
||||
)
|
||||
} else {
|
||||
chStr = fmt.Sprintf(
|
||||
`<span color='%s' background='%s' font='%s' size='%s'>% 3d%%</span>`,
|
||||
Conf.Battery.ChargeColor.AlmostEmpty,
|
||||
Conf.Battery.Background,
|
||||
Conf.Battery.Font,
|
||||
Conf.Battery.FontSize,
|
||||
ch,
|
||||
)
|
||||
}
|
||||
|
||||
case ch < 10:
|
||||
if Conf.Battery.ChargeColor.Empty == "" {
|
||||
chStr = fmt.Sprintf(
|
||||
"<span color='%s' background='%s' font='%s' size='%s'>% 3d%%</span>",
|
||||
Conf.Battery.Color,
|
||||
Conf.Battery.Background,
|
||||
Conf.Battery.Font,
|
||||
Conf.Battery.FontSize,
|
||||
ch,
|
||||
)
|
||||
} else {
|
||||
chStr = fmt.Sprintf(
|
||||
`<span color='%s' background='%s' font='%s' size='%s'>% 3d%%</span>`,
|
||||
Conf.Battery.ChargeColor.Empty,
|
||||
Conf.Battery.Background,
|
||||
Conf.Battery.Font,
|
||||
Conf.Battery.FontSize,
|
||||
ch,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
battsInfo += fmt.Sprintf(
|
||||
"<span color='%s' background='%s' font='%s' size='%s'>%s</span><span color='%s' background='%s' font='%s' size='%s'>B%d </span>%s<span color='%s' background='%s' font='%s' size='%s'> %s</span>",
|
||||
Conf.Battery.Color,
|
||||
Conf.Battery.Background,
|
||||
Conf.Battery.SymbolFont,
|
||||
Conf.Battery.SymbolFontSize,
|
||||
Conf.Battery.Symbol,
|
||||
Conf.Battery.Color,
|
||||
Conf.Battery.Background,
|
||||
Conf.Battery.Font,
|
||||
Conf.Battery.FontSize,
|
||||
i,
|
||||
chStr,
|
||||
Conf.Battery.Color,
|
||||
Conf.Battery.Background,
|
||||
Conf.Battery.Font,
|
||||
Conf.Battery.FontSize,
|
||||
status,
|
||||
)
|
||||
}
|
||||
|
||||
if battsInfo != "" {
|
||||
Batts = battsInfo
|
||||
}
|
||||
}
|
||||
|
||||
if c.BatteryString == Batts {
|
||||
c.BatteryString = Batts
|
||||
c.UpdateReady <- true
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
27
internal/lib/clock.go
Normal file
27
internal/lib/clock.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UpdateClock get and updates (on i3bar) info about system clock.
|
||||
func (c MyConfig) UpdateClock() {
|
||||
for {
|
||||
currentTime := time.Now()
|
||||
hours, minutes, _ := currentTime.Clock()
|
||||
year, month, day := currentTime.Date()
|
||||
dow := currentTime.Weekday()
|
||||
rmonth := [12]string{"Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"}
|
||||
rdow := [7]string{"Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"}
|
||||
|
||||
myclock := fmt.Sprintf(" %s, %d %s %d % 2d:%02d ", rdow[dow], day, rmonth[month-1], year, hours, minutes)
|
||||
|
||||
if myclock != c.ClockTime {
|
||||
c.ClockTime = myclock
|
||||
c.UpdateReady <- true
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
69
internal/lib/collection.go
Normal file
69
internal/lib/collection.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Collection is sync.Map, that stores arbitrary data.
|
||||
type Collection struct {
|
||||
items sync.Map
|
||||
close chan struct{}
|
||||
}
|
||||
|
||||
// Item just interface for stoing any data.
|
||||
type item struct {
|
||||
data interface{}
|
||||
}
|
||||
|
||||
// NewCollection creates new instance of "collection".
|
||||
func NewCollection() *Collection {
|
||||
c := &Collection{ //nolint:exhaustruct
|
||||
close: make(chan struct{}),
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Get returns value by given key. Or empty value.
|
||||
func (collection *Collection) Get(key interface{}) (interface{}, bool) {
|
||||
obj, exists := collection.items.Load(key)
|
||||
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
item := obj.(item)
|
||||
|
||||
return item.data, true
|
||||
}
|
||||
|
||||
// Set saves value to given "collection".
|
||||
func (collection *Collection) Set(key interface{}, value interface{}) {
|
||||
collection.items.Store(key, item{
|
||||
data: value,
|
||||
})
|
||||
}
|
||||
|
||||
// Range apply function f to all keys/values in collection. Retuns immediatly on first error.
|
||||
func (collection *Collection) Range(f func(key, value interface{}) bool) {
|
||||
fn := func(key, value interface{}) bool {
|
||||
item := value.(item)
|
||||
|
||||
return f(key, item.data)
|
||||
}
|
||||
|
||||
collection.items.Range(fn)
|
||||
}
|
||||
|
||||
// Delete deletes key and corresponding value from collection.
|
||||
func (collection *Collection) Delete(key interface{}) {
|
||||
collection.items.Delete(key)
|
||||
}
|
||||
|
||||
// Close frees memory occupied by collection and deletes collection itself.
|
||||
func (collection *Collection) Close() {
|
||||
collection.close <- struct{}{}
|
||||
collection.items = sync.Map{}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
1570
internal/lib/config.go
Normal file
1570
internal/lib/config.go
Normal file
File diff suppressed because it is too large
Load Diff
77
internal/lib/cputemp.go
Normal file
77
internal/lib/cputemp.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UpdateCPUTemperature gets and updates average CPU cores temperature.
|
||||
func (c MyConfig) UpdateCPUTemperature() {
|
||||
n := len(Conf.CPUTemp.File)
|
||||
|
||||
for {
|
||||
var (
|
||||
temperature = make([]int64, n)
|
||||
tSum int64
|
||||
tAvg int64
|
||||
)
|
||||
|
||||
for i, filename := range Conf.CPUTemp.File {
|
||||
file, err := os.Open(filename)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to open %s: %s", filename, err)
|
||||
} else {
|
||||
reader := bufio.NewReader(file)
|
||||
line, _, err := reader.ReadLine()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to read from %s: %s", filename, err)
|
||||
err = file.Close()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to close %s: %s", filename, err)
|
||||
}
|
||||
} else {
|
||||
err = file.Close()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to close %s: %s", filename, err)
|
||||
} else {
|
||||
temp, err := strconv.ParseInt(string(line), 10, 32)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to convert string to number from file %s: %s", filename, err)
|
||||
} else {
|
||||
if temp > 1000 {
|
||||
temp /= 1000
|
||||
}
|
||||
|
||||
temperature[i] = temp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(temperature) == 1 {
|
||||
tAvg = temperature[0]
|
||||
} else {
|
||||
for _, t := range temperature {
|
||||
tSum += t
|
||||
}
|
||||
|
||||
tAvg = tSum / int64(len(temperature))
|
||||
}
|
||||
|
||||
if c.CPUTemperature != tAvg {
|
||||
c.CPUTemperature = tAvg
|
||||
c.UpdateReady <- true
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}
|
||||
45
internal/lib/cron.go
Normal file
45
internal/lib/cron.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
)
|
||||
|
||||
// myCron contains cron handler.
|
||||
var myCron gocron.Scheduler
|
||||
|
||||
// RunCron parses config part, related to cron tasks and kicks cron to run.
|
||||
func RunCron() {
|
||||
var err error
|
||||
|
||||
myCron, err = gocron.NewScheduler()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to create built-in cron: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for _, job := range Conf.Cron.Tasks {
|
||||
_, err := myCron.NewJob(
|
||||
gocron.CronJob(job.Time, false),
|
||||
gocron.NewTask(
|
||||
// Сука, просто заспавнить бинарь, ну почему это надо делать через жопу-то?
|
||||
any(
|
||||
func() bool {
|
||||
RunChan <- job.Cmd
|
||||
|
||||
return true
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to add cron job: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
myCron.Start()
|
||||
}
|
||||
227
internal/lib/i3.go
Normal file
227
internal/lib/i3.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"log"
|
||||
"reflect"
|
||||
"regexp"
|
||||
|
||||
"go.i3wm.org/i3"
|
||||
)
|
||||
|
||||
type WinList struct {
|
||||
Instance Collection
|
||||
Class Collection
|
||||
}
|
||||
|
||||
// WL contains collections of all windows currently running, collection of their classes and collection of their
|
||||
// instances.
|
||||
var WL WinList
|
||||
|
||||
// UpdateI3WinList subsribes to all window-related events and send them to I3EventParser() for more detailed parsing.
|
||||
func (c MyConfig) UpdateI3WinList() {
|
||||
// TODO: Здесь получить начальный список окон, управляемых i3wm (через GetTree()) и сложить их атрибутику в переменную WL
|
||||
Tree, err := i3.GetTree()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to get list of windows: %s", err)
|
||||
}
|
||||
|
||||
c.ExtractProps(Tree.Root)
|
||||
|
||||
i3wm := i3.Subscribe(i3.WindowEventType)
|
||||
|
||||
for i3wm.Next() {
|
||||
e := i3wm.Event().(*i3.WindowEvent)
|
||||
go c.I3EventParser(e)
|
||||
}
|
||||
|
||||
log.Fatal(i3wm.Close())
|
||||
}
|
||||
|
||||
// I3EventParser parses i3wm events and updates WL (aka Windows List).
|
||||
func (c MyConfig) I3EventParser(e *i3.WindowEvent) {
|
||||
switch e.Change {
|
||||
case "new":
|
||||
var (
|
||||
count uint64
|
||||
)
|
||||
|
||||
countI, exist := WL.Class.Get(e.Container.WindowProperties.Class)
|
||||
|
||||
if exist {
|
||||
count = interfaceToUint64(countI)
|
||||
} else {
|
||||
count = 0
|
||||
}
|
||||
|
||||
count++
|
||||
WL.Class.Set(e.Container.WindowProperties.Class, count)
|
||||
|
||||
countI, exist = WL.Instance.Get(e.Container.WindowProperties.Instance)
|
||||
|
||||
if exist {
|
||||
count = interfaceToUint64(countI)
|
||||
} else {
|
||||
count = 0
|
||||
}
|
||||
|
||||
count++
|
||||
WL.Instance.Set(e.Container.WindowProperties.Instance, count)
|
||||
|
||||
c.UpdateReady <- true
|
||||
|
||||
case "close":
|
||||
countI, exist := WL.Class.Get(e.Container.WindowProperties.Class)
|
||||
|
||||
if exist {
|
||||
count := interfaceToUint64(countI)
|
||||
|
||||
if count > 1 {
|
||||
count--
|
||||
|
||||
WL.Class.Set(e.Container.WindowProperties.Class, count)
|
||||
} else {
|
||||
WL.Class.Delete(e.Container.WindowProperties.Class)
|
||||
}
|
||||
}
|
||||
|
||||
countI, exist = WL.Instance.Get(e.Container.WindowProperties.Instance)
|
||||
|
||||
if exist {
|
||||
count := interfaceToUint64(countI)
|
||||
|
||||
if count > 1 {
|
||||
count--
|
||||
|
||||
WL.Instance.Set(e.Container.WindowProperties.Instance, count)
|
||||
} else {
|
||||
WL.Instance.Delete(e.Container.WindowProperties.Instance)
|
||||
}
|
||||
}
|
||||
|
||||
c.UpdateReady <- true
|
||||
}
|
||||
}
|
||||
|
||||
// interfaceToUint превращает данный интерфейс в Uint64.
|
||||
// Если может, конечно :) .
|
||||
func interfaceToUint64(iface interface{}) uint64 {
|
||||
// А теперь мы начинаем дурдом, нам надо превратить ёбанный interface{} в []string
|
||||
// Поскольку interface{} может быть чем угодно, перестрахуемся
|
||||
if reflect.TypeOf(iface).Kind() == reflect.Uint64 {
|
||||
return reflect.ValueOf(iface).Uint()
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// HasWindows returns true if given Window Class and/or Instance has more than 0 windows according our observations.
|
||||
func HasWindows(className string, instanceName string) bool {
|
||||
var (
|
||||
ret bool
|
||||
myclass bool
|
||||
myinstance bool
|
||||
)
|
||||
|
||||
switch {
|
||||
case className != "" && instanceName != "":
|
||||
// Since we agree that we use regexp for searching window class and instance, but not yet decide what to do
|
||||
// with exact match, comment it out and leave it here.
|
||||
/*
|
||||
_, myclass = WL.Class.Get(className)
|
||||
_, myinstance = WL.Instance.Get(instanceName)
|
||||
*/
|
||||
myclass = FindWindowClass(className)
|
||||
myinstance = FindWindowInstance(instanceName)
|
||||
ret = myclass && myinstance
|
||||
|
||||
case className != "":
|
||||
// _, ret = WL.Class.Get(className)
|
||||
ret = FindWindowClass(className)
|
||||
|
||||
case instanceName != "":
|
||||
// _, ret = WL.Instance.Get(instanceName)
|
||||
ret = FindWindowInstance(instanceName)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// FindWindowInstance returns true if given regex matces at least one pattern in WL.Class collection.
|
||||
// Otherwise, returns false.
|
||||
func FindWindowInstance(regexpStr string) bool {
|
||||
var found = false
|
||||
|
||||
f := func(key interface{}, value interface{}) bool {
|
||||
keyStr := reflect.ValueOf(key).String()
|
||||
|
||||
re, err := regexp.Compile(regexpStr) //nolint:nolintlint, gocritic, we do not want shit our pants and panic on
|
||||
// erroneous regexp, we want log it to stderr, that it.
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to perform regexp match: %s", err)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if re.Match([]byte(keyStr)) { //nolint:mirror
|
||||
found = true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
WL.Instance.Range(f)
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
// FindWindowClass returns true if given regex matces at least one pattern in WL.Class collection.
|
||||
// Otherwise, returns false.
|
||||
func FindWindowClass(regexpStr string) bool {
|
||||
var found = false
|
||||
|
||||
f := func(key interface{}, value interface{}) bool {
|
||||
keyStr := reflect.ValueOf(key).String()
|
||||
|
||||
re, err := regexp.Compile(regexpStr) //nolint:nolintlint, gocritic, we do not want shit our pants and panic on
|
||||
// erroneous regexp, we want log it to stderr, that it.
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to perform regexp match: %s", err)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if re.Match([]byte(keyStr)) { //nolint:mirror
|
||||
found = true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
WL.Class.Range(f)
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
// ExtractProps fills WL stricture collections with instance name and class name of given node (x11, that is).
|
||||
func (c MyConfig) ExtractProps(n *i3.Node) {
|
||||
var e i3.WindowEvent
|
||||
e.Change = "new"
|
||||
e.Container = *n
|
||||
|
||||
c.I3EventParser(&e)
|
||||
|
||||
for _, node := range n.Nodes {
|
||||
c.ExtractProps(node)
|
||||
}
|
||||
|
||||
for _, node := range n.FloatingNodes {
|
||||
c.ExtractProps(node)
|
||||
}
|
||||
}
|
||||
62
internal/lib/ifmon.go
Normal file
62
internal/lib/ifmon.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// IfStatus global var conaining cumulative status of all monitored net intefaces.
|
||||
var IfStatus string
|
||||
|
||||
// UpdateIfStatus updates network interfaces status for i3bar.
|
||||
func (c MyConfig) UpdateIfStatus() {
|
||||
for {
|
||||
var statusSum string
|
||||
|
||||
for _, item := range Conf.NetIf.If {
|
||||
var (
|
||||
name = item.Name
|
||||
)
|
||||
|
||||
if name == "" {
|
||||
name = filepath.Base(item.Dir)
|
||||
}
|
||||
|
||||
operstate, err := os.ReadFile(item.Dir + "/operstate")
|
||||
|
||||
if err != nil {
|
||||
operstate = []byte("?")
|
||||
|
||||
log.Printf("Unable to get net if status from file %s: %s", item.Dir+"/operstate", err)
|
||||
} else {
|
||||
switch strings.TrimSpace(string(operstate)) {
|
||||
case "up":
|
||||
StatusStr := fmt.Sprintf("<span foreground=\"%s\">⍋</span>", Conf.NetIf.UpColor)
|
||||
operstate = []byte(StatusStr)
|
||||
case "down":
|
||||
statusStr := fmt.Sprintf("<span foreground=\"%s\">⍒</span>", Conf.NetIf.DownColor)
|
||||
operstate = []byte(statusStr)
|
||||
default:
|
||||
operstate = []byte(`?`)
|
||||
}
|
||||
}
|
||||
|
||||
if statusSum != "" {
|
||||
statusSum += " "
|
||||
}
|
||||
|
||||
statusSum += fmt.Sprintf("%s:%s", name, operstate)
|
||||
}
|
||||
|
||||
if c.IfStatus != statusSum {
|
||||
c.IfStatus = statusSum
|
||||
c.UpdateReady <- true
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}
|
||||
24
internal/lib/la.go
Normal file
24
internal/lib/la.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/load"
|
||||
)
|
||||
|
||||
// UpdateLaStats вытаскивает показание LA за последнюю минуту.
|
||||
func (c MyConfig) UpdateLaStats() {
|
||||
for {
|
||||
l, _ := load.Avg()
|
||||
|
||||
lav := fmt.Sprintf("%.2f", l.Load1)
|
||||
|
||||
if c.La != lav {
|
||||
c.La = lav
|
||||
c.UpdateReady <- true
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}
|
||||
59
internal/lib/memory.go
Normal file
59
internal/lib/memory.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
)
|
||||
|
||||
// Mem struct with mem stats.
|
||||
type Mem struct {
|
||||
Usedpct uint64
|
||||
Shared uint64
|
||||
Swap uint64
|
||||
}
|
||||
|
||||
// Memory stores string with mem stats for i3bar.
|
||||
var Memory Mem
|
||||
|
||||
// UpdateMemStats parses mem info stats.
|
||||
func (c MyConfig) UpdateMemStats() {
|
||||
for {
|
||||
v, err := mem.VirtualMemory()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to get memory statistics: %s", err)
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
sw, err := mem.SwapMemory()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to get swap statistics: %s", err)
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if Conf.Mem.ShowSwap {
|
||||
if Memory.Usedpct != uint64(v.UsedPercent) || Memory.Shared != v.Shared/1024/1024 ||
|
||||
Memory.Swap != v.SwapTotal-v.SwapFree {
|
||||
Memory.Usedpct = uint64(v.UsedPercent)
|
||||
Memory.Shared = v.Shared / 1024 / 1024
|
||||
Memory.Swap = sw.Used
|
||||
c.UpdateReady <- true
|
||||
}
|
||||
} else {
|
||||
if Memory.Usedpct != uint64(v.UsedPercent) || Memory.Shared != v.Shared/1024/1024 {
|
||||
Memory.Usedpct = uint64(v.UsedPercent)
|
||||
Memory.Shared = v.Shared / 1024 / 1024
|
||||
c.UpdateReady <- true
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}
|
||||
227
internal/lib/pulseaudio.go
Normal file
227
internal/lib/pulseaudio.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
p "github.com/mafik/pulseaudio"
|
||||
)
|
||||
|
||||
var SoundVolume = "🔊:0%"
|
||||
var pa *p.Client
|
||||
|
||||
var SVPAHandlerChan = make(chan ClickEvent, 256)
|
||||
|
||||
// TODO: https://twin.sh/articles/44/add-a-timeout-to-any-function-in-go timeout pulseaudio calls
|
||||
|
||||
// UpdateVolumeInfo updates info about current Sound Volume.
|
||||
func (c MyConfig) UpdateVolumeInfo() {
|
||||
var err error
|
||||
|
||||
pa, err = p.NewClient()
|
||||
|
||||
// It can happen if no pulseaudio server running for current user.
|
||||
// If no server running we have to run one.
|
||||
if err != nil {
|
||||
if err := PaReinit(); err != nil {
|
||||
log.Print(err)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
vol, err := pa.Volume()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable get volume from pulseaudio server: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
SoundVolume = fmt.Sprintf(
|
||||
"<span color='%s' background='%s' font='%s' size='%s'>%s</span>",
|
||||
c.SimpleVolumePa.Color,
|
||||
c.SimpleVolumePa.Background,
|
||||
c.SimpleVolumePa.SymbolFont,
|
||||
c.SimpleVolumePa.SymbolFontSize,
|
||||
c.SimpleVolumePa.Symbol,
|
||||
)
|
||||
|
||||
SoundVolume += fmt.Sprintf(
|
||||
"<span color='%s' background='%s' font='%s' size='%s'>:%d%%</span>",
|
||||
c.SimpleVolumePa.Color,
|
||||
c.SimpleVolumePa.Background,
|
||||
c.SimpleVolumePa.Font,
|
||||
c.SimpleVolumePa.FontSize,
|
||||
int64(vol*100),
|
||||
)
|
||||
|
||||
c.UpdateReady <- true
|
||||
|
||||
for {
|
||||
// Subscribe to update notification channel, to get info that volume changed.
|
||||
pulseUpdate, err := pa.Updates()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to subscribe to pulseaudio updates: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Rake update events.
|
||||
for range pulseUpdate {
|
||||
vol, err = pa.Volume()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable get volume from pulseaudio server: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
SoundVolume = fmt.Sprintf(
|
||||
"<span color='%s' background='%s' font='%s' size='%s'>%s</span>",
|
||||
c.SimpleVolumePa.Color,
|
||||
c.SimpleVolumePa.Background,
|
||||
c.SimpleVolumePa.SymbolFont,
|
||||
c.SimpleVolumePa.SymbolFontSize,
|
||||
c.SimpleVolumePa.Symbol,
|
||||
)
|
||||
|
||||
SoundVolume += fmt.Sprintf(
|
||||
"<span color='%s' background='%s' font='%s' size='%s'>:%d%%</span>",
|
||||
c.SimpleVolumePa.Color,
|
||||
c.SimpleVolumePa.Background,
|
||||
c.SimpleVolumePa.Font,
|
||||
c.SimpleVolumePa.FontSize,
|
||||
int64(vol*100),
|
||||
)
|
||||
|
||||
c.UpdateReady <- true
|
||||
}
|
||||
|
||||
if err := PaReinit(); err != nil {
|
||||
log.Print(err)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// This code is unreachable :(
|
||||
pa.Close() //nolint:govet
|
||||
}
|
||||
|
||||
// PaReinit re-inits pulseaudio and connection to it.
|
||||
func PaReinit() error {
|
||||
var err error
|
||||
|
||||
// If we out of updates, seems someone killed pulseaudio server. Restart it.
|
||||
cmd := exec.Command("pulseaudio", "--check")
|
||||
|
||||
if err := cmd.Run(); err == nil {
|
||||
log.Printf("pulseaudio already running, but seems not responding, try gracefully kill it")
|
||||
|
||||
cmd = exec.Command("pulseaudio", "--kill")
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("unable to kill pulseaudio: %w", err)
|
||||
}
|
||||
|
||||
cnt := 0
|
||||
|
||||
for {
|
||||
cmd := exec.Command("pulseaudio", "--check")
|
||||
|
||||
// PA not running. Breakout of loop.
|
||||
if err := cmd.Run(); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if cnt >= 5 {
|
||||
return errors.New("timeout waiting pulseaudio to exit") //nolint: goerr113
|
||||
}
|
||||
|
||||
log.Print("Waiting for pulseaudio to exit")
|
||||
|
||||
cnt++
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// PA has annoying feature: socket activation. Sound system must be persistent and available to all users regardless
|
||||
// of init or other things. They just fail to understand that. Or they fucked up pulseaudio just because they scary
|
||||
// of security something responsibility. Anyway this is grand design flaw, but we still have to bear with it.
|
||||
// This setting defines if pulseaudio will be run in manner that allows it to exit is other login detected (how
|
||||
// they guess it - I do not know). In general case we do not want to allow pa to exit and leave our session without
|
||||
// audio.
|
||||
if Conf.SimpleVolumePa.DontExitOnLogin {
|
||||
// Do not exit on any kind of login/logout events.
|
||||
cmd = exec.Command("pulseaudio", "--exit-idle-time=-1", "--start")
|
||||
} else {
|
||||
// Exit immediately on logout(?).
|
||||
cmd = exec.Command("pulseaudio", "--exit-idle-time=0", "--start")
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("unable to initialize pulseaudio server instance: %w", err)
|
||||
}
|
||||
|
||||
pa, err = p.NewClient()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to make client connection to pulseaudio: %w", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c MyConfig) SVPAHandler() {
|
||||
for e := range SVPAHandlerChan {
|
||||
if e.Button == 3 {
|
||||
RunChan <- Conf.SimpleVolumePa.RightClickCmd
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
vol, err := pa.Volume()
|
||||
|
||||
if err != nil {
|
||||
if err := PaReinit(); err != nil {
|
||||
log.Printf("Unable to get pulseaudio volume: %s", err)
|
||||
} else {
|
||||
vol, err = pa.Volume()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to get volume pulseaudio server behaves weirdly: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch e.Button {
|
||||
case Conf.SimpleVolumePa.WheelUp:
|
||||
vol += float32(Conf.SimpleVolumePa.Step) / 100
|
||||
|
||||
if vol > (float32(Conf.SimpleVolumePa.MaxVolumeLimit) / 100) {
|
||||
vol = float32(Conf.SimpleVolumePa.MaxVolumeLimit) / 100
|
||||
}
|
||||
|
||||
if err := pa.SetVolume(vol); err != nil {
|
||||
log.Printf("Unable to set pulseaudio volume: %s", err)
|
||||
}
|
||||
|
||||
case Conf.SimpleVolumePa.WheelDown:
|
||||
vol -= float32(Conf.SimpleVolumePa.Step) / 100
|
||||
|
||||
if vol < 0 {
|
||||
vol = 0
|
||||
}
|
||||
|
||||
if err := pa.SetVolume(vol); err != nil {
|
||||
log.Printf("Unable to set pulseaudio volume: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
internal/lib/signal.go
Normal file
35
internal/lib/signal.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var PrintOutput = true
|
||||
|
||||
// SigHandler OS signal handler.
|
||||
func (c MyConfig) SigHandler() {
|
||||
for s := range c.SigChan {
|
||||
switch s {
|
||||
case syscall.SIGUSR1:
|
||||
log.Print("Got SIGUSR1, stopping output")
|
||||
|
||||
PrintOutput = false
|
||||
|
||||
case syscall.SIGUSR2:
|
||||
log.Print("Got SIGUSR2, resuming output")
|
||||
|
||||
PrintOutput = true
|
||||
|
||||
c.UpdateReady <- true
|
||||
|
||||
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
|
||||
os.Exit(0)
|
||||
|
||||
// We have signal that we're not interested in, so make a new loop iteration.
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
59
internal/lib/spawn.go
Normal file
59
internal/lib/spawn.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RunChan to this channel we send command and its argv to execute as separate process.
|
||||
var RunChan = make(chan []string, 128)
|
||||
|
||||
// Spawner forks and execs given program, and also detaches form its control tty.
|
||||
func (c MyConfig) Spawner() {
|
||||
for prg := range RunChan {
|
||||
devnullR, _ := os.Open(os.DevNull)
|
||||
devnullW, _ := os.OpenFile(os.DevNull, os.O_WRONLY|os.O_APPEND, 0644)
|
||||
|
||||
spa := syscall.SysProcAttr{
|
||||
Setsid: true,
|
||||
Foreground: false,
|
||||
}
|
||||
|
||||
pa := syscall.ProcAttr{
|
||||
Dir: os.Getenv(`HOME`),
|
||||
Env: os.Environ(),
|
||||
Files: []uintptr{devnullR.Fd(), devnullW.Fd(), devnullW.Fd()},
|
||||
Sys: &spa,
|
||||
}
|
||||
|
||||
bin, err := exec.LookPath(prg[0])
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to spawn %s: %s", prg[0], err)
|
||||
}
|
||||
|
||||
_, err = syscall.ForkExec(bin, prg, &pa)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to spawn %s: %s", prg[0], err)
|
||||
}
|
||||
|
||||
// Close file errors we don't handle :) at all.
|
||||
_ = devnullR.Close()
|
||||
_ = devnullW.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// CleanZombies reaps processes spawned by Spawner() and already exited.
|
||||
func (c MyConfig) CleanZombies() {
|
||||
r := syscall.Rusage{}
|
||||
|
||||
for {
|
||||
_, _ = syscall.Wait4(-1, nil, 0, &r)
|
||||
|
||||
time.Sleep(1 * time.Minute)
|
||||
}
|
||||
}
|
||||
133
internal/lib/stdin.go
Normal file
133
internal/lib/stdin.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ClickEvent struct as described at https://i3wm.org/docs/i3bar-protocol.html
|
||||
type ClickEvent struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Instance string `json:"instance,omitempty"`
|
||||
Button int `json:"button"`
|
||||
Modifiers []string `json:"modifiers"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
RelativeX int `json:"relative_x"`
|
||||
RelativeY int `json:"relative_y"`
|
||||
OutputX int `json:"output_x"`
|
||||
OutputY int `json:"output_y"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
// ParseStdin tries to parse text that i3bar prints to our stdin. Currently - it is mouse click events on different
|
||||
// area names of i3bar.
|
||||
func (c MyConfig) ParseStdin() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
// De-facto it is jsonl, except first line is garbage. Also, first symbol in each strint garbage too.
|
||||
firstelem := true
|
||||
|
||||
for {
|
||||
var e ClickEvent
|
||||
|
||||
buf, err := reader.ReadString('\n')
|
||||
|
||||
if err != nil {
|
||||
if !firstelem {
|
||||
log.Printf("Unable to read from stdin: %s", err)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
log.Print("String from stdin has zero length, skipping")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i < len(buf); i++ {
|
||||
if strings.HasPrefix(buf, "{") {
|
||||
break
|
||||
}
|
||||
|
||||
buf = buf[1:]
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
log.Print("String from stdin has zero length, skipping")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(buf), &e)
|
||||
|
||||
if err != nil {
|
||||
if firstelem {
|
||||
firstelem = false
|
||||
} else {
|
||||
log.Printf("Unable to decode json line from stdin: %s, buf: %s", err, buf)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Just in case.
|
||||
if firstelem {
|
||||
firstelem = false
|
||||
}
|
||||
|
||||
// Clock clicks parse.
|
||||
if e.Name == "wallclock" {
|
||||
if Conf.Clock.LeftClick.Enabled && e.Button == 1 {
|
||||
RunChan <- Conf.Clock.LeftClick.Cmd
|
||||
} else if Conf.Clock.RightClick.Enabled && e.Button == 3 {
|
||||
RunChan <- Conf.Clock.RightClick.Cmd
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if e.Name == "simple-volume-pa" {
|
||||
if Conf.SimpleVolumePa.Enabled {
|
||||
SVPAHandlerChan <- e
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if !Conf.AppButtons.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, app := range Conf.Apps {
|
||||
switch {
|
||||
case app.Name != "" && app.Instance != "":
|
||||
if app.Name == e.Name && app.Instance == e.Instance {
|
||||
prg := append([]string{}, app.Cmd)
|
||||
prg = append(prg, app.Args...)
|
||||
|
||||
RunChan <- prg
|
||||
|
||||
break
|
||||
}
|
||||
case app.Name != "" && e.Name == app.Name:
|
||||
prg := append([]string{}, app.Cmd)
|
||||
prg = append(prg, app.Args...)
|
||||
|
||||
RunChan <- prg
|
||||
|
||||
case app.Instance != "" && e.Instance == app.Instance:
|
||||
prg := append([]string{}, app.Cmd)
|
||||
prg = append(prg, app.Args...)
|
||||
|
||||
RunChan <- prg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
internal/lib/vpn.go
Normal file
94
internal/lib/vpn.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UpdateVPNStatus periodically update status of openvpn daemon.
|
||||
func (c MyConfig) UpdateVPNStatus() {
|
||||
var (
|
||||
vpnCStatus string
|
||||
tcpCheck string
|
||||
vpnCheck string
|
||||
)
|
||||
|
||||
for {
|
||||
if c.VPNFileCheck() {
|
||||
if Conf.Vpn.UpColor == "" {
|
||||
vpnCheck = `⍋`
|
||||
} else {
|
||||
vpnCheck = fmt.Sprintf(`<span foreground="%s">⍋</span>`, c.Vpn.UpColor)
|
||||
}
|
||||
} else {
|
||||
if Conf.Vpn.DownColor == "" {
|
||||
vpnCheck = `⍒`
|
||||
} else {
|
||||
vpnCheck = fmt.Sprintf(`<span foreground="%s">⍒</span>`, c.Vpn.DownColor)
|
||||
}
|
||||
}
|
||||
|
||||
if Conf.Vpn.TCPCheck.Enabled {
|
||||
if c.VPNTCPCheck() {
|
||||
if Conf.Vpn.UpColor == "" {
|
||||
tcpCheck = `✔`
|
||||
} else {
|
||||
tcpCheck = fmt.Sprintf(`<span foreground="%s">✔</span>`, c.Vpn.UpColor)
|
||||
}
|
||||
} else {
|
||||
if Conf.Vpn.DownColor == "" {
|
||||
tcpCheck = `✘`
|
||||
} else {
|
||||
tcpCheck = fmt.Sprintf(`<span foreground="%s">✘</span>`, c.Vpn.DownColor)
|
||||
}
|
||||
}
|
||||
|
||||
vpnCStatus = fmt.Sprintf("VPN:%s:%s", vpnCheck, tcpCheck)
|
||||
} else {
|
||||
vpnCStatus = fmt.Sprintf("VPN:%s", vpnCheck)
|
||||
}
|
||||
|
||||
if c.VPNStatus != vpnCStatus {
|
||||
c.VPNStatus = vpnCStatus
|
||||
c.UpdateReady <- true
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// VPNTCPCheck intended to check arbitrary service inside vpn segment, to indicate that openvpn sevice not stoned.
|
||||
func (c MyConfig) VPNTCPCheck() bool {
|
||||
conn, err := net.DialTimeout(
|
||||
"tcp",
|
||||
fmt.Sprintf("%s:%d", c.Vpn.TCPCheck.Host, c.Vpn.TCPCheck.Port),
|
||||
time.Second*time.Duration(c.Vpn.TCPCheck.Timeout),
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
_ = conn.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// VPNFileCheck checks modification time of openvpn-status file.
|
||||
func (c MyConfig) VPNFileCheck() bool {
|
||||
var (
|
||||
fi os.FileInfo
|
||||
err error
|
||||
)
|
||||
|
||||
if fi, err = os.Stat(c.Vpn.StatusFile); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if time.Since(fi.ModTime()).Seconds() > float64(c.Vpn.MtimeThreshold) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user