taffybar: add AIUsage stack widget that switches on scratchpad state
Reads $XDG_STATE_HOME/hypr/ai-scratchpad (written by the Super+Alt+C toggle) to show either the OpenAI or Anthropic usage section. Uses a Gtk.Stack for instant switching and fsnotify to watch the state file for live updates without polling. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
116
dotfiles/config/taffybar/TaffybarConfig/AIUsage.hs
Normal file
116
dotfiles/config/taffybar/TaffybarConfig/AIUsage.hs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
-- | A usage widget that follows the Hyprland AI scratchpad selection.
|
||||||
|
--
|
||||||
|
-- The Hyprland config (scratchpads.lua) lets SUPER+ALT+C toggle whichever AI
|
||||||
|
-- app is currently selected via rofi_ai_scratchpad.sh, which records the
|
||||||
|
-- choice in $XDG_STATE_HOME/hypr/ai-scratchpad. This widget reads the same
|
||||||
|
-- state file and shows the matching provider's usage section (OpenAI for
|
||||||
|
-- "codex", Anthropic for "claude"), switching live when the file changes.
|
||||||
|
module TaffybarConfig.AIUsage
|
||||||
|
( aiUsageWidget,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Exception (IOException, try)
|
||||||
|
import Control.Monad (void)
|
||||||
|
import Control.Monad.IO.Class (liftIO)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified GI.Gtk as Gtk
|
||||||
|
import qualified System.FSNotify as FSNotify
|
||||||
|
import System.Directory (createDirectoryIfMissing, getHomeDirectory)
|
||||||
|
import System.Environment (lookupEnv)
|
||||||
|
import System.FilePath (takeFileName, (</>))
|
||||||
|
import System.Taffybar.Context (TaffyIO)
|
||||||
|
import System.Taffybar.Util (postGUIASync)
|
||||||
|
import System.Taffybar.Widget.AnthropicUsage
|
||||||
|
( AnthropicUsageDisplayMode (AnthropicUsageDisplayRemaining),
|
||||||
|
AnthropicUsageStackConfig (..),
|
||||||
|
anthropicUsageSectionNewWith,
|
||||||
|
defaultAnthropicUsageStackConfig,
|
||||||
|
)
|
||||||
|
import System.Taffybar.Widget.OpenAIUsage
|
||||||
|
( OpenAIUsageDisplayMode (OpenAIUsageDisplayRemaining),
|
||||||
|
OpenAIUsageStackConfig (..),
|
||||||
|
defaultOpenAIUsageStackConfig,
|
||||||
|
openAIUsageSectionNewWith,
|
||||||
|
)
|
||||||
|
import TaffybarConfig.WidgetUtil (decorateWithClassAndBox, usageLogoWidget)
|
||||||
|
|
||||||
|
codexChild, claudeChild :: Text
|
||||||
|
codexChild = "codex"
|
||||||
|
claudeChild = "claude"
|
||||||
|
|
||||||
|
aiScratchpadStateDir :: IO FilePath
|
||||||
|
aiScratchpadStateDir = do
|
||||||
|
stateHome <- lookupEnv "XDG_STATE_HOME"
|
||||||
|
base <- case stateHome of
|
||||||
|
Just dir | not (null dir) -> pure dir
|
||||||
|
_ -> (</> ".local/state") <$> getHomeDirectory
|
||||||
|
pure (base </> "hypr")
|
||||||
|
|
||||||
|
aiScratchpadStateFile :: FilePath
|
||||||
|
aiScratchpadStateFile = "ai-scratchpad"
|
||||||
|
|
||||||
|
-- | Read the currently selected AI scratchpad, defaulting to codex like the
|
||||||
|
-- Hyprland side does.
|
||||||
|
readActiveAIScratchpad :: IO Text
|
||||||
|
readActiveAIScratchpad = do
|
||||||
|
dir <- aiScratchpadStateDir
|
||||||
|
result <- try (readFile (dir </> aiScratchpadStateFile)) :: IO (Either IOException String)
|
||||||
|
pure $ case result of
|
||||||
|
Right contents
|
||||||
|
| T.strip (T.pack contents) == claudeChild -> claudeChild
|
||||||
|
_ -> codexChild
|
||||||
|
|
||||||
|
openAIUsageSection :: TaffyIO Gtk.Widget
|
||||||
|
openAIUsageSection = do
|
||||||
|
iconWidget <- liftIO $ usageLogoWidget "openai-symbol.svg" "OpenAI usage"
|
||||||
|
openAIUsageSectionNewWith
|
||||||
|
iconWidget
|
||||||
|
defaultOpenAIUsageStackConfig
|
||||||
|
{ openAIUsageStackDefaultDisplayMode = OpenAIUsageDisplayRemaining
|
||||||
|
}
|
||||||
|
|
||||||
|
anthropicUsageSection :: TaffyIO Gtk.Widget
|
||||||
|
anthropicUsageSection = do
|
||||||
|
iconWidget <- liftIO $ usageLogoWidget "claude-symbol.svg" "Claude usage"
|
||||||
|
anthropicUsageSectionNewWith
|
||||||
|
iconWidget
|
||||||
|
defaultAnthropicUsageStackConfig
|
||||||
|
{ anthropicUsageStackDefaultDisplayMode = AnthropicUsageDisplayRemaining
|
||||||
|
}
|
||||||
|
|
||||||
|
-- | Show usage for whichever AI app the Hyprland AI scratchpad currently
|
||||||
|
-- targets, switching live when the selection changes.
|
||||||
|
aiUsageWidget :: TaffyIO Gtk.Widget
|
||||||
|
aiUsageWidget = do
|
||||||
|
openAIWidget <- openAIUsageSection
|
||||||
|
anthropicWidget <- anthropicUsageSection
|
||||||
|
stackWidget <- liftIO $ do
|
||||||
|
stack <- Gtk.stackNew
|
||||||
|
Gtk.stackAddNamed stack openAIWidget codexChild
|
||||||
|
Gtk.stackAddNamed stack anthropicWidget claudeChild
|
||||||
|
readActiveAIScratchpad >>= Gtk.stackSetVisibleChildName stack
|
||||||
|
|
||||||
|
let syncVisibleChild =
|
||||||
|
readActiveAIScratchpad
|
||||||
|
>>= \name -> postGUIASync (Gtk.stackSetVisibleChildName stack name)
|
||||||
|
|
||||||
|
void $ Gtk.onWidgetRealize stack $ do
|
||||||
|
stateDir <- aiScratchpadStateDir
|
||||||
|
createDirectoryIfMissing True stateDir
|
||||||
|
manager <- FSNotify.startManager
|
||||||
|
void $
|
||||||
|
FSNotify.watchDir
|
||||||
|
manager
|
||||||
|
stateDir
|
||||||
|
((== aiScratchpadStateFile) . takeFileName . FSNotify.eventPath)
|
||||||
|
(const syncVisibleChild)
|
||||||
|
syncVisibleChild
|
||||||
|
void $ Gtk.onWidgetUnrealize stack $ FSNotify.stopManager manager
|
||||||
|
|
||||||
|
Gtk.widgetShowAll stack
|
||||||
|
Gtk.toWidget stack
|
||||||
|
decorateWithClassAndBox "ai-usage" stackWidget
|
||||||
@@ -33,12 +33,6 @@ import qualified System.Taffybar.Widget.Audio as Audio
|
|||||||
import System.Taffybar.Widget.CPUMonitor (cpuMonitorNew)
|
import System.Taffybar.Widget.CPUMonitor (cpuMonitorNew)
|
||||||
import System.Taffybar.Widget.Generic.Graph (GraphConfig (..), GraphDirection (..), GraphStyle (..), defaultGraphConfig)
|
import System.Taffybar.Widget.Generic.Graph (GraphConfig (..), GraphDirection (..), GraphStyle (..), defaultGraphConfig)
|
||||||
import qualified System.Taffybar.Widget.NetworkManager as NetworkManager
|
import qualified System.Taffybar.Widget.NetworkManager as NetworkManager
|
||||||
import System.Taffybar.Widget.OpenAIUsage
|
|
||||||
( OpenAIUsageDisplayMode (OpenAIUsageDisplayRemaining),
|
|
||||||
OpenAIUsageStackConfig (..),
|
|
||||||
defaultOpenAIUsageStackConfig,
|
|
||||||
openAIUsageSectionNewWith,
|
|
||||||
)
|
|
||||||
import System.Taffybar.Widget.SNIMenu (withNmAppletMenu)
|
import System.Taffybar.Widget.SNIMenu (withNmAppletMenu)
|
||||||
import System.Taffybar.Widget.SNITray
|
import System.Taffybar.Widget.SNITray
|
||||||
( CollapsibleSNITrayParams (..),
|
( CollapsibleSNITrayParams (..),
|
||||||
@@ -60,6 +54,7 @@ import System.Taffybar.Widget.Util
|
|||||||
)
|
)
|
||||||
import qualified System.Taffybar.Widget.Wlsunset as Wlsunset
|
import qualified System.Taffybar.Widget.Wlsunset as Wlsunset
|
||||||
import qualified System.Taffybar.Widget.Workspaces as Workspaces
|
import qualified System.Taffybar.Widget.Workspaces as Workspaces
|
||||||
|
import TaffybarConfig.AIUsage (aiUsageWidget)
|
||||||
import TaffybarConfig.Host (laptopHosts)
|
import TaffybarConfig.Host (laptopHosts)
|
||||||
import TaffybarConfig.WidgetUtil
|
import TaffybarConfig.WidgetUtil
|
||||||
( decorateWithClassAndBox,
|
( decorateWithClassAndBox,
|
||||||
@@ -375,16 +370,6 @@ usageSectionWidget klass iconFile tooltip stackBuilder =
|
|||||||
section <- buildIconLabelBox iconWidget stack
|
section <- buildIconLabelBox iconWidget stack
|
||||||
widgetSetClassGI section "usage-section"
|
widgetSetClassGI section "usage-section"
|
||||||
|
|
||||||
openAIUsageWidget :: TaffyIO Gtk.Widget
|
|
||||||
openAIUsageWidget = do
|
|
||||||
iconWidget <- liftIO $ usageLogoWidget "openai-symbol.svg" "OpenAI usage"
|
|
||||||
decorateWithClassAndBoxM "openai-usage" $
|
|
||||||
openAIUsageSectionNewWith
|
|
||||||
iconWidget
|
|
||||||
defaultOpenAIUsageStackConfig
|
|
||||||
{ openAIUsageStackDefaultDisplayMode = OpenAIUsageDisplayRemaining
|
|
||||||
}
|
|
||||||
|
|
||||||
sniPriorityVisibilityThresholdDefault :: Int
|
sniPriorityVisibilityThresholdDefault :: Int
|
||||||
sniPriorityVisibilityThresholdDefault = 0
|
sniPriorityVisibilityThresholdDefault = 0
|
||||||
|
|
||||||
@@ -445,7 +430,7 @@ endWidgetsForHost hostName =
|
|||||||
let baseEndWidgets =
|
let baseEndWidgets =
|
||||||
[ sniTrayWidget,
|
[ sniTrayWidget,
|
||||||
audioWidget,
|
audioWidget,
|
||||||
openAIUsageWidget,
|
aiUsageWidget,
|
||||||
cpuWidget,
|
cpuWidget,
|
||||||
ramSwapWidget,
|
ramSwapWidget,
|
||||||
diskUsageWidget,
|
diskUsageWidget,
|
||||||
@@ -458,7 +443,7 @@ endWidgetsForHost hostName =
|
|||||||
sniTrayWidget,
|
sniTrayWidget,
|
||||||
asusDiskUsageWidget,
|
asusDiskUsageWidget,
|
||||||
audioBacklightWidget,
|
audioBacklightWidget,
|
||||||
openAIUsageWidget,
|
aiUsageWidget,
|
||||||
cpuWidget,
|
cpuWidget,
|
||||||
ramSwapWidget,
|
ramSwapWidget,
|
||||||
sunLockWidget,
|
sunLockWidget,
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ cabal-version: >=1.10
|
|||||||
executable taffybar
|
executable taffybar
|
||||||
hs-source-dirs: .
|
hs-source-dirs: .
|
||||||
main-is: taffybar.hs
|
main-is: taffybar.hs
|
||||||
other-modules: TaffybarConfig.Config
|
other-modules: TaffybarConfig.AIUsage
|
||||||
|
, TaffybarConfig.Config
|
||||||
, TaffybarConfig.ChromeFavicons
|
, TaffybarConfig.ChromeFavicons
|
||||||
, TaffybarConfig.Host
|
, TaffybarConfig.Host
|
||||||
, TaffybarConfig.Widgets
|
, TaffybarConfig.Widgets
|
||||||
@@ -27,6 +28,7 @@ executable taffybar
|
|||||||
, containers
|
, containers
|
||||||
, directory
|
, directory
|
||||||
, filepath
|
, filepath
|
||||||
|
, fsnotify
|
||||||
, gi-gdk3
|
, gi-gdk3
|
||||||
, gi-gtk3
|
, gi-gtk3
|
||||||
, gi-gdkpixbuf
|
, gi-gdkpixbuf
|
||||||
|
|||||||
Reference in New Issue
Block a user