mailmate 1.1.0 → 1.2.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: 5e25504c9cfa00a8d2c0c94e387b593c9151a66981150c17e1fbd328d531707d
4
- data.tar.gz: 4f0ac4ca053f30eed50305e2f333e9969871bbf02565115c2a72dacb0072c023
3
+ metadata.gz: b986acdac48e1c98c6cc80ae0cc1686af00161a5de3cc17b0946ea09c43b17e1
4
+ data.tar.gz: 6867d4736f7c4118ea775c60b108a36dc76a5234312b20a36cb5c60070233712
5
5
  SHA512:
6
- metadata.gz: 8953a409f2855ca1b04a1f4c574c73c4a724e29398d5a5951423b61fd0313f06a21242581219d14f1de3c28436d777f09ee1289f5cb69ca02deeb70bedb8a5ef
7
- data.tar.gz: b4dc5f74fa58a58c592ce125c2e42dce062ebbeb841284d74c878e12c6202856f2624f2da4adba6bc24a2919d150a1b8f5b54c302fd71507d1c9085d6d5e4c35
6
+ metadata.gz: 45d6fc1c66f4549fdf14a899bdba83d1b7d0510b5a20e9469e75f44d92e8b5285bd4cba548a2524d9561831cd34ee64cf3fe3239829afe8b4dc19aa27e4cdbda
7
+ data.tar.gz: 7b88d5bb8668614368c969d0e6ba10f14a444594b4fe55fdf6dc14bf536e35b29c8845818bff44b65f880c4c5a724d551b245c23c4f3ee56d78247043da1af9c
data/README.md CHANGED
@@ -60,6 +60,10 @@ mmopen 'message://%3Cabc%40example.com%3E'
60
60
 
61
61
  # Print the mid: URL instead of opening it (useful in pipelines)
62
62
  mmopen 183715 --print
63
+
64
+ # Spawn the viewer in the background — MailMate stays where it is and
65
+ # your keyboard focus is not stolen. Useful when scripting / cross-app.
66
+ mmopen 183715 --background # also: -g
63
67
  ```
64
68
 
65
69
  ### `mm-mailboxes` — list accounts and mailboxes
@@ -113,6 +117,11 @@ mm-modify 183715 archive --dry-run
113
117
 
114
118
  # Verify the new flags after acting
115
119
  mm-modify 183715 read --verify
120
+
121
+ # As of 1.2.0, `--verify` works in `--dry-run` mode too — pair them as a
122
+ # post-hoc state probe (run the action first, then re-run with --dry-run
123
+ # --verify to confirm the change took effect).
124
+ mm-modify 183715 read --dry-run --verify
116
125
  ```
117
126
 
118
127
  **Tip — batch your actions.** Doing all related changes in **one** `mm-modify` invocation is still worthwhile: one open/close cycle instead of two, and the chain runs deterministically without you having to think about ordering. Splitting is safe — `path_for` falls back to a filesystem glob if MailMate's `#source` index is briefly stale after a fast-move — just slower than batching.
@@ -148,11 +157,15 @@ The `mm` prefix is for tab completion: typing `mm<tab>` in a shell lists every c
148
157
 
149
158
  A few rough edges to be aware of:
150
159
 
151
- 1. **Non-move `mm-modify` actions still take over the UI.** Same-account `move` actions use a fast path — a direct `.eml` rename on disk — so they're silent and don't activate MailMate. Everything else (`read`, `flag`, `tag`, `archive`, `junk`, `delete`, etc.) still drives MailMate's UI: each invocation opens a message-viewer window via the `mid:` URL, runs AppleScript key-binding selectors against it, then closes the window. Two consequences:
152
- - **Focus is stolen.** When the `mid:` URL fires, macOS brings MailMate forward and the spawned message-viewer window takes keyboard focus. Anything you were typing into another app goes to MailMate instead.
153
- - **The close at the end can close the wrong window.** `mm-modify` ends by sending the standard "close window" keystroke. If focus has drifted (or another app's window has come forward in the meantime), that keystroke lands on **your** window your editor, your browser tab not MailMate's viewer.
160
+ 1. **Non-move `mm-modify` actions briefly spawn a MailMate viewer window.** Same-account `move` actions use a fast path — a direct `.eml` rename on disk — so they're entirely silent. Everything else (`read`, `flag`, `tag`, `archive`, `junk`, `delete`, etc.) drives MailMate's URL handler: each invocation opens a message-viewer window via the `mid:` URL in the background, runs AppleScript key-binding selectors against it, then closes the window.
161
+
162
+ **As of 1.2.0 this runs in the background.** MailMate is not brought to the foreground, your keyboard focus stays in whatever app you were using, and you can keep working — even during bulk loops. The viewer windows still appear briefly in MailMate's own window list before closing, but they don't take over your screen. The previous behavior (full-screen takeover, close-keystroke landing on the wrong window) is gone.
154
163
 
155
- For one-off changes this is just annoying; for a loop of hundreds of messages it makes the machine unusable while it runs. Batch multiple actions into one `mm-modify` invocation when you can — they share a single open/close cycle (or skip it entirely for pure-move). The `--keep-window` flag avoids the close-keystroke entirely if you don't mind cleaning up viewers manually.
164
+ Two residual caveats:
165
+ - **Don't bring MailMate to the foreground during a run.** If you click into MailMate or Cmd-Tab to it while `mm-modify` is mid-flight, MailMate's responder chain shifts and subsequent perform-selectors may misbehave. Wait for the run to finish.
166
+ - **MailMate must be running.** See #3.
167
+
168
+ The `--keep-window` flag leaves the spawned viewers open if you want to inspect them. Batch multiple actions into one `mm-modify` invocation when you can — they share a single open/close cycle (or skip it entirely for pure-move).
156
169
 
157
170
  2. **`eml-id` is machine-local; prefer `Message-ID:`.** The integer eml-id (also shown as MailMate's "Msg ID" column) is just the filename of the `.eml` on disk and differs on every install — copy/pasting an eml-id from your desktop to your laptop will refer to a different message (or none at all). For anything you want to keep, store the RFC `Message-ID:` header (which `mmmessage` prints) and pass that to the CLIs. The `mid:%3C<message-id>%3E` URL scheme works portably for the same reason.
158
171
 
@@ -162,6 +175,8 @@ A few rough edges to be aware of:
162
175
 
163
176
  ## Status
164
177
 
178
+ 1.2.0 — `mm-modify` no longer brings MailMate to the foreground and is roughly 8× faster on single-action invocations. Internally: the open call uses `open -g -a MailMate <url>` to keep MailMate in the background, and the fixed `--settle` sleeps are replaced by active waits (polling for the spawned viewer window to appear). `mmopen` gains a `--background` / `-g` flag for ad-hoc use. `mm-modify --verify` now works in `--dry-run` mode as a post-hoc state probe. `--settle` is preserved for backward compat; it now caps the active-wait timeout rather than fixing sleep duration.
179
+
165
180
  1.1.0 — `reverse_markdown` (and its transitive `nokogiri` dep) is now opt-in rather than auto-installed. Run `gem install reverse_markdown` if you want `mmmessage --markdown`; everything else is unchanged.
166
181
 
167
182
  1.0.0 — initial public release; API stable from this point. Breaking changes bump the major version going forward.
@@ -32,13 +32,20 @@ module Mailmate
32
32
  end
33
33
 
34
34
  # Open a URL in MailMate (used for `mid:` URLs to select a message).
35
+ # Uses `open -g -a MailMate` so the URL is dispatched to MailMate's URL
36
+ # handler without bringing MailMate to the foreground — keeping the
37
+ # user's keyboard focus where it was. The viewer window is still spawned
38
+ # for `mid:` URLs. The AppleScript-native alternative
39
+ # (`tell application "MailMate" to open location`) was tried but is
40
+ # synchronous and can hang on `mid:` URLs while MailMate processes the
41
+ # spawn — `open -g` is asynchronous and avoids the wait.
35
42
  def open_url(url)
36
43
  Mailmate::PlatformError.check_darwin!(component: "AppleScriptDriver") unless dry_run
37
44
  if dry_run
38
- @output.puts "DRY: open -a MailMate #{url.inspect}"
45
+ @output.puts "DRY: open -g -a MailMate #{url.inspect}"
39
46
  return
40
47
  end
41
- success = system("open", "-a", "MailMate", url)
48
+ success = system("/usr/bin/open", "-g", "-a", "MailMate", url)
42
49
  raise Error, "open command failed for #{url}" unless success
43
50
  end
44
51
 
@@ -98,9 +98,9 @@ module Mailmate
98
98
  mute Toggle mute state
99
99
  delete Delete (move to trash)
100
100
  BANNER
101
- o.on("--verify", "After running, print the message's current flags") { opts[:verify] = true }
101
+ o.on("--verify", "Print the message's current flags. Works with --dry-run as a post-hoc 'check state' probe (run the action first, then re-run with --dry-run --verify).") { opts[:verify] = true }
102
102
  o.on("--dry-run", "Print the actions; don't run") { opts[:dry_run] = true }
103
- o.on("--settle SECONDS", Float, "Sleep between operations (default 3.5)") { |s| opts[:settle] = s }
103
+ o.on("--settle SECONDS", Float, "Timeout for waiting for MailMate's viewer window to spawn after open_url (default 3.5)") { |s| opts[:settle] = s }
104
104
  o.on("--keep-window", "Don't close the spawned message-viewer window") { opts[:keep_window] = true }
105
105
  end
106
106
  parser.parse!(argv)
@@ -258,9 +258,15 @@ module Mailmate
258
258
 
259
259
  windows_before = driver.window_ids
260
260
  driver.open_url(mid_url)
261
- sleep(opts[:settle]) unless opts[:dry_run]
262
- new_windows = driver.window_ids - windows_before
263
-
261
+ # Active wait: poll for the new viewer window instead of sleeping a
262
+ # fixed `settle`. Windows typically spawn in 200-500ms; the previous
263
+ # fixed 3.5s sleep was wildly conservative. `--settle` now controls
264
+ # the timeout for this wait rather than the sleep duration.
265
+ new_windows = opts[:dry_run] ? [] : wait_for_new_window(driver, windows_before, timeout: opts[:settle])
266
+
267
+ # Send each action's selector without sleeping between them.
268
+ # AppleEvents queue per-app and process in order, so a subsequent
269
+ # `close window id N` will execute only after `perform` is committed.
264
270
  actions.each do |name, selector, args|
265
271
  case selector
266
272
  when :ensure_flagged, :ensure_not_flagged
@@ -271,16 +277,19 @@ module Mailmate
271
277
  $stdout.puts "#{name}: already #{want ? "flagged" : "not flagged"} — no-op"
272
278
  else
273
279
  driver.perform("toggleFlag:")
274
- sleep(opts[:settle]) unless opts[:dry_run]
275
280
  end
276
281
  else
277
282
  driver.perform(selector, *args)
278
- sleep(opts[:settle]) unless opts[:dry_run]
279
283
  end
280
284
  end
281
285
 
282
- if opts[:verify] && !opts[:dry_run]
283
- sleep(opts[:settle])
286
+ # --verify is now permitted in --dry-run mode so callers can use
287
+ # `mm-modify <id> <any-action> --dry-run --verify` as a post-hoc
288
+ # "what's the current flag state?" probe after a separate action run.
289
+ # In non-dry-run mode, a short fixed wait gives MailMate time to
290
+ # flush #flags before we read it back; in dry-run no wait is needed.
291
+ if opts[:verify]
292
+ sleep(1) unless opts[:dry_run]
284
293
  $stdout.puts "Flags now: #{current_flags(eml_id).inspect}"
285
294
  end
286
295
 
@@ -289,6 +298,20 @@ module Mailmate
289
298
  end
290
299
  end
291
300
 
301
+ # Poll for a new MailMate window appearing in `driver.window_ids` that
302
+ # isn't in `windows_before`. Returns the set of new window IDs, or an
303
+ # empty array if `timeout` elapses without a new window appearing.
304
+ def wait_for_new_window(driver, windows_before, timeout:, poll: 0.05)
305
+ deadline = Time.now + timeout
306
+ loop do
307
+ diff = driver.window_ids - windows_before
308
+ return diff unless diff.empty?
309
+ break if Time.now >= deadline
310
+ sleep(poll)
311
+ end
312
+ []
313
+ end
314
+
292
315
  def current_flags(eml_id)
293
316
  # AppleScript actions write the index asynchronously — bust just the
294
317
  # #flags cache to pick up the latest values without throwing away
@@ -44,26 +44,30 @@ module Mailmate
44
44
  return 0
45
45
  end
46
46
 
47
- system("/usr/bin/open", url)
47
+ cmd = ["/usr/bin/open"]
48
+ cmd << "-g" if opts[:background]
49
+ cmd << url
50
+ system(*cmd)
48
51
  $?.exitstatus
49
52
  end
50
53
 
51
54
  def parse_options(argv)
52
- opts = { print_only: false }
55
+ opts = { print_only: false, background: false }
53
56
  OptionParser.new do |o|
54
- o.banner = "Usage: mmopen <id> [--print]"
57
+ o.banner = "Usage: mmopen <id> [--print] [--background|-g]"
55
58
  o.separator ""
56
59
  o.separator "Open a MailMate message in MailMate's UI. <id> can be a local"
57
60
  o.separator "eml-id, an RFC Message-ID (with or without angle brackets), or"
58
61
  o.separator "a message://… or mid:… URL."
59
62
  o.on("--print", "Print the mid: URL instead of opening it (for piping)") { opts[:print_only] = true }
63
+ o.on("-g", "--background", "Open without bringing MailMate to the foreground") { opts[:background] = true }
60
64
  end.parse!(argv)
61
65
  opts
62
66
  end
63
67
 
64
68
  def usage_error(msg)
65
69
  warn "mmopen: #{msg}"
66
- warn "Usage: mmopen <id> [--print]"
70
+ warn "Usage: mmopen <id> [--print] [--background|-g]"
67
71
  2
68
72
  end
69
73
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mailmate
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mailmate
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Murphy-Dye