Skip to content

Commit

Permalink
feat: custom status icons (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
rszyma authored Jan 24, 2025
1 parent 1b49e80 commit b217d8c
Show file tree
Hide file tree
Showing 12 changed files with 131 additions and 24 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ kanata-tray
.envrc
kanata_tray_lastrun.log
result
*.kbd
kanata-tray.*toml*
kanata
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ allow_concurrent_presets = false
kanata_executable = '~/bin/kanata' # if empty or omitted, system $PATH will be searched.
kanata_config = '' # if empty or not omitted, kanata default config locations will be used.
tcp_port = 5829 # if not specified, defaults to 5829
extra_args = ['-n', '-c=~/.config/kanata/another.kbd']

[defaults.hooks]
# Hooks allow running custom commands on specific events (e.g. starting preset).
Expand Down Expand Up @@ -91,6 +90,21 @@ Other notes:
- You can use `~` in `kanata_config`, `kanata_executable` and `extra_args` to substitute to your "home" directory.
- On Windows: make sure to surround paths with single-quotes `'` instead of double-quotes, otherwise paths will not work (because `\` would be treated as escape character).

### Overriding status icons

It's possible to customize looks of status icons - i.e. reload, idle, paused.

Similar to `icons` folder (layer icons), `status_icons` folder will be created
next to `kanata-tray.toml` if it doesn't exists. When first creating, it will
also populate with default icons. To override status icons, simply replace it
with other one of your choice.

- Accepted file types are the same as in `icons`.
- The filename prefix must match specifc status i.e. files must start with one of:
"default", "crash", "pause", "live-reload".
- If there are multiple files matching the prefix, only one of them will be loaded,
and other ignored.

### Hooks

Hooks allow running custom commands on specific events (e.g. starting preset).
Expand Down
16 changes: 8 additions & 8 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"github.com/labstack/gommon/log"
"github.com/skratchdot/open-golang/open"

"github.com/rszyma/kanata-tray/icons"
runner_pkg "github.com/rszyma/kanata-tray/runner"
"github.com/rszyma/kanata-tray/status_icons"
)

type SystrayApp struct {
Expand Down Expand Up @@ -45,7 +45,7 @@ type SystrayApp struct {
}

func NewSystrayApp(menuTemplate []PresetMenuEntry, layerIcons LayerIcons, allowConcurrentPresets bool, logFilepath string) *SystrayApp {
systray.SetIcon(icons.Default)
systray.SetIcon(status_icons.Default)
systray.SetTooltip("kanata-tray")

t := &SystrayApp{
Expand Down Expand Up @@ -125,7 +125,7 @@ func (t *SystrayApp) runPreset(presetIndex int, runner *runner_pkg.Runner) {
}

func (app *SystrayApp) StartProcessingLoop(runner *runner_pkg.Runner, configFolder string) {
app.setIcon(icons.Pause)
app.setIcon(status_icons.Pause)

// handle autoruns
autoranOnePreset := false
Expand Down Expand Up @@ -156,7 +156,7 @@ func (app *SystrayApp) StartProcessingLoop(runner *runner_pkg.Runner, configFold
if event.Item.LayerChange != nil {
icon := app.layerIcons.IconForLayerName(event.PresetName, event.Item.LayerChange.NewLayer)
if icon == nil {
icon = icons.Default
icon = status_icons.Default
}
app.setIcon(icon)
}
Expand All @@ -177,7 +177,7 @@ func (app *SystrayApp) StartProcessingLoop(runner *runner_pkg.Runner, configFold
}
if event.Item.ConfigFileReload != nil {
prevIcon := app.currentIconData
app.setIcon(icons.LiveReload)
app.setIcon(status_icons.LiveReload)
time.Sleep(150 * time.Millisecond)
app.setIcon(prevIcon)
}
Expand All @@ -192,14 +192,14 @@ func (app *SystrayApp) StartProcessingLoop(runner *runner_pkg.Runner, configFold
if runnerPipelineErr != nil {
log.Errorf("Kanata runner terminated with an error: %v", runnerPipelineErr)
app.setStatus(i, statusCrashed)
app.setIcon(icons.Crash)
app.setIcon(status_icons.Crash)
} else {
log.Infof("Previous kanata process terminated successfully")
app.setStatus(i, statusIdle)
if app.isAnyPresetRunning() {
app.setIcon(icons.Default)
app.setIcon(status_icons.Default)
} else {
app.setIcon(icons.Pause)
app.setIcon(status_icons.Pause)
}
}
if app.scheduledPresetIndex != -1 {
Expand Down
15 changes: 0 additions & 15 deletions icons/icons.go

This file was deleted.

10 changes: 10 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/rszyma/kanata-tray/app"
"github.com/rszyma/kanata-tray/config"
"github.com/rszyma/kanata-tray/runner"
"github.com/rszyma/kanata-tray/status_icons"
)

var (
Expand Down Expand Up @@ -162,6 +163,15 @@ func mainImpl() error {
}
layerIcons := app.ResolveIcons(configFolder, cfg)

err = status_icons.CreateDefaultStatusIconsDirIfNotExists(configFolder)
if err != nil {
return fmt.Errorf("CreateDefaultStatusIconsDirIfNotExists: %v", err)
}
err = status_icons.LoadCustomStatusIcons(configFolder)
if err != nil {
return fmt.Errorf("LoadCustomStatusIcons: %v", err)
}

runner := runner.NewRunner()

onReady := func() {
Expand Down
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
95 changes: 95 additions & 0 deletions status_icons/status_icons.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package status_icons

import (
_ "embed"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"

"github.com/labstack/gommon/log"
)

//go:embed default.ico
var Default []byte

//go:embed crash.ico
var Crash []byte

//go:embed pause.ico
var Pause []byte

//go:embed live-reload.ico
var LiveReload []byte

//////////////////////////////////////////////

var statusIconsDir string = "status_icons"

func LoadCustomStatusIcons(configDir string) error {
prefixes := []string{"default", "crash", "pause", "live-reload"}
for i, prefix := range prefixes {
matches, err := filepath.Glob(filepath.Join(
configDir, statusIconsDir, fmt.Sprintf("%s.*", prefix),
))
if err != nil {
return fmt.Errorf("filepath.Glob: %v", err)
}
if len(matches) < 1 {
continue
}

// Take only first match, ignore others. There should be only 1 matching
// icon name anyway.
match := matches[0]

log.Infof("loading status icon: %s", match)
fileContent, err := os.ReadFile(match)
if err != nil {
log.Errorf("LoadCustomStatusIcons: os.ReadFile: %v", err)
continue
}

switch i {
case 0:
Default = fileContent
case 1:
Crash = fileContent
case 2:
Pause = fileContent
case 3:
LiveReload = fileContent
default:
panic("out of range")
}
}

return nil
}

func CreateDefaultStatusIconsDirIfNotExists(configDir string) error {
customIconsPath := filepath.Join(configDir, statusIconsDir)
_, err := os.Stat(customIconsPath)

if errors.Is(err, fs.ErrNotExist) {
log.Infof("status_icons dir doesn't exist. Creating it and populating with the default icons.")
err := os.MkdirAll(customIconsPath, os.ModePerm)
if err != nil {
return fmt.Errorf("failed to create folder: %v", err)
}
names := []string{"default.ico", "crash.ico", "pause.ico", "live-reload.ico"}
data := [][]byte{Default, Crash, Pause, LiveReload}
for i, name := range names {
path := filepath.Join(customIconsPath, name)
err := os.WriteFile(path, data[i], 0o644)
if err != nil {
return fmt.Errorf("writing file %s failed", path)
}
}
} else if err != nil {
return fmt.Errorf("error checking if %s dir exists", customIconsPath)
}
// already exists, do nothing.
return nil
}

0 comments on commit b217d8c

Please sign in to comment.