1
0
mirror of https://github.com/dcarrillo/dotfiles.git synced 2025-10-01 04:59:09 +00:00

[polybar] Add polytasks module

This commit is contained in:
2025-09-27 12:51:08 +02:00
parent 022647ddf4
commit 9fd1c3757a
6 changed files with 332 additions and 6 deletions

View File

@@ -60,7 +60,7 @@ font-4 = "NotoSans-Regular:size=28:weight=bold:antialias=true;-11
font-5 = "FontAwesome:size=21:antialias=true;4" font-5 = "FontAwesome:size=21:antialias=true;4"
font-6 = "Font Awesome 6 Brands-Regular-400:size=32:antialias=true;1" font-6 = "Font Awesome 6 Brands-Regular-400:size=32:antialias=true;1"
modules-left = polywins modules-left = polytasks
modules-center = custom_date modules-center = custom_date
modules-right = updates cpu_bar memory_bar docker vpn network_status network_usage syncthing_status pulseaudio tray modules-right = updates cpu_bar memory_bar docker vpn network_status network_usage syncthing_status pulseaudio tray

View File

@@ -1,10 +1,12 @@
#!/usr/bin/env bash #!/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 TERMINAL_CMD=${TERMINAL_CMD:-"kitty --class=info --override='foreground=#c69026' "}
export BROWSER_CMD=${BROWSER_CMD:-"firefox"} 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} export ROFI_THEME=${ROFI_THEME:-orange}
function wait_for_polybar function wait_for_polybar
@@ -25,12 +27,21 @@ function kill_polybar
wait_for_polybar stopped 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 function launch_polybar
{ {
for monitor in $(polybar --list-monitors | cut -d":" -f1); do for monitor in $(polybar --list-monitors | cut -d":" -f1); do
export MONITOR=$monitor export MONITOR=$monitor
polybar top -c ~/.config/polybar/bar.ini >/dev/null & polybar top -c $POLYBAR_PATH/bar.ini >/dev/null &
done done
compile_src
wait_for_polybar started wait_for_polybar started
} }

View File

@@ -147,9 +147,9 @@ tail = true
interval = 5 interval = 5
click-left = $TERMINAL_CMD yay -Suy & click-left = $TERMINAL_CMD yay -Suy &
[module/polywins] [module/polytasks]
type = custom/script type = custom/script
exec = ~/.config/polybar/scripts/polywins 2>/dev/null exec = ~/.config/polybar/scripts/polytasks
format = <label> format = <label>
label = "%output%" label = "%output%"
label-padding = 0 label-padding = 0

View File

@@ -0,0 +1,5 @@
module main
go 1.25.1
require github.com/godbus/dbus/v5 v5.1.0

View 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=

View 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
}
}
}
}