mirror of
https://github.com/dcarrillo/dotfiles.git
synced 2025-10-01 03:49:10 +00:00
[polybar] Add polytasks module
This commit is contained in:
@@ -60,7 +60,7 @@ font-4 = "NotoSans-Regular:size=28:weight=bold:antialias=true;-11
|
||||
font-5 = "FontAwesome:size=21:antialias=true;4"
|
||||
font-6 = "Font Awesome 6 Brands-Regular-400:size=32:antialias=true;1"
|
||||
|
||||
modules-left = polywins
|
||||
modules-left = polytasks
|
||||
modules-center = custom_date
|
||||
modules-right = updates cpu_bar memory_bar docker vpn network_status network_usage syncthing_status pulseaudio tray
|
||||
|
||||
|
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
[ -f ~/.config/polybar/bar.env ] && . ~/.config/polybar/bar.env
|
||||
POLYBAR_PATH=~/.config/polybar
|
||||
|
||||
[ -f $POLYBAR_PATH/bar.env ] && . $POLYBAR_PATH/bar.env
|
||||
|
||||
export TERMINAL_CMD=${TERMINAL_CMD:-"kitty --class=info --override='foreground=#c69026' "}
|
||||
export BROWSER_CMD=${BROWSER_CMD:-"firefox"}
|
||||
export WM_CONTROL=${WM_CONTROL:-"~/.config/polybar/scripts/switch_window_state"}
|
||||
export WM_CONTROL=${WM_CONTROL:-"$POLYBAR_PATH/scripts/switch_window_state"}
|
||||
export ROFI_THEME=${ROFI_THEME:-orange}
|
||||
|
||||
function wait_for_polybar
|
||||
@@ -25,12 +27,21 @@ function kill_polybar
|
||||
wait_for_polybar stopped
|
||||
}
|
||||
|
||||
function compile_src
|
||||
{
|
||||
pushd $POLYBAR_PATH/scripts/src/polytasks || return
|
||||
echo "Compiling polytasks..."
|
||||
go build -buildvcs=false -ldflags="-s -w" -o $POLYBAR_PATH/scripts/polytasks .
|
||||
popd || return
|
||||
}
|
||||
|
||||
function launch_polybar
|
||||
{
|
||||
for monitor in $(polybar --list-monitors | cut -d":" -f1); do
|
||||
export MONITOR=$monitor
|
||||
polybar top -c ~/.config/polybar/bar.ini >/dev/null &
|
||||
polybar top -c $POLYBAR_PATH/bar.ini >/dev/null &
|
||||
done
|
||||
compile_src
|
||||
wait_for_polybar started
|
||||
}
|
||||
|
||||
|
@@ -147,9 +147,9 @@ tail = true
|
||||
interval = 5
|
||||
click-left = $TERMINAL_CMD yay -Suy &
|
||||
|
||||
[module/polywins]
|
||||
[module/polytasks]
|
||||
type = custom/script
|
||||
exec = ~/.config/polybar/scripts/polywins 2>/dev/null
|
||||
exec = ~/.config/polybar/scripts/polytasks
|
||||
format = <label>
|
||||
label = "%output%"
|
||||
label-padding = 0
|
||||
|
5
.config/polybar/scripts/src/polytasks/go.mod
Normal file
5
.config/polybar/scripts/src/polytasks/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module main
|
||||
|
||||
go 1.25.1
|
||||
|
||||
require github.com/godbus/dbus/v5 v5.1.0
|
2
.config/polybar/scripts/src/polytasks/go.sum
Normal file
2
.config/polybar/scripts/src/polytasks/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
308
.config/polybar/scripts/src/polytasks/main.go
Normal file
308
.config/polybar/scripts/src/polytasks/main.go
Normal file
@@ -0,0 +1,308 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
var iconMap = map[string]string{
|
||||
"brave-browser": "",
|
||||
"chromium": "",
|
||||
"code": "",
|
||||
"firefox": "",
|
||||
"glrnvim": "",
|
||||
"keepassxc": "",
|
||||
"info": "",
|
||||
"kitty": "",
|
||||
"nuclear": "",
|
||||
"nvim": "",
|
||||
"org.gnome.calculator": "",
|
||||
"org.gnome.calendar": "",
|
||||
"org.gnome.console": "",
|
||||
"org.gnome.eog": "",
|
||||
"org.gnome.evince": "",
|
||||
"org.gnome.fileroller": "",
|
||||
"org.gnome.nautilus": "",
|
||||
"org.gnome.settings": "",
|
||||
"org.telegram.desktop": "",
|
||||
"seahorse": "",
|
||||
"slack": "",
|
||||
"spotube": "",
|
||||
"steam": "",
|
||||
"thunderbird": "",
|
||||
"vpn": "",
|
||||
"tor-browser": "",
|
||||
"vivaldi-stable": "",
|
||||
}
|
||||
|
||||
var blacklistedClasses = map[string]bool{
|
||||
"": true,
|
||||
"polybar": true,
|
||||
}
|
||||
|
||||
var whatIsMyPath string
|
||||
|
||||
const (
|
||||
activeTextColor = "#F5A70A"
|
||||
activeLeft = "%{F" + activeTextColor + "}"
|
||||
activeRight = "%{F-}"
|
||||
)
|
||||
|
||||
type WindowInfo struct {
|
||||
ID int `json:"id"`
|
||||
Class string `json:"class"`
|
||||
}
|
||||
|
||||
func evalJS(conn *dbus.Conn, js string) (bool, string, error) {
|
||||
obj := conn.Object("org.gnome.Shell", "/org/gnome/Shell")
|
||||
|
||||
call := obj.Call("org.gnome.Shell.Eval", 0, js)
|
||||
if call.Err != nil {
|
||||
return false, "", call.Err
|
||||
}
|
||||
|
||||
var success bool
|
||||
var value string
|
||||
if err := call.Store(&success, &value); err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
return success, value, nil
|
||||
}
|
||||
|
||||
func checkUnsafeMode(conn *dbus.Conn) (bool, error) {
|
||||
js := `global.context ? global.context.unsafe_mode : false`
|
||||
|
||||
success, value, err := evalJS(conn, js)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check unsafe mode: %w", err)
|
||||
}
|
||||
|
||||
if success {
|
||||
var unsafeMode bool
|
||||
if err := json.Unmarshal([]byte(value), &unsafeMode); err != nil {
|
||||
return false, fmt.Errorf("failed to parse unsafe mode value: %w", err)
|
||||
}
|
||||
return unsafeMode, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("failed to execute unsafe mode check")
|
||||
}
|
||||
|
||||
func getWindowList(conn *dbus.Conn) ([]WindowInfo, error) {
|
||||
js := `
|
||||
global
|
||||
.get_window_actors()
|
||||
.map(a => a.meta_window)
|
||||
.map(w => ({id: w.get_id(), class: w.get_wm_class()}))
|
||||
`
|
||||
|
||||
success, value, err := evalJS(conn, js)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get window list: %w", err)
|
||||
}
|
||||
|
||||
var allWindows []WindowInfo
|
||||
if success {
|
||||
if err := json.Unmarshal([]byte(value), &allWindows); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse window list: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var result []WindowInfo
|
||||
for _, win := range allWindows {
|
||||
if win.Class != "" && !blacklistedClasses[strings.ToLower(win.Class)] {
|
||||
result = append(result, win)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].Class < result[j].Class
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getActiveWindow(conn *dbus.Conn) (int, error) {
|
||||
js := `global.get_window_actors().find(a => a.meta_window.has_focus())?.meta_window.get_id()`
|
||||
|
||||
success, value, err := evalJS(conn, js)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get active window: %w", err)
|
||||
}
|
||||
|
||||
if success {
|
||||
var id int
|
||||
if err := json.Unmarshal([]byte(value), &id); err != nil {
|
||||
return 0, fmt.Errorf("failed to parse active window ID: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func closeWindow(conn *dbus.Conn, windowID int) error {
|
||||
js := fmt.Sprintf(`
|
||||
const window = global.get_window_actors()
|
||||
.map(a => a.meta_window)
|
||||
.find(w => w.get_id() === %d);
|
||||
if (window) {
|
||||
window.delete(global.get_current_time());
|
||||
}
|
||||
`, windowID)
|
||||
|
||||
success, _, err := evalJS(conn, js)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute close window script: %w", err)
|
||||
}
|
||||
|
||||
if !success {
|
||||
return fmt.Errorf("failed to close window with ID %d", windowID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func focusOrMinimize(conn *dbus.Conn, windowID int) error {
|
||||
js := fmt.Sprintf(`
|
||||
const window = global.get_window_actors()
|
||||
.map(a => a.meta_window)
|
||||
.find(w => w.get_id() === %d);
|
||||
|
||||
if (window) {
|
||||
if (window.has_focus()) {
|
||||
window.minimize();
|
||||
} else {
|
||||
window.activate(global.get_current_time());
|
||||
}
|
||||
}
|
||||
`, windowID)
|
||||
|
||||
success, _, err := evalJS(conn, js)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute raise or minimize window script: %w", err)
|
||||
}
|
||||
|
||||
if !success {
|
||||
return fmt.Errorf("failed to raise or minimize window with ID %d", windowID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func polyPrintWindowList(conn *dbus.Conn) error {
|
||||
windows, err := getWindowList(conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get window list: %w", err)
|
||||
}
|
||||
|
||||
if len(windows) > 0 {
|
||||
fmt.Print("%{T4}Tasks: %{T-}")
|
||||
}
|
||||
for _, win := range windows {
|
||||
activeID, err := getActiveWindow(conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get active window: %w", err)
|
||||
}
|
||||
|
||||
icon := getIcon(win.Class)
|
||||
if win.ID == activeID {
|
||||
icon = fmt.Sprintf("%s%s%s", activeLeft, icon, activeRight)
|
||||
}
|
||||
fmt.Printf(" %%{A1:%s focus_or_minimize %d:}", whatIsMyPath, win.ID)
|
||||
fmt.Printf("%%{A2:%s close %d:}", whatIsMyPath, win.ID)
|
||||
fmt.Printf("%%{T6}%s%%{T-}%%{A}%%{A}", icon)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleCLICommands(conn *dbus.Conn, args []string) error {
|
||||
if len(args) != 3 {
|
||||
return fmt.Errorf("invalid number of arguments, expected: <command> <window_id>")
|
||||
}
|
||||
|
||||
command := args[1]
|
||||
windowIDStr := args[2]
|
||||
|
||||
windowID, err := strconv.Atoi(windowIDStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid window ID '%s': %w", windowIDStr, err)
|
||||
}
|
||||
|
||||
switch command {
|
||||
case "close":
|
||||
return closeWindow(conn, windowID)
|
||||
case "focus_or_minimize":
|
||||
return focusOrMinimize(conn, windowID)
|
||||
default:
|
||||
return fmt.Errorf("unknown command '%s', supported commands: close, focus", command)
|
||||
}
|
||||
}
|
||||
|
||||
func getIcon(class string) string {
|
||||
if icon, exists := iconMap[strings.ReplaceAll(strings.ToLower(class), " ", "-")]; exists {
|
||||
return icon
|
||||
}
|
||||
return strings.ToUpper(class[:1])
|
||||
}
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
whatIsMyPath, err = os.Executable()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get executable path: %v", err)
|
||||
}
|
||||
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
if err := handleCLICommands(conn, os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
var lastWindowCount int
|
||||
var lastActiveWindow int
|
||||
for range ticker.C {
|
||||
if safe, err := checkUnsafeMode(conn); err != nil || !safe {
|
||||
fmt.Println("GNOME Shell unsafe mode is not enabled.")
|
||||
continue
|
||||
}
|
||||
|
||||
windows, err := getWindowList(conn)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
activeID, err := getActiveWindow(conn)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(windows) != lastWindowCount || activeID != lastActiveWindow {
|
||||
if err := polyPrintWindowList(conn); err == nil {
|
||||
lastWindowCount = len(windows)
|
||||
lastActiveWindow = activeID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user