xdo 0.0.4

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.
@@ -0,0 +1,11 @@
1
+ =Deprecations
2
+ The following method uses are _deprecated_ and will be removed in the next minor version bump. They show a warning if you use them unless otherwise stated.
3
+
4
+ * XWindow.desktop_name= is obsolete now and has noo effect anymore.
5
+ * XWindow.desktop_name is obsolete now. It always returns "x-nautilus-desktop".
6
+ * XWindow.focus_desktop has been deprecated. Use XWindow.unfocus instead.
7
+ * All Clipboard module methods take a hash in order to specify which clipboards to interact with now take the symbols directly via a rest argument.
8
+ * All Mouse module methods that took constant values now accept symbols. The old behaviour is deprecated now.
9
+ * All XWindow class methods that take a hash in order to specify which windows to search now take the symbols directly via a rest argument.
10
+ * All XWindow class methods that take a string as the window to search for are now accepting Regular Expressions. That means, in later version you have to pass in a regexp if you don't want to look for an entire title at once, i.e. <tt>"edit"</tt> won't find a "gedit" window, but <tt>/edit/</tt> will. <b>This doesn't show a warning. </b>
11
+ * XWindow.from_name has been deprecated. The old functionality (i.e. the ability to specify all search parameters) is now provided by the XWindow.from_search method. XWindow.from_name will be used as an alias for XWindow.from_title in the next version.
@@ -0,0 +1,33 @@
1
+ =History of the xdo gem
2
+ This file shows the history of the xdo gem
3
+ from it's very beginning. Important changes in the
4
+ API are marked <b>bold</b>. Bugfixes are <i>italic</i>.
5
+
6
+ ==0.0.4
7
+ * <b>Several methods now accept a +sync+ parameter wrapping +xdotool+'s <tt>--sync</tt> option. </b>
8
+ * <b>Added XWindow.unfocus</b>
9
+ * <b>Added XWindow.root_id</b>
10
+ * <b>Added XWindow.from_root</b>
11
+ * <b>Added XWindow.from_null</b>
12
+ * Removed the internal SmallScanner class. We're now using StringScanner directly.
13
+ * All methods in the Mouse module now accept symbols for the +button+ parameter.
14
+ * <i>XWindow#exists? aways returned false. This has been fixed.</i>
15
+ * <i>XWindow#visible? always returned false. This has been fixed.</i>
16
+ * <b>Added XWindow#to_i and XWindow#to_s.</b>
17
+ * <b>Added XWindow#zero? and XWindow#nonzero?.</b>
18
+ * All methods in the Keyboard module that accept a +w_id+ parameter now can take XWindow objects.
19
+ * Removed Keyboard.sequence_escape.
20
+ * Several deprecations. See DEPRECATE.rdoc.
21
+ * <b>Added XWindow.from_title.</b>
22
+
23
+ ==0.0.3
24
+ * <i>Some XWindow instance methods (like #close!) showed wrong error messages due to the redundance of Kernel#raise with XWindow#raise. This has been fixed. </i>
25
+ * If run alone, the "test_xwindow.rb" test file failed with obscure errors. Fixed.
26
+
27
+ ==0.0.2
28
+ * Replaced fork{system("gedit")} in full_demo.rb with spawn("gedit")
29
+ * Removed "require 'pp'" from keyboard.rb
30
+ * <b>Added the Simulatable mixin</b>
31
+ * <b>Corrected window id usage for the XDo::Keyboard module</b>
32
+ * Added a test for the window id usage
33
+ * Made the test-unit gem a development dependency
@@ -0,0 +1,53 @@
1
+ --
2
+ This file is part of Xdo.
3
+ Copyright © 2009, 2010 Marvin Gülker
4
+ Initia in potestate nostra sunt, de eventu fortuna iudicat.
5
+ ++
6
+ =XDo
7
+ XDo is a library to simulate keyboard and mouse input and manipulating windows on the X server.
8
+ It's wrapped around the command-line tools xdotool[http://www.semicomplete.com/projects/xdotool/],
9
+ xsel[http://linux.die.net/man/1/xsel], xwininfo[http://linux.die.net/man/1/xwininfo], eject[http://linux.die.net/man/1/eject] and xkill[http://linux.die.net/man/1/xkill],
10
+ so you will need to have them installed if you want to use Xdo (even if xwininfo, eject and xkill are usually already installed).
11
+ If not, try to install them via your favourite packaging manager.
12
+ After they're installed, install XDo via RubyGems:
13
+ sudo gem install xdo
14
+ ==Usage
15
+ #Require some of XDo's files
16
+ require "xdo/keyboard"
17
+ require "xdo/mouse"
18
+ require "xdo/xwindow"
19
+ #Move the cursor
20
+ XDo::Mouse.move(100, 100)
21
+ #Simulate text (with special escape sequences!)
22
+ XDo::Keyboard.simulate("This is{TAB}text.")
23
+ #Some sequences can be shortened:
24
+ XDo::Keyboard.simulate("This ist\ttext.")
25
+ #And this will move a window containing the string "gedit",
26
+ #unless it's maximized.
27
+ win = XDo::XWindow.from_title(/gedit/)
28
+ win.move(200, 200)
29
+ ==Files
30
+ You can require the following files in your projects:
31
+ * xdo/clipboard: Clipboard access
32
+ * xdo/drive: Get control of CD/DVD devices
33
+ * xdo/keyboard: Pretty self-explaining
34
+ * xdo/mouse: Automate the mouse
35
+ * xdo/xwindow: Manipulate windows in various ways
36
+ As an helpful extra, I created an executable ruby file "xinfo.rb". Thanks to RubyGems,
37
+ you can start this GUI tool right from the command line by typing:
38
+ xinfo.rb
39
+ It's by far not perfect, maybe not even good, but I think it can be useful sometimes
40
+ (you will need to have wxRuby installed, try <tt>sudo gem install wxruby-ruby19</tt>).
41
+ If you're looking for a more professional program, try the "X window information" tool.
42
+ ==Notes
43
+ * If your +xdotool+ seems to reject the --window option, you are not using the current version. Try building the newest one from the source.
44
+ * I recommand the "X window information" tool to get infos about your windows if you aren't satisfied by the xinfo.rb shipped with this package.
45
+ ==Fairly incomplete
46
+ * I'm sure there are several things I didn't notice that can be automated somehow. If you know about, email me! Please add a description of the possibilities and a sample script.
47
+ * Another interesting thing are the samples. There are many Linux distrubitions out there, and even many of them rely on X. I cannot test with another than a recent Ubuntu machine, but if you want to contribute and send samples for another OS, I want to encourage you to - I surely won't reject your work. :-)
48
+ ==License/Copyright
49
+ Copyright © 2009, 2010 Marvin Gülker
50
+ This library is free software; you may redistribute it and/or modify it
51
+ under the terms of Ruby's license (see http://www.ruby-lang.org/en/LICENSE.txt).
52
+ You can contact me at sutniuq ät gmx Dot net.
53
+ Initia in potestate nostra sunt, de eventu fortuna iudicat.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.4
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env ruby
2
+ #Encoding: UTF-8
3
+ #This file is part of Xdo.
4
+ #Copyright © 2009, 2010 Marvin Gülker
5
+ # Initia in potestate nostra sunt, de eventu fortuna iudicat.
6
+ #
7
+ #This program displays information about the currently selected window
8
+ #and the mouse. The displayed infos are updated every 1/2 second,
9
+ #but set XInfo::UPDATE_TIME to another value if you'd like to change that.
10
+
11
+ require "logger"
12
+ require "wx"
13
+
14
+ require_relative("../lib/xdo/xwindow")
15
+ require_relative("../lib/xdo/mouse")
16
+
17
+ #Class that retrieves the information of windows.
18
+ class InfoGetter
19
+
20
+ attr_reader :act_win
21
+ attr_reader :cursorpos
22
+
23
+ def initialize
24
+ @act_win = XDo::XWindow.from_active
25
+ @cursorpos = XDo::Mouse.position
26
+ end
27
+
28
+ def update
29
+ @act_win = XDo::XWindow.from_active
30
+ @cursorpos = XDo::Mouse.position
31
+ end
32
+
33
+ end
34
+
35
+ class XInfoFrame < Wx::Frame
36
+ include Wx
37
+
38
+ #Time intervall of updating the infos, in milliseconds.
39
+ #Default is 500, which is 1/2 second.
40
+ UPDATE_TIME = 500
41
+
42
+ def initialize(parent = nil)
43
+ super(parent, title: "XInfo", size: Size.new(300, 400), style: DEFAULT_FRAME_STYLE | STAY_ON_TOP)
44
+ self.background_colour = NULL_COLOUR
45
+ THE_APP.log.debug("Creating controls")
46
+ create_controls
47
+ THE_APP.log.debug("Setting up timer")
48
+ create_updater
49
+ end
50
+
51
+ private
52
+
53
+ def create_controls
54
+ StaticText.new(self, -1, "Title of active window: ", Point.new(20, 20))
55
+ @title = TextCtrl.new(self, pos: Point.new(20, 50), size: Size.new(260, 24), style: TE_READONLY)
56
+ StaticText.new(self, -1, "ID of active window: ", Point.new(20, 80))
57
+ @id = TextCtrl.new(self, -1, "", Point.new(20, 110), Size.new(260, 24), TE_READONLY)
58
+ StaticText.new(self, -1, "Absoloute and relative upper-left coords: ", Point.new(20,140))
59
+ StaticText.new(self, -1, "Abs: ", Point.new(20, 173))
60
+ @abs_xy = TextCtrl.new(self, -1, "", Point.new(60, 170), Size.new(70, 24), TE_READONLY)
61
+ StaticText.new(self, -1, "Rel: ", Point.new(150, 173))
62
+ @rel_xy = TextCtrl.new(self, -1, "", Point.new(190, 170), Size.new(70, 24), TE_READONLY)
63
+ StaticText.new(self, -1, "Size: ", Point.new(20, 200))
64
+ StaticText.new(self, -1, "Width: ", Point.new(20, 233))
65
+ @width = TextCtrl.new(self, -1, "", Point.new(65, 230), Size.new(70, 24), TE_READONLY)
66
+ StaticText.new(self, -1, "Height: ", Point.new(150, 233))
67
+ @height = TextCtrl.new(self, -1, "", Point.new(200, 230), Size.new(70, 24), TE_READONLY)
68
+
69
+ StaticText.new(self, -1, "Mouse position: ", Point.new(20, 300))
70
+ StaticText.new(self, -1, "X: ", Point.new(20, 333))
71
+ @x = TextCtrl.new(self, -1, "", Point.new(60, 330), Size.new(70, 24), TE_READONLY)
72
+ StaticText.new(self, -1, "Y: ", Point.new(150, 333))
73
+ @y = TextCtrl.new(self, -1, "", Point.new(190, 330), Size.new(70, 24), TE_READONLY)
74
+ end
75
+
76
+ def create_updater
77
+ @updater = InfoGetter.new
78
+ Timer.every(UPDATE_TIME) do
79
+ begin
80
+ @updater.update
81
+ @title.value = @updater.act_win.title
82
+ @id.value = @updater.act_win.id.inspect
83
+ @abs_xy.value = @updater.act_win.abs_position.join(", ")
84
+ @rel_xy.value = @updater.act_win.rel_position.join(", ")
85
+ ary = @updater.act_win.size
86
+ @width.value = ary[0].inspect
87
+ @height.value = ary[1].inspect
88
+ end
89
+
90
+ curpos = @updater.cursorpos
91
+ @x.value = curpos[0].inspect
92
+ @y.value = curpos[1].inspect
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ #Main App object of this program.
99
+ class XInfo < Wx::App
100
+ include Wx
101
+
102
+ #Logger.
103
+ attr_reader :log
104
+
105
+ def on_init
106
+ @log = Logger.new($stdout)
107
+ @log.level = Logger::INFO unless $VERBOSE || $DEBUG
108
+ @log.info("Started")
109
+ @log.debug "Creating main window"
110
+ @mainwindow = XInfoFrame.new
111
+ @mainwindow.show
112
+ end
113
+
114
+ def on_run
115
+ super
116
+ rescue => e
117
+ @log.fatal(e.class.name)
118
+ @log.fatal(e.message)
119
+ e.backtrace.each{|trace| @log.fatal(trace)}
120
+ message = "A #{e.class} occured: #{e.message} \n\nBacktrace: \n\n#{e.backtrace.join("\n")}"
121
+ message << "\n\nIf you want to contact me about the error, send an email to sutniuq ät gmx Dot net."
122
+ msgbox = MessageDialog.new(@mainwindow, message, $!.class.name, OK | ICON_ERROR)
123
+ msgbox.show_modal
124
+ raise
125
+ end
126
+
127
+ def on_exit
128
+ super
129
+ @log.info("Finished.")
130
+ end
131
+
132
+ end
133
+
134
+ if ARGV.include?("-h") or ARGV.include?("--help")
135
+ puts "This is xinfo.rb, from xdo #{XDo::VERSION}."
136
+ puts "xinfo.rb is a tool for inspecting GUI windows on X."
137
+ puts
138
+ puts "Copyright © 2010 Marvin Gülker"
139
+ puts "Licensed under the same terms as Ruby."
140
+ puts "You can find Ruby's license at http://www.ruby-lang.org/en/LICENSE.txt."
141
+ puts
142
+ puts "xinfo.rb doesn't understand many command-line options."
143
+ puts "There's -h for this message, -V for verbose output"
144
+ puts "and -d for debugging. -v shows XDo's version."
145
+ exit
146
+ elsif ARGV.include?("-v") or ARGV.include?("--version")
147
+ puts "This is xinfo.rb, from xdo #{XDo::VERSION}."
148
+ exit
149
+ end
150
+
151
+ $VERBOSE = true if ARGV.include?("-V")
152
+ $DEBUG = true if ARGV.include?("-d")
153
+
154
+ x = XInfo.new
155
+ x.main_loop
@@ -0,0 +1,37 @@
1
+ #Encoding: UTF-8
2
+ #This file is part of Xdo.
3
+ #Copyright © 2009 Marvin Gülker
4
+ # Initia in potestate nostra sunt, de eventu fortuna iudicat.
5
+
6
+ require "open3"
7
+ require "strscan"
8
+
9
+ #The namespace of this library.
10
+ module XDo
11
+
12
+ #The command to start xdotool.
13
+ XDOTOOL = "xdotool"
14
+
15
+ #The command to start xsel.
16
+ XSEL = "xsel"
17
+
18
+ #The command to start xwininfo.
19
+ XWININFO = "xwininfo"
20
+
21
+ #The command to start xkill.
22
+ XKILL = "xkill"
23
+
24
+ #The command to start eject.
25
+ EJECT = "eject"
26
+
27
+ #The version of this library.
28
+ VERSION = File.read(File.join(File.expand_path(File.dirname(__FILE__)), "..", "VERSION")).freeze
29
+
30
+ #Class for errors in this library.
31
+ class XError < StandardError
32
+ end
33
+
34
+ class ParseError < StandardError
35
+ end
36
+
37
+ end #module XDo
@@ -0,0 +1,208 @@
1
+ #Encoding: UTF-8
2
+ #This file is part of Xdo.
3
+ #Copyright © 2009, 2010 Marvin Gülker
4
+ # Initia in potestate nostra sunt, de eventu fortuna iudicat.
5
+
6
+ require_relative("../xdo")
7
+
8
+ module XDo
9
+
10
+ #A module for interaction with the X clipboard. Please note, that the X clipboard
11
+ #consists of three parts: The PRIMARY clipboard, the CLIPBOARD clipboard, and
12
+ #the SECONDARY clipboard. The clipboard you access normally via [CTRL]+[C]
13
+ #or by right-clicking and selecting "copy", is usually the CLIPBOARD clipboard (but that
14
+ #depends on the application you use). The three main methods of this module (#read, #write
15
+ #and #clear) take a list symbols of the clipboards to interact with. If you don't want to
16
+ #pass in the symbols, use the predefined read_xy, write_xy and clear_xy methods. They cannot
17
+ #access more than one clipboard at a time.
18
+ #The symbols for the clipboards are:
19
+ #[PRIMARY] :primary
20
+ #[SECONDARY] :secondary
21
+ #[CLIPBOARD] :clipboard
22
+ #You cannot store complex objects like images via this interface, only strings. However,
23
+ #you could translate an image into a string (packed pixels maybe?) and put that on the
24
+ #clipboard -- for your own application this may be fine, but it won't magically allow
25
+ #a user to paste that image into a graphics program.
26
+ #
27
+ #The +xsel+ program used by this module is quite outdated. As far as I can see, it's
28
+ #last update happened in 2002 and since I do not believe that software exists that
29
+ #won't break over a period of 8 years without a single modification while updating systems I'm about to
30
+ #switch to a newer one. +xclip+ is likely, but that one got it's last update in early
31
+ #2009...
32
+ module Clipboard
33
+
34
+ class << self
35
+
36
+ ##
37
+ # :singleton-method: read_primary
38
+ #Returns the contents of the PRIMARY clipboard.
39
+ #See #read for an explanation.
40
+
41
+ ##
42
+ # :singleton-method: read_clipboard
43
+ #Returns the contents of the CLIPBOARD clipboard.
44
+ #See #read for an explanation.
45
+
46
+ ##
47
+ # :singleton-method: read_secondary
48
+ #Returns the contents of the SECONDARY clipboard.
49
+ #See #read for an explanation.
50
+
51
+ ##
52
+ # :singleton-method: write_primary
53
+ #Writes to the PRIMARY clipboard.
54
+ #See #write for an explanation.
55
+
56
+ ##
57
+ # :singleton-method: write_clipboard
58
+ #Writes to the CLIPBOARD clipboard.
59
+ #See #write for an explanation.
60
+
61
+ ##
62
+ # :singleton-method: write_secondary
63
+ #Writes to the SECONDARY clipboard.
64
+ #See #write for an explanation.
65
+
66
+ ##
67
+ # :singleton-method: clear_primary
68
+ #Clears the PRIMARY clipboard.
69
+ #See #clear for an explanation.
70
+
71
+ ##
72
+ # :singleton-method: clear_clipboard
73
+ #Clears the CLIPBOARD clipboard.
74
+ #See #clear for an explanation.
75
+
76
+ ##
77
+ # :singleton-method: clear_secondary
78
+ #Clears the SECONDARY clipboard.
79
+ #See #clear for an explanation.
80
+
81
+ #Reads text from a X clipboard.
82
+ #===Parameters
83
+ #[<tt>*from</tt>] (<tt>:clipboard</tt>, <tt>:primary</tt>, <tt>:secondary</tt>) Specifies from which clipboards you want to read (in 70% of all cases you want to read from <tt>:clipboard</tt>).
84
+ #===Return value
85
+ #A hash of form
86
+ # {:clip_sym => "clipboard_content"}
87
+ #If you didn't pass any arguments to #read, the hash will contain keys for
88
+ #all clipboard, i.e. for <tt>:clipboard</tt>, <tt>:primary</tt> and <tt>:secondary</tt>.
89
+ #If you did, only those symbols will be included you passed. See
90
+ #the _Example_ section for an example of this.
91
+ #===Example
92
+ # XDo::Clipboard.read #| {:clipboard => "...", :primary => "...", :secondary => "..."}
93
+ # XDo::Clipboard.read(:primary) #| {:primary => "..."}
94
+ # XDo::Clipboard.read(:clipboard, :secondary) #| {clipboard => "...", :secondary => "..."}
95
+ #===Remarks
96
+ #You could also use one of the read_* methods for convenience.
97
+ def read(*from)
98
+ if from.first.kind_of? Hash
99
+ warn("#{caller.first}: Deprecation warning: Use symbols as a rest argument now!")
100
+ from = from.first.keys
101
+ end
102
+ from.concat([:clipboard, :primary, :secondary]) if from.empty?
103
+
104
+ hsh = {}
105
+ hsh[:primary] = `#{XSEL}` if from.include? :primary
106
+ hsh[:clipboard] = `#{XSEL} -b` if from.include? :clipboard
107
+ hsh[:secondary] = `#{XSEL} -s` if from.include? :secondary
108
+ hsh
109
+ end
110
+
111
+
112
+ #Writes text to a X clipboard.
113
+ #===Parameters
114
+ #[<tt>*to</tt>] (<tt>:clipboard</tt>) Specifies to what clipboards you want to wrote to.
115
+ #===Return value
116
+ #The text written.
117
+ #===Example
118
+ # XDo::Clipboard.write("I love Ruby") #You can now paste this via [CTRL] + [V]
119
+ # XDo::Clipboard.write("I love Ruby", :primary) #You can now paste this via a middle-mouse-button click
120
+ # XDo::Clipboard.write("I love Ruby", :clipboard, :primary) #Both of the above
121
+ #===Remarks
122
+ #You could also use one of the write_* methods for convenience.
123
+ def write(text, *to)
124
+ if to.first.kind_of? Hash
125
+ warn("#{caller.first}: Deprecation warning: Use symbols as a rest argument now!")
126
+ to = to.first.keys
127
+ end
128
+ to << :clipboard if to.empty?
129
+
130
+ IO.popen("xsel -i", "w"){|io| io.write(text)} if to.include? :primary
131
+ IO.popen("xsel -b -i", "w"){|io| io.write(text)} if to.include? :clipboard
132
+ IO.popen("xsel -s -i", "w"){|io| io.write(text)} if to.include? :secondary
133
+ text
134
+ end
135
+
136
+ #Appends text to a X clipboard.
137
+ #===Parameters
138
+ #[+text+] The text to append.
139
+ #[<tt>*to</tt>] (<tt>:clipboard</tt>) The clipboards to which you want to append.
140
+ #===Return value
141
+ #Undefined.
142
+ #===Example
143
+ # XDo::Clipboard.write("I love ")
144
+ # XDo::Clipboard.append("Ruby")
145
+ # puts XDo::Clipboard.read(:clipboard)[:clipboard] #=> I love Ruby
146
+ #
147
+ # XDo::Clipboard.write("I love", :primary)
148
+ # XDo::Clipboard.append("Ruby", :primary, :clipboard)
149
+ # #If you now paste via [CTRL] + [V], you'll get 'Ruby'. If you
150
+ # #paste via the middle mouse button, you'll get 'I love Ruby'
151
+ # #(Assuming you didn't execute the first block of code, of course).
152
+ def append(text, *to)
153
+ if to.first.kind_of? Hash
154
+ warn("#{caller.first}: Deprecation warning: Use symbols as a rest argument now!")
155
+ to = to.first.keys
156
+ end
157
+ to << :clipboard if to.empty?
158
+
159
+ IO.popen("xsel -a -i", "w"){|io| io.write(text)} if to.include? :primary
160
+ IO.popen("xsel -b -a -i", "w"){|io| io.write(text)} if to.include? :clipboard
161
+ IO.popen("xsel -s -a -i", "w"){|io| io.write(text)} if to.include? :secondary
162
+ end
163
+
164
+ #Clears the specified clipboards.
165
+ #===Parameters
166
+ #[<tt>*clips</tt>] (<tt>:primary</tt>, <tt>:clipboard</tt>, <tt>:secondary</tt>) The clipboards you want to clear.
167
+ #===Return value
168
+ #nil.
169
+ #===Example
170
+ # XDo::Clipboard.write("I love Ruby")
171
+ # XDo::Clipboard.clear
172
+ # #Nothing can be pasted anymore
173
+ #
174
+ # XDo::Clipboard.write("I love Ruby", :clipboard, :primary)
175
+ # XDo::Clipboard.clear(:primary)
176
+ # #You can still paste via [CTRL] + [V], but not with the middle mouse button
177
+ def clear(*clips)
178
+ if clips.first.kind_of? Hash
179
+ warn("#{caller.first}: Deprecation warning: Use symbols as a rest argument now!")
180
+ clips = clips.first.keys
181
+ end
182
+ clips.concat([:primary, :clipboard, :secondary]) if clips.empty?
183
+
184
+ `#{XSEL} -c` if clips.include? :primary
185
+ `#{XSEL} -b -c` if clips.include? :clipboard
186
+ `#{XSEL} -s -c` if clips.include? :secondary
187
+ nil
188
+ end
189
+
190
+ [:primary, :clipboard, :secondary].each do |sym|
191
+
192
+ define_method(:"read_#{sym}") do
193
+ read(sym)[sym]
194
+ end
195
+
196
+ define_method(:"write_#{sym}") do |text|
197
+ write(text, sym)
198
+ end
199
+
200
+ define_method(:"clear_#{sym}") do
201
+ clear(sym)
202
+ end
203
+
204
+ end
205
+
206
+ end
207
+ end
208
+ end