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.Generic.Graph (GraphConfig (..), GraphDirection (..), GraphStyle (..), defaultGraphConfig)
|
||||
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.SNITray
|
||||
( CollapsibleSNITrayParams (..),
|
||||
@@ -60,6 +54,7 @@ import System.Taffybar.Widget.Util
|
||||
)
|
||||
import qualified System.Taffybar.Widget.Wlsunset as Wlsunset
|
||||
import qualified System.Taffybar.Widget.Workspaces as Workspaces
|
||||
import TaffybarConfig.AIUsage (aiUsageWidget)
|
||||
import TaffybarConfig.Host (laptopHosts)
|
||||
import TaffybarConfig.WidgetUtil
|
||||
( decorateWithClassAndBox,
|
||||
@@ -375,16 +370,6 @@ usageSectionWidget klass iconFile tooltip stackBuilder =
|
||||
section <- buildIconLabelBox iconWidget stack
|
||||
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 = 0
|
||||
|
||||
@@ -445,7 +430,7 @@ endWidgetsForHost hostName =
|
||||
let baseEndWidgets =
|
||||
[ sniTrayWidget,
|
||||
audioWidget,
|
||||
openAIUsageWidget,
|
||||
aiUsageWidget,
|
||||
cpuWidget,
|
||||
ramSwapWidget,
|
||||
diskUsageWidget,
|
||||
@@ -458,7 +443,7 @@ endWidgetsForHost hostName =
|
||||
sniTrayWidget,
|
||||
asusDiskUsageWidget,
|
||||
audioBacklightWidget,
|
||||
openAIUsageWidget,
|
||||
aiUsageWidget,
|
||||
cpuWidget,
|
||||
ramSwapWidget,
|
||||
sunLockWidget,
|
||||
|
||||
@@ -13,7 +13,8 @@ cabal-version: >=1.10
|
||||
executable taffybar
|
||||
hs-source-dirs: .
|
||||
main-is: taffybar.hs
|
||||
other-modules: TaffybarConfig.Config
|
||||
other-modules: TaffybarConfig.AIUsage
|
||||
, TaffybarConfig.Config
|
||||
, TaffybarConfig.ChromeFavicons
|
||||
, TaffybarConfig.Host
|
||||
, TaffybarConfig.Widgets
|
||||
@@ -27,6 +28,7 @@ executable taffybar
|
||||
, containers
|
||||
, directory
|
||||
, filepath
|
||||
, fsnotify
|
||||
, gi-gdk3
|
||||
, gi-gtk3
|
||||
, gi-gdkpixbuf
|
||||
|
||||
Reference in New Issue
Block a user