-
Notifications
You must be signed in to change notification settings - Fork 1
/
ctoolu
executable file
·234 lines (200 loc) · 5.8 KB
/
ctoolu
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
#! /usr/bin/env ruby
require 'rubygems'
require 'dbus'
require 'gtk2'
require 'pathname'
require 'yaml'
HOME = ENV['HOME']
# actual url is constructed as url % pattern.match(text).captures
CtooluAction = Struct.new :label, :pattern, :url, :commands
class CtooluCommand < Struct.new :label, :command, :keep_output
def execute(captures)
substituted = command % captures
if keep_output
$clipboard.Set `#{substituted}`
else
run_in_background substituted
end
end
private
def run_in_background(command)
child_pid = fork
if child_pid
# reap and ignore the return status, don't leave a zombie around
Process.detach(child_pid)
else
exec command
end
end
end
# "X Desktop Group" is a former name of freedesktop.org.
# API modeled after python-xdg, because xdg.gem sucks.
module XDG
XDG_DATA_HOME = ENV.fetch('XDG_DATA_HOME', "#{HOME}/.local/share")
XDG_DATA_DIRS = [XDG_DATA_HOME] +
ENV.fetch('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':')
XDG_CONFIG_HOME = ENV.fetch('XDG_CONFIG_HOME', "#{HOME}/.config")
XDG_CONFIG_DIRS = [XDG_CONFIG_HOME] +
ENV.fetch('XDG_CONFIG_DIRS', '/etc/xdg').split(':')
# @return [Array<String>] paths
def load_data_paths(resource)
paths = XDG_DATA_DIRS.map { |dir| Pathname.new(dir) + resource }
paths.select { |p| p.absolute? && p.exist? }
end
module_function :load_data_paths
# @return [Array<String>] paths
def load_config_paths(resource)
paths = XDG_CONFIG_DIRS.map { |dir| Pathname.new(dir) + resource }
paths.select { |p| p.absolute? && p.exist? }
end
module_function :load_config_paths
# @return [String, nil]
def load_first_config(resource)
# correctly returns nil for an empty list
load_config_paths(resource).first
end
module_function :load_first_config
end # module XDG
# USAGE:
# m = CtooluMenu.new
# actions.each do |a|
# m.add_group(a.label) { a.execute }
# a.commands.each do |c|
# m.add_item(c.label) { c.execute }
# end
# end
# m.show
#
# This will work even for empty lists of actions (no menu) or commands.
class CtooluMenu
# implementation:
# http://ruby-gnome2.sourceforge.jp/hiki.cgi?Gtk%3A%3AMenu
def initialize
@menu = Gtk::Menu.new
@seen_group = false
end
def add_group(label, &action)
if @seen_group
@menu.append Gtk::MenuItem.new(nil) # separator
end
@seen_group = true
add_item(label, &action)
end
def add_item(label, &action)
item = Gtk::MenuItem.new(label)
item.signal_connect("activate", &action)
@menu.append item
end
def show
if @seen_group
add_group("_Cancel") do
# do nothing
end
@menu.show_all
parent_shell = parent_item = nil
mouse_button = 1
activation_time = 0 # Time.now.to_i did not work
@menu.popup(parent_shell, parent_item, mouse_button, activation_time)
end
end
end
class CtooluConfig
include Singleton
attr_reader :config
def initialize
config_filename = XDG.load_first_config('ctoolu.yaml')
if config_filename.nil?
fail "Configuration ctoolu.yaml not found. Try `strace -e file #{$0}`"
end
@config = YAML.load_file(config_filename)
end
def self.text_source
self.instance.config["text_source"]
end
def self.auto_activate
self.instance.config["auto_activate"]
end
end
class Ctoolu < DBus::Object
def initialize(path)
super(path)
@actions = load_actions
end
# returns an Array of all CtooluActions.
# Directories are searched in order of preference:
# a file in the systemwide directory will be ignored if an equally named
# file is present in the user's directory.
def load_actions
seen_basenames = Set.new
actions = []
try_dirs = XDG.load_data_paths "ctoolu"
try_dirs.each do |dir|
file_names = Dir.glob(File.join(dir, "*.yaml"))
file_names.each do |fn|
basename = File.basename(fn)
if seen_basenames.include?(basename)
puts "SKIP #{fn}" if $DEBUG
next
end
puts "LOAD #{fn}" if $DEBUG
seen_basenames.add(basename)
actions += YAML.load_file(fn)
end
end
if actions.empty?
fail "No rules found. Try `strace -e file #{$0}`"
end
actions
end
dbus_interface "net.vidner.Ctoolu" do
dbus_method :Activate do
look($clipboard.Get[0])
end
# ruby-dbus will one day translate MethodNames to method_names automatically
dbus_method :Look, "in text:s" do |text|
look(text)
end
end
def look(text)
menu = CtooluMenu.new
@actions.each do |action|
match_data = action.pattern.match(text)
next unless match_data
menu.add_group("#{action.label} (#{match_data})") do
action.url ||= "%s"
$clipboard.Set(action.url % match_data.captures)
end
action.commands ||= []
action.commands.each do |command|
menu.add_item(command.label) do
command.execute match_data.captures
end
end
end
menu.show
end
end
bus = DBus::SessionBus.instance
bus.glibize
ctoolu = Ctoolu.new("/net/vidner/Ctoolu")
service = bus.request_service "net.vidner.Ctoolu"
service.export ctoolu
clipboard_relay_svc = bus["net.vidner.ClipboardRelay"]
case CtooluConfig.text_source
when "primary"
clipboard_path = "/net/vidner/ClipboardRelay/Primary"
when "clipboard"
clipboard_path = "/net/vidner/ClipboardRelay/Clipboard"
else
fail "Unknown CtooluConfig.text_source '#{CtooluConfig.text_source}'"
end
clipboard = clipboard_relay_svc.object(clipboard_path)
clipboard.introspect
clipboard.default_iface = "net.vidner.ClipboardRelay"
if CtooluConfig.auto_activate
# FIXME there is no ProxyObject#interfacce
# FIXME ProxyObjectInterface#on_signal does not mimic ProxyObject#on_signal
clipboard.on_signal("Changed") { |text| ctoolu.look(text) }
end
$clipboard = clipboard
Gtk.main