diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 290e1e5..7ae2016 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -24,4 +24,3 @@ jobs: args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: EndBug/latest-tag@latest diff --git a/CHANGELOG.md b/CHANGELOG.md index e2a145b..b9c4983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,28 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.0] - 2024-01-21 + +### Added + +- add support for input source collections, allowing users to group and cycle through different sets of input sources +- introduce dynamic starting behavior, enabling the utility to begin with the currently active input source upon initialization + +### Changed + +- update the configuration structure to accommodate collections of input sources, replacing the previous primary and additional input sources configuration +- modify the Double Press Mode to switch between different input source collections +- adjust the Single Press Mode to cycle through input sources within the current collection +- move Homebrew tap and `betterglobekey.rb` formula to `Serpentiel/homebrew-tools` + +### Removed + +- remove the distinction between primary and additional input sources in the configuration, in favor of the new collection-based approach + +### Fixed + +- fix an issue where the utility would not start from the currently active input source + ## [2.1.1] - 2023-04-05 ### Changed @@ -113,7 +135,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - initial release -[unreleased]: https://github.com/Serpentiel/betterglobekey/compare/v2.1.1...HEAD +[unreleased]: https://github.com/Serpentiel/betterglobekey/compare/v3.0.0...HEAD +[3.0.0]: https://github.com/Serpentiel/betterglobekey/releases/tag/v3.0.0 [2.1.1]: https://github.com/Serpentiel/betterglobekey/releases/tag/v2.1.1 [2.1.0]: https://github.com/Serpentiel/betterglobekey/releases/tag/v2.1.0 [2.0.1]: https://github.com/Serpentiel/betterglobekey/releases/tag/v2.0.1 diff --git a/README.md b/README.md index d348dbf..43e0527 100644 --- a/README.md +++ b/README.md @@ -77,32 +77,45 @@ experience, and I sincerely hope that one day Apple is going to make it this way ## Getting Started -The utility replaces the default behavior of the Globe key and adds two new modes to it: +The utility enhances the functionality of the Globe key by introducing two distinct modes of operation and starting +from the currently active input source: 1. **Single Press Mode** - Single press mode is the mode that is activated when the Globe key is pressed once. + Single press mode is activated when the Globe key is pressed once. - Single press mode cycles between your primary input sources—I believe most of the users out there will not even need - the other available mode as it is probably only useful if you have more than average amount of input sources. + In this mode, the utility cycles through a collection of input sources. Each press of the Globe key switches to the + next input source within the current collection. - Single press mode uses the input sources defined in the config's `input_sources.primary` array. + The collections of input sources are defined in the configuration under `input_sources`. Each key-value pair within + this map represents a named collection of input sources. For example: -2. **Double Press Mode** + ```yaml + input_sources: + foo: + - com.apple.keylayout.US + - com.apple.keylayout.Russian + bar: + - com.apple.keylayout.Finnish + - com.apple.keylayout.Ukrainian + - com.apple.inputmethod.Kotoeri.RomajiTyping.Japanese + ``` + + Upon initialization, the utility determines the current active input source and starts from that particular source + within its respective collection. - Double press mode is the mode that is activated when the Globe key is double pressed. +2. **Double Press Mode** - Double press mode cycles between your additional input sources. If you use multiple input sources, you - probably use only several input sources frequently—you might consider putting those that you use the least under - additional input sources. + Double press mode is activated when the Globe key is double-pressed. - Double press mode uses the input sources defined in the config's `input_sources.additional` array. + In this mode, the utility switches between different collections of input sources. Each double press of the Globe + key cycles to the next collection in the configuration. - Double press maximum delay is also configurable in the config's `double_press.maximum_delay` property. + The maximum time interval between the first and second press that is considered a double press can be configured + in the `double_press.maximum_delay` property. This delay is specified in milliseconds. - > **N.B.** This is not working as designed at the moment—this is supposed to open the original input source popup, but - > implementing it requires some reverse engineering. There is probably a function in macOS private API that can be used - > to open the popup. +These enhancements aim to provide a more versatile and user-friendly experience for managing multiple input sources, +especially for users who frequently switch between different languages or keyboard layouts. ### Prerequisites @@ -115,7 +128,7 @@ The utility replaces the default behavior of the Globe key and adds two new mode - Install the utility via [Homebrew](https://brew.sh): ```bash - brew tap Serpentiel/betterglobekey https://github.com/Serpentiel/betterglobekey.git + brew tap Serpentiel/tools brew install betterglobekey ``` diff --git a/betterglobekey.rb b/betterglobekey.rb deleted file mode 100644 index 1a08092..0000000 --- a/betterglobekey.rb +++ /dev/null @@ -1,37 +0,0 @@ -class Betterglobekey < Formula - desc "Make macOS Globe key great again!" - version "latest" - homepage "https://github.com/Serpentiel/betterglobekey" - url "https://github.com/Serpentiel/betterglobekey.git", tag: "latest" - license "MIT" - head "https://github.com/Serpentiel/betterglobekey.git", branch: "main" - - depends_on "go" - - def install - system "go", "build", "-o", "#{bin}/betterglobekey", "./main.go" - - output = Utils.safe_popen_read("#{bin}/betterglobekey", "completion", "bash") - (bash_completion/"betterglobekey").write output - - output = Utils.safe_popen_read("#{bin}/betterglobekey", "completion", "zsh") - (zsh_completion/"_betterglobekey").write output - - output = Utils.safe_popen_read("#{bin}/betterglobekey", "completion", "fish") - (fish_completion/"betterglobekey.fish").write output - end - - service do - run "#{bin}/betterglobekey" - keep_alive true - end - - test do - str_default = shell_output("#{bin}/betterglobekey") - str_help = shell_output("#{bin}/betterglobekey --help") - assert_equal str_default, str_help - - assert_match "Usage:", str_help - assert_match "Available Commands:", str_help - end -end diff --git a/cmd/root.go b/cmd/root.go index 588d894..0b69d96 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,7 +5,7 @@ import ( "context" "os" - "github.com/Serpentiel/betterglobekey/internal/eventhandler" + "github.com/Serpentiel/betterglobekey/internal/pkg/eventhandler" "github.com/Serpentiel/betterglobekey/internal/provide" "github.com/Serpentiel/betterglobekey/pkg/logger" hook "github.com/robotn/gohook" diff --git a/go.mod b/go.mod index 2e81724..402f466 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Serpentiel/betterglobekey -go 1.19 +go 1.21 require ( github.com/robotn/gohook v0.41.0 @@ -8,7 +8,7 @@ require ( github.com/spf13/viper v1.18.2 go.uber.org/fx v1.20.1 go.uber.org/zap v1.26.0 - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) @@ -18,7 +18,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -28,9 +28,9 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/vcaesar/keycode v0.10.1 // indirect go.uber.org/atomic v1.10.0 // indirect - go.uber.org/dig v1.17.0 // indirect + go.uber.org/dig v1.17.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index cbd57c0..243dcb6 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,37 @@ github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robotn/gohook v0.41.0 h1:h1vK3w/UQpq0YkIiGnxm9Awv85W54esL0/NUYGueggo= github.com/robotn/gohook v0.41.0/go.mod h1:FedpuAkVqzM5t67L5fcf3hSSCUDO9cM5YkWCw1U+nuc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= @@ -53,25 +61,28 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW github.com/vcaesar/keycode v0.10.1 h1:0DesGmMAPWpYTCYddOFiCMKCDKgNnwiQa2QXindVUHw= github.com/vcaesar/keycode v0.10.1/go.mod h1:JNlY7xbKsh+LAGfY2j4M3znVrGEm5W1R8s/Uv6BJcfQ= github.com/vcaesar/tt v0.20.0 h1:9t2Ycb9RNHcP0WgQgIaRKJBB+FrRdejuaL6uWIHuoBA= +github.com/vcaesar/tt v0.20.0/go.mod h1:GHPxQYhn+7OgKakRusH7KJ0M5MhywoeLb8Fcffs/Gtg= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI= -go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= +go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= +go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= diff --git a/internal/assets/.betterglobekey.example.yaml b/internal/assets/.betterglobekey.example.yaml index a81aa1b..5dc0d7d 100644 --- a/internal/assets/.betterglobekey.example.yaml +++ b/internal/assets/.betterglobekey.example.yaml @@ -1,26 +1,32 @@ -# logger defines the parameters for the logger. +# Logger settings logger: - # path is the path to the log file. + # Path to the log file path: betterglobekey.log - - # retain defines the parameters for log file retention. + # Log file retention settings retain: - # days is the number of days to retain log files. + # Number of days to retain log files days: 30 - - # copies is the number of log files to retain. + # Number of log files to retain copies: 3 -# double_press defines double press configuration options. +# Configuration options for double press of the Globe key double_press: - # maximum_delay is the maximum time in milliseconds between the first and second press of the Globe key to be - # considered a double press. + # Maximum time (in milliseconds) between first and second press to consider as a double press maximum_delay: 250 -# input_sources defines the input sources to be used when the Globe key is pressed. -input_sources: - # primary defines the primary input sources. This is used when the Globe key has not been double pressed. - primary: [] - - # additional defines the additional input sources. This is used when the Globe key has been double pressed. - additional: [] +# Input sources configuration +# +# Each key-value pair within the 'input_sources' map represents a named collection of input sources. +# Single press of the Globe key cycles through input sources within the current collection. +# Double press of the Globe key switches between these collections. +# +# Example configuration: +# input_sources: +# foo: +# - com.apple.keylayout.US +# - com.apple.keylayout.Russian +# bar: +# - com.apple.keylayout.Finnish +# - com.apple.keylayout.Ukrainian +# - com.apple.inputmethod.Kotoeri.RomajiTyping.Japanese +input_sources: {} diff --git a/internal/eventhandler/fnkeyhandler.go b/internal/eventhandler/fnkeyhandler.go deleted file mode 100644 index fbb6c0b..0000000 --- a/internal/eventhandler/fnkeyhandler.go +++ /dev/null @@ -1,119 +0,0 @@ -// Package eventhandler encapsulates logic to handle keyboard events. -package eventhandler - -import ( - "time" - - "github.com/Serpentiel/betterglobekey/pkg/inputsource" - "github.com/Serpentiel/betterglobekey/pkg/logger" - "github.com/spf13/viper" -) - -// newFnKeyHandler returns a new fnKeyHandler. -func newFnKeyHandler(v *viper.Viper, l logger.Logger) *fnKeyHandler { - return &fnKeyHandler{ - l: l, - - doublePressMaximumDelay: v.GetInt("double_press.maximum_delay"), - primaryInputSources: v.GetStringSlice("input_sources.primary"), - additionalInputSources: v.GetStringSlice("input_sources.additional"), - } -} - -var _ handler = (*fnKeyHandler)(nil) - -// fnKeyHandler is a handler for the fn key up event. -type fnKeyHandler struct { - // l is the logger. - l logger.Logger - - // doublePressMaximumDelay is the maximum delay between two presses of the fn key for them to be considered as a - // double press. - doublePressMaximumDelay int - - // primaryInputSources is a slice of the primary input sources. - primaryInputSources []string - - // additionalInputSources is a slice of the additional input sources. - additionalInputSources []string - - // doublePressable is a bool that indicates if the key is double pressable. - doublePressable bool - - // doublePressed is a bool that indicates if the key is double pressed. - doublePressed bool - - // currentInputSource is the current input source. - currentInputSource string - - // previousInputSource is the previous input source. - previousInputSource string -} - -// getNextInputSource returns the next input source. -func (h *fnKeyHandler) getNextInputSource(inputSources *[]string) int { - nextInputSource := 0 - - for k, v := range *inputSources { - if v == h.currentInputSource { - if k == len(*inputSources)-1 { - break - } - - nextInputSource = k + 1 - } - } - - return nextInputSource -} - -// setInputSource sets the input source. -func (h *fnKeyHandler) setInputSource(inputSource string) { - h.previousInputSource = h.currentInputSource - - inputsource.Select(inputSource) - - h.l.Info("input source set", "from", h.previousInputSource, "to", inputSource) -} - -// KeyUp is called when the key is released. -func (h *fnKeyHandler) KeyUp() handlerFunc { - return func() { - if len(h.additionalInputSources) > 0 { - doublePressTicker := time.NewTicker(time.Duration(h.doublePressMaximumDelay) * time.Millisecond) - - h.doublePressed = h.doublePressable - - h.doublePressable = !h.doublePressable - - go func() { - if <-doublePressTicker.C; true { - h.doublePressable = false - - doublePressTicker.Stop() - - return - } - }() - } - - h.currentInputSource = inputsource.Current() - - if !h.doublePressed { - h.l.Info("globe key pressed") - - h.setInputSource(h.primaryInputSources[h.getNextInputSource(&h.primaryInputSources)]) - } else { - // TODO: This is not working as designed at the moment—this is supposed to open the original - // input source popup, but implementing it requires some reverse engineering. - // There is probably a function in macOS private API that can be used to open the popup. - h.l.Info("globe key double pressed") - - h.setInputSource(h.previousInputSource) - - h.currentInputSource = inputsource.Current() - - h.setInputSource(h.additionalInputSources[h.getNextInputSource(&h.additionalInputSources)]) - } - } -} diff --git a/internal/eventhandler/eventhandler.go b/internal/pkg/eventhandler/eventhandler.go similarity index 100% rename from internal/eventhandler/eventhandler.go rename to internal/pkg/eventhandler/eventhandler.go diff --git a/internal/pkg/eventhandler/fnkeyhandler.go b/internal/pkg/eventhandler/fnkeyhandler.go new file mode 100644 index 0000000..37eb648 --- /dev/null +++ b/internal/pkg/eventhandler/fnkeyhandler.go @@ -0,0 +1,202 @@ +package eventhandler + +import ( + "errors" + "time" + + "github.com/Serpentiel/betterglobekey/pkg/inputsource" + "github.com/Serpentiel/betterglobekey/pkg/logger" + "github.com/Serpentiel/betterglobekey/pkg/util" + "github.com/spf13/viper" +) + +// errNoInputSourceAvailable is the error returned when no input source is available. +var errNoInputSourceAvailable = errors.New("no input source available") + +// newFnKeyHandler initializes and returns a new fnKeyHandler instance. +func newFnKeyHandler(v *viper.Viper, l logger.Logger) *fnKeyHandler { + inputSources := v.GetStringMapStringSlice("input_sources") + + currentInputSource := inputsource.Current() + + var currentCollection string + + for collection, sources := range inputSources { + if util.Contains(sources, currentInputSource) { + currentCollection = collection + + break + } + } + + if currentCollection == "" && len(inputSources) > 0 { + for k := range inputSources { + currentCollection = k + + break + } + } + + return &fnKeyHandler{ + l: l, + doublePressMaximumDelay: v.GetInt("double_press.maximum_delay"), + inputSources: inputSources, + + lastInputSourceInCollection: make(map[string]string), + currentCollection: currentCollection, + currentInputSource: currentInputSource, + lastPressTime: time.Now(), + } +} + +// fnKeyHandler is a type that represents a handler for the fn key. +type fnKeyHandler struct { + // l is the logger. + l logger.Logger + // doublePressMaximumDelay is the maximum delay between two presses to be considered a double press. + doublePressMaximumDelay int + // inputSources is a map of input source collections. + inputSources map[string][]string + + // lastInputSourceInCollection is a map of the last input source used in each collection. + lastInputSourceInCollection map[string]string + // currentCollection is the current input source collection. + currentCollection string + // currentInputSource is the current input source. + currentInputSource string + // lastPressTime is the time of the last press. + lastPressTime time.Time +} + +// KeyUp handles key up events and determines whether it's a single or double press. +func (h *fnKeyHandler) KeyUp() handlerFunc { + return func() { + currentTime := time.Now() + elapsed := currentTime.Sub(h.lastPressTime) + h.lastPressTime = currentTime + + if elapsed <= time.Duration(h.doublePressMaximumDelay)*time.Millisecond { + h.handleDoublePress() + } else { + h.handleSinglePress() + } + } +} + +// handleSinglePress handles a single press of the fn key. +func (h *fnKeyHandler) handleSinglePress() { + h.l.Info("globe key pressed") + + nextSource, err := h.getNextInputSource() + if err != nil { + h.l.Error("failed to get next input source", "error", err) + + return + } + + h.lastInputSourceInCollection[h.currentCollection] = nextSource + h.setInputSource(nextSource) +} + +// handleDoublePress handles a double press of the fn key. +func (h *fnKeyHandler) handleDoublePress() { + h.l.Info("globe key double pressed") + + h.decrementPreviousInputSource(h.currentCollection) + + h.switchCollection() + h.setInputSourceToLastOrFirst() +} + +// getNextInputSource returns the next input source within the current collection. +func (h *fnKeyHandler) getNextInputSource() (string, error) { + collection, exists := h.inputSources[h.currentCollection] + if !exists || len(collection) == 0 { + return "", errNoInputSourceAvailable + } + + for i, source := range collection { + if source == h.currentInputSource { + return collection[(i+1)%len(collection)], nil + } + } + + return collection[0], nil +} + +// switchCollection cycles to the next input source collection. +func (h *fnKeyHandler) switchCollection() { + var nextCollection string + + foundCurrent := false + + for name := range h.inputSources { + if foundCurrent { + nextCollection = name + + break + } + + if name == h.currentCollection { + foundCurrent = true + } + } + + if nextCollection == "" { + for name := range h.inputSources { + nextCollection = name + + break + } + } + + h.currentCollection = nextCollection +} + +// setInputSourceToLastOrFirst sets the input source to the last used in the current collection or the first one. +func (h *fnKeyHandler) setInputSourceToLastOrFirst() { + nextSource := h.lastInputSourceInCollection[h.currentCollection] + + if nextSource == "" { + nextSource = h.inputSources[h.currentCollection][0] + } + + if nextSource != "" { + h.setInputSource(nextSource) + } +} + +// decrementPreviousInputSource decrements the last input source in the specified collection. +func (h *fnKeyHandler) decrementPreviousInputSource(collection string) { + lastSource := h.lastInputSourceInCollection[collection] + collectionSources := h.inputSources[collection] + + index := -1 + + for i, source := range collectionSources { + if source == lastSource { + index = i + + break + } + } + + if index > 0 { + h.lastInputSourceInCollection[collection] = collectionSources[index-1] + } else if index == 0 { + h.lastInputSourceInCollection[collection] = collectionSources[len(collectionSources)-1] + } +} + +// setInputSource sets the input source if it exists. +func (h *fnKeyHandler) setInputSource(inputSource string) { + h.currentInputSource = inputSource + + if util.Contains(inputsource.All(), inputSource) { + inputsource.Select(inputSource) + + h.l.Info("input source set", "source", inputSource) + } else { + h.l.Info("input source not found", "source", inputSource) + } +} diff --git a/internal/eventhandler/rawcode.go b/internal/pkg/eventhandler/rawcode.go similarity index 100% rename from internal/eventhandler/rawcode.go rename to internal/pkg/eventhandler/rawcode.go diff --git a/pkg/util/slices.go b/pkg/util/slices.go new file mode 100644 index 0000000..d3ebb94 --- /dev/null +++ b/pkg/util/slices.go @@ -0,0 +1,13 @@ +// Package util contains utility functions. +package util + +// Contains checks if a slice contains a given string. +func Contains(slice []string, str string) bool { + for _, v := range slice { + if v == str { + return true + } + } + + return false +}