Change project layout to make easier debug / tests"

This commit is contained in:
eleksir
2024-09-22 23:52:57 +03:00
parent 9d3bc186cc
commit 4bff21f4c7
112 changed files with 271 additions and 1683 deletions

172
internal/lib/batteries.go Normal file
View 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
View 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)
}
}

View 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

File diff suppressed because it is too large Load Diff

77
internal/lib/cputemp.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}