fusuma-plugin-appmatcher 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 01dac2dad9fcdf720ae85a3d640e1f41d22b34cff26f688b8eb05ecff25ed354
4
- data.tar.gz: 1bf60e0762ce6ea974de6006c7c8bba37eb596d64655905db14e1072b4a4a48e
3
+ metadata.gz: 1eb7a30939beb5781398de99a315a6382beb4d8183e9f87fa076c560a6d2497e
4
+ data.tar.gz: e198858920d58779268878849b14fe35a57ad3b4fb9c90c973d1347acf65e791
5
5
  SHA512:
6
- metadata.gz: ce5c29f65ae143c0d345cae02082345e5f32fcf3b5ba18ad711ce0c9da6b205f8895a9636c733971e0ce060529b921876d414d1295b4e79e46f584a4a6f13f8c
7
- data.tar.gz: 468eab88d695b833d383d04088111cd7656fab4ee6921cef081737f8170b110e377780012e099bb1d95acc788d4bef9b583f53993a50f8ba69aed75ebbaace53
6
+ metadata.gz: fdce434ee079f9dc5ac2c258e7f5748ee4add701a7eb36d49a90ef9922046f12e8340240ba46d6750b6c6b83ed92b57ef9a0a9f1cc8473715a965bb884754d4d
7
+ data.tar.gz: 1633f89b6c50380ab6bf1acd31253f60b96d252f554c5630e23c0b6fd318d63ebd827602fb6414b48f8af6f9dd21ea373c8ef741592e93b2912c1bea74777490
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [Fusuma](https://github.com/iberianpig/fusuma) plugin configure app-specific gestures
4
4
 
5
5
  * Switch gesture mappings by detecting active application.
6
- * Support X11, Ubuntu-Wayland
6
+ * Support X11, GNOME Wayland, Hyprland, COSMIC
7
7
 
8
8
 
9
9
  ## Installation
@@ -27,6 +27,14 @@ $ fusuma-appmatcher --install-gnome-extension
27
27
 
28
28
  Restart your session(logout/login), then activate Appmatcher on gnome-extensions-app
29
29
 
30
+ ### Install cos-cli for COSMIC Desktop
31
+
32
+ On POP!_OS COSMIC, fusuma-plugin-appmatcher uses [cos-cli](https://github.com/estin/cos-cli) (a third-party CLI) to read active window state from the cosmic-comp Wayland compositor.
33
+
34
+ ```sh
35
+ $ cargo install --git https://github.com/estin/cos-cli
36
+ ```
37
+
30
38
  ## List Running Application names
31
39
 
32
40
  `$ fusuma-appmatcher -l` prints Running Application names.
@@ -97,10 +105,71 @@ swipe:
97
105
  sendkey: 'LEFTSHIFT+LEFTCTRL+W'
98
106
  ```
99
107
 
108
+ ## Multiple Applications (OR condition)
109
+
110
+ **Requires fusuma v3.12.0 or later**
111
+
112
+ You can specify multiple applications using array format. The gesture will be triggered when **any** of the listed applications is active.
113
+
114
+ ```yaml
115
+ ---
116
+ context:
117
+ application:
118
+ - Google-chrome
119
+ - Firefox
120
+ swipe:
121
+ 3:
122
+ left:
123
+ sendkey: 'LEFTALT+RIGHT'
124
+ right:
125
+ sendkey: 'LEFTALT+LEFT'
126
+ up:
127
+ sendkey: 'LEFTCTRL+T'
128
+ down:
129
+ sendkey: 'LEFTCTRL+W'
130
+ ```
131
+
132
+ ## Combining with Other Context Plugins (AND condition)
133
+
134
+ **Requires fusuma v3.12.0 or later**
135
+
136
+ You can combine `application` with other context conditions. When multiple keys are specified under `context:`, **all** conditions must be satisfied (AND logic).
137
+
138
+ For example, with [fusuma-plugin-thumbsense](https://github.com/iberianpig/fusuma-plugin-thumbsense):
139
+
140
+ ```yaml
141
+ ---
142
+ context:
143
+ thumbsense: true
144
+ application:
145
+ - Alacritty
146
+ - Gnome-terminal
147
+ swipe:
148
+ 3:
149
+ up:
150
+ sendkey: 'LEFTSHIFT+LEFTCTRL+T'
151
+ down:
152
+ sendkey: 'LEFTSHIFT+LEFTCTRL+W'
153
+ ```
154
+
155
+ In this example, the gesture is only triggered when:
156
+ 1. Thumbsense mode is active (finger is touching the touchpad)
157
+ 2. AND the active application is either Alacritty or Gnome-terminal
158
+
159
+ This AND logic works with any context plugin that provides context conditions
160
+
100
161
  ## Contributing
101
162
 
102
163
  Bug reports and pull requests are welcome on GitHub at https://github.com/iberianpig/fusuma-plugin-appmatcher. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
103
164
 
165
+ ### Help Wanted: Support for Other Wayland Compositors
166
+
167
+ Currently, this plugin supports X11, GNOME Wayland, Hyprland, and COSMIC. We'd love to expand support to other Wayland compositors (Sway, KDE Plasma, wlroots-based compositors, etc.).
168
+
169
+ If you're using an unsupported compositor:
170
+ - Please [open an issue](https://github.com/iberianpig/fusuma-plugin-appmatcher/issues) to let us know
171
+ - Help with testing and feedback is greatly appreciated
172
+
104
173
  ## License
105
174
 
106
175
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "json"
5
+ require_relative "user_switcher"
6
+ require "fusuma/multi_logger"
7
+ require "fusuma/custom_process"
8
+
9
+ module Fusuma
10
+ module Plugin
11
+ module Appmatcher
12
+ # Search Active Window's Name for COSMIC desktop via cos-cli (third-party).
13
+ # cos-cli must be installed separately:
14
+ # cargo install --git https://github.com/estin/cos-cli
15
+ class Cosmic
16
+ include UserSwitcher
17
+
18
+ attr_reader :reader, :writer
19
+
20
+ # Search PATH in pure Ruby: the external `which` command is not
21
+ # installed on minimal systems (e.g. Arch containers).
22
+ # @return [Boolean]
23
+ def self.available?
24
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |dir|
25
+ path = File.join(dir, "cos-cli")
26
+ File.executable?(path) && !File.directory?(path)
27
+ end
28
+ end
29
+
30
+ def initialize
31
+ @reader, @writer = IO.pipe
32
+ end
33
+
34
+ def watch_start
35
+ as_user(proctitle: self.class.name.underscore) do |_user|
36
+ @reader.close
37
+ register_on_application_changed(Matcher.new)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def register_on_application_changed(matcher)
44
+ @writer.puts(matcher.active_application || "NOT FOUND")
45
+ matcher.on_active_application_changed { |name| notify(name) }
46
+ end
47
+
48
+ def notify(name)
49
+ @writer.puts(name)
50
+ rescue Errno::EPIPE
51
+ exit 0
52
+ rescue => e
53
+ MultiLogger.error e.message
54
+ exit 1
55
+ end
56
+
57
+ class Matcher
58
+ # @return [String, nil]
59
+ def active_application
60
+ extract_activated_app_id(fetch_info)
61
+ end
62
+
63
+ # @return [Array<String>]
64
+ def running_applications
65
+ state = fetch_info
66
+ return [] unless state
67
+ (state["apps"] || []).map { |a| a["app_id"] }.compact.uniq
68
+ end
69
+
70
+ # Sentinel value distinct from nil, so the first iteration always
71
+ # yields (otherwise initial nil == nil would skip yielding NOT FOUND).
72
+ UNSET = Object.new.freeze
73
+ private_constant :UNSET
74
+
75
+ def on_active_application_changed
76
+ last = UNSET
77
+ subscribe_state_change do |state|
78
+ app_id = extract_activated_app_id(state)
79
+ next if app_id == last
80
+ last = app_id
81
+ yield(app_id || "NOT FOUND")
82
+ end
83
+ rescue => e
84
+ MultiLogger.error "Cosmic subscription error: #{e.message}"
85
+ sleep 1
86
+ retry
87
+ end
88
+
89
+ private
90
+
91
+ # @return [Hash, nil]
92
+ def fetch_info
93
+ stdout, _stderr, status = Open3.capture3("cos-cli", "info", "--json")
94
+ return nil unless status.success? && !stdout.empty?
95
+ JSON.parse(stdout)
96
+ rescue JSON::ParserError, Errno::ENOENT
97
+ nil
98
+ end
99
+
100
+ # @param state [Hash, nil]
101
+ # @return [String, nil]
102
+ def extract_activated_app_id(state)
103
+ return nil unless state
104
+ apps = state["apps"] || []
105
+ focused = apps.find { |a| (a["state"] || []).include?("activated") }
106
+ focused && focused["app_id"]
107
+ end
108
+
109
+ def subscribe_state_change
110
+ # cos-cli serve is a stdio JSON-RPC server and exits on stdin EOF,
111
+ # so keep stdin open while reading notifications.
112
+ Open3.popen3("cos-cli", "serve") do |_stdin, stdout, stderr, _wait_thr|
113
+ stdout.each_line do |line|
114
+ msg = JSON.parse(line)
115
+ next unless msg["method"] == "state_change"
116
+ state = msg.dig("params", "state")
117
+ yield state if state
118
+ rescue JSON::ParserError => e
119
+ MultiLogger.warn "Failed to parse cos-cli message: #{e.message}"
120
+ end
121
+ # A clean serve exit must not stop the watcher silently:
122
+ # raise so on_active_application_changed resubscribes via retry.
123
+ raise "cos-cli serve exited: #{stderr.read}"
124
+ end
125
+ rescue Errno::ENOENT
126
+ MultiLogger.error "cos-cli command not found. Install with: cargo install --git https://github.com/estin/cos-cli"
127
+ raise
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "json"
5
+ require_relative "user_switcher"
6
+ require "fusuma/multi_logger"
7
+ require "fusuma/custom_process"
8
+
9
+ module Fusuma
10
+ module Plugin
11
+ module Appmatcher
12
+ # Search Active Window's Name for Hyprland
13
+ class Hyprland
14
+ include UserSwitcher
15
+
16
+ attr_reader :reader, :writer
17
+
18
+ def initialize
19
+ @reader, @writer = IO.pipe
20
+ end
21
+
22
+ # fork process and watch signal
23
+ # @return [Integer] Process id
24
+ def watch_start
25
+ as_user(proctitle: self.class.name.underscore) do |_user|
26
+ @reader.close
27
+ register_on_application_changed(Matcher.new)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def register_on_application_changed(matcher)
34
+ @writer.puts(matcher.active_application || "NOT FOUND")
35
+
36
+ matcher.on_active_application_changed do |name|
37
+ notify(name)
38
+ end
39
+ end
40
+
41
+ def notify(name)
42
+ @writer.puts(name)
43
+ rescue Errno::EPIPE
44
+ exit 0
45
+ rescue => e
46
+ MultiLogger.error e.message
47
+ exit 1
48
+ end
49
+
50
+ # Look up application name using hyprctl
51
+ class Matcher
52
+ ACTIVEWINDOW_EVENT = "activewindow"
53
+
54
+ # @return [Array<String>]
55
+ def running_applications
56
+ output = `hyprctl clients -j 2>/dev/null`
57
+ return [] if output.empty?
58
+ JSON.parse(output).map { |c| c["class"] }.compact.uniq
59
+ rescue JSON::ParserError
60
+ []
61
+ end
62
+
63
+ # @return [String, nil]
64
+ def active_application
65
+ output = `hyprctl -j activewindow 2>/dev/null`
66
+ return nil if output.empty? || output.strip == "{}"
67
+ JSON.parse(output)["class"]
68
+ rescue JSON::ParserError
69
+ nil
70
+ end
71
+
72
+ def on_active_application_changed
73
+ socket = connect_to_socket2
74
+ return unless socket
75
+
76
+ loop do
77
+ line = socket.gets
78
+ break unless line
79
+
80
+ event, data = line.chomp.split(">>", 2)
81
+ next unless event == ACTIVEWINDOW_EVENT
82
+
83
+ window_class, _window_title = data.split(",", 2)
84
+ yield(window_class.to_s.empty? ? "NOT FOUND" : window_class)
85
+ end
86
+ rescue Errno::ECONNRESET, Errno::EPIPE, IOError
87
+ # socket disconnected
88
+ ensure
89
+ socket&.close
90
+ end
91
+
92
+ private
93
+
94
+ # @return [String, nil]
95
+ def find_socket_path
96
+ instance_sig = ENV["HYPRLAND_INSTANCE_SIGNATURE"]
97
+ return nil unless instance_sig
98
+
99
+ xdg_runtime = ENV.fetch("XDG_RUNTIME_DIR", "/tmp")
100
+ [
101
+ File.join(xdg_runtime, "hypr", instance_sig, ".socket2.sock"),
102
+ File.join("/tmp", "hypr", instance_sig, ".socket2.sock")
103
+ ].find { |p| File.exist?(p) }
104
+ end
105
+
106
+ # @return [UNIXSocket, nil]
107
+ def connect_to_socket2
108
+ path = find_socket_path
109
+ return nil unless path
110
+ UNIXSocket.new(path)
111
+ rescue Errno::ENOENT, Errno::ECONNREFUSED
112
+ nil
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -3,7 +3,7 @@
3
3
  module Fusuma
4
4
  module Plugin
5
5
  module Appmatcher
6
- VERSION = "0.10.0"
6
+ VERSION = "0.12.0"
7
7
  end
8
8
  end
9
9
  end
@@ -5,6 +5,8 @@ require "fusuma/plugin/appmatcher/version"
5
5
  require "fusuma/plugin/appmatcher/x11"
6
6
  require "fusuma/plugin/appmatcher/gnome_extension"
7
7
  require "fusuma/plugin/appmatcher/gnome_extensions/installer"
8
+ require "fusuma/plugin/appmatcher/hyprland"
9
+ require "fusuma/plugin/appmatcher/cosmic"
8
10
  require "fusuma/plugin/appmatcher/unsupported_backend"
9
11
 
10
12
  module Fusuma
@@ -30,6 +32,18 @@ module Fusuma
30
32
  MultiLogger.warn "$ fusuma-appmatcher --install-gnome-extension"
31
33
  MultiLogger.warn ""
32
34
  end
35
+ when /Hyprland/i
36
+ return Hyprland if hyprland_available?
37
+ when /COSMIC/i
38
+ if Cosmic.available?
39
+ return Cosmic
40
+ else
41
+ MultiLogger.warn "cos-cli command not found"
42
+ MultiLogger.warn "Please install cos-cli to use appmatcher with COSMIC desktop:"
43
+ MultiLogger.warn ""
44
+ MultiLogger.warn " $ cargo install --git https://github.com/estin/cos-cli"
45
+ MultiLogger.warn ""
46
+ end
33
47
  end
34
48
  end
35
49
 
@@ -44,6 +58,17 @@ module Fusuma
44
58
  def xdg_current_desktop
45
59
  ENV.fetch("ORIGINAL_XDG_CURRENT_DESKTOP", ENV.fetch("XDG_CURRENT_DESKTOP", ""))
46
60
  end
61
+
62
+ def hyprland_available?
63
+ instance_sig = ENV["HYPRLAND_INSTANCE_SIGNATURE"]
64
+ return false unless instance_sig
65
+
66
+ xdg_runtime = ENV.fetch("XDG_RUNTIME_DIR", "/tmp")
67
+ [
68
+ File.join(xdg_runtime, "hypr", instance_sig, ".socket2.sock"),
69
+ File.join("/tmp", "hypr", instance_sig, ".socket2.sock")
70
+ ].any? { |p| File.exist?(p) }
71
+ end
47
72
  end
48
73
  end
49
74
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fusuma-plugin-appmatcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - iberianpig
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-20 00:00:00.000000000 Z
11
+ date: 2026-06-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rexml
@@ -68,12 +68,14 @@ files:
68
68
  - exe/fusuma-appmatcher
69
69
  - fusuma-plugin-appmatcher.gemspec
70
70
  - lib/fusuma/plugin/appmatcher.rb
71
+ - lib/fusuma/plugin/appmatcher/cosmic.rb
71
72
  - lib/fusuma/plugin/appmatcher/gnome_extension.rb
72
73
  - lib/fusuma/plugin/appmatcher/gnome_extensions/appmatcher45@iberianpig.dev/extension.js
73
74
  - lib/fusuma/plugin/appmatcher/gnome_extensions/appmatcher45@iberianpig.dev/metadata.json
74
75
  - lib/fusuma/plugin/appmatcher/gnome_extensions/appmatcher@iberianpig.dev/extension.js
75
76
  - lib/fusuma/plugin/appmatcher/gnome_extensions/appmatcher@iberianpig.dev/metadata.json
76
77
  - lib/fusuma/plugin/appmatcher/gnome_extensions/installer.rb
78
+ - lib/fusuma/plugin/appmatcher/hyprland.rb
77
79
  - lib/fusuma/plugin/appmatcher/unsupported_backend.rb
78
80
  - lib/fusuma/plugin/appmatcher/user_switcher.rb
79
81
  - lib/fusuma/plugin/appmatcher/version.rb