workroom 0.1.0 → 1.0.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: 709e14f9e8836400b840ad9772320ad6ddc9f2fbb163c800a0ba932b5d26ebe1
4
- data.tar.gz: 6334a7e2daa13faaf34e3d7931bfe1082ec752b456f88531b255d429515c99da
3
+ metadata.gz: 3583c94db1c58b26989df8e310ae2550460fd54fda21ba4bcad2837ee758b738
4
+ data.tar.gz: c3de17570cc26d2c477af06bac848e523b9d862024e2d26a042fb4876fec82ad
5
5
  SHA512:
6
- metadata.gz: 07ec6e20dfc896dec2a75cd11fe402b31c9dad2619ca323ac5b191ef23aa8cef681c742d41254356e62edef460aa5377c1e37d20c461d1e9f14caf15dec14658
7
- data.tar.gz: 8915fa7afb229916379e8c0343550734b25300f87d6a64d3b495b4509afe1112e976e3138720920c1881dbac02366aee68dee7bed4e8f26d6c27ed95d6d49e40
6
+ metadata.gz: 6435ed5642829932a7ce87b506655140cfc7418180a47e9695d8b33ae4be39f1c603115a2fef4538c456563412b3e1b62a9bf100ecbfecc74755a81842f8fa1f
7
+ data.tar.gz: e55f11b6f8cdb53343991f4b21cbbbd056a4ae6f718516346586a1943f34bbedc61638a6c5af744f2dfe6df9506014cc468e47bcabdd38e29a913d95601f7447
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Workroom
2
2
 
3
- Create and manage local development workrooms using [JJ](https://martinvonz.github.io/jj/) workspaces or git worktrees.
3
+ Create and manage local development workrooms using [Git](https://git-scm.com/) worktrees or [Jujutsu](https://martinvonz.github.io/jj/) workspaces.
4
4
 
5
- A workroom is an isolated copy of your project created as a sibling directory, allowing you to work on multiple branches or features simultaneously without stashing or switching contexts.
5
+ A workroom is an isolated copy of your project, allowing you to work on multiple branches or features simultaneously without stashing or switching contexts. Workrooms are created under a centralized directory (`~/workrooms` by default, configurable via `workrooms_dir` in `~/.config/workroom/config.json`).
6
6
 
7
7
  ## Installation
8
8
 
@@ -23,17 +23,29 @@ gem install workroom
23
23
  ## Requirements
24
24
 
25
25
  - Ruby >= 3.1
26
- - [JJ (Jujutsu)](https://martinvonz.github.io/jj/) or Git
26
+ - [JJ (Jujutsu)](https://martinvonz.github.io/jj/) or [Git](https://git-scm.com/)
27
27
 
28
28
  ## Usage
29
29
 
30
30
  ### Create a workroom
31
31
 
32
32
  ```bash
33
- workroom create my-feature
33
+ workroom create
34
34
  ```
35
35
 
36
- This creates a new workroom at `../my-feature` relative to your project root. Workroom automatically detects whether you're using JJ or Git and uses the appropriate mechanism (JJ workspace or git worktree).
36
+ A random friendly name (e.g. `swift-meadow`) is auto-generated. Workroom automatically detects whether you're using JJ or Git and uses the appropriate mechanism (JJ workspace or git worktree).
37
+
38
+ Alias: `workroom c`
39
+
40
+ ### List workrooms
41
+
42
+ ```bash
43
+ workroom list
44
+ ```
45
+
46
+ Lists all workrooms for the current project. When run from outside a known project, lists all workrooms grouped by parent project. When run from inside a workroom, shows the parent project path.
47
+
48
+ Aliases: `workroom ls`, `workroom l`
37
49
 
38
50
  ### Delete a workroom
39
51
 
@@ -43,14 +55,25 @@ workroom delete my-feature
43
55
 
44
56
  Removes the workspace/worktree and cleans up the directory. You'll be prompted for confirmation before deletion.
45
57
 
58
+ When run without a name, an interactive multi-select menu is shown, allowing you to pick one or more workrooms to delete:
59
+
60
+ ```bash
61
+ workroom delete
62
+ ```
63
+
64
+ To skip the confirmation prompt (useful for scripting), pass `--confirm` with the workroom name:
65
+
66
+ ```bash
67
+ workroom delete my-feature --confirm my-feature
68
+ ```
69
+
70
+ Alias: `workroom d`
71
+
46
72
  ### Options
47
73
 
48
74
  - `-v`, `--verbose` - Print detailed output
49
75
  - `-p`, `--pretend` - Run through the command without making changes (dry run)
50
-
51
- ### Naming rules
52
-
53
- Workroom names must be alphanumeric (dashes and underscores allowed) and must not start or end with a dash or underscore.
76
+ - `--confirm NAME` - Skip delete confirmation when NAME matches the workroom being deleted
54
77
 
55
78
  ## Setup and teardown scripts
56
79
 
data/bin/workroom CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'bundler'
5
- Bundler.require
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '/../lib'))
5
+ require 'workroom'
6
6
 
7
7
  Workroom::Commands.start
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'open3'
4
4
  require 'thor'
5
+ require 'tty-prompt'
6
+ require 'pathname'
5
7
 
6
8
  module Workroom
7
9
  class Commands < Thor
@@ -19,15 +21,19 @@ module Workroom
19
21
  true
20
22
  end
21
23
 
22
- desc 'create NAME', 'Create a new workroom'
24
+ map 'c' => :create
25
+ map 'd' => :delete
26
+ map %w[ls l] => :list
27
+
28
+ desc 'create|c', 'Create a new workroom'
23
29
  long_desc <<-DESC, wrap: false
24
- Create a new workroom with the given NAME at the same level as your main project directory,
25
- using JJ workspaces if available, otherwise falling back to git worktrees.
30
+ Create a new workroom at the same level as your main project directory, using JJ workspaces
31
+ if available, otherwise falling back to git worktrees. A random friendly name is
32
+ auto-generated.
26
33
  DESC
27
- def create(name)
28
- @name = name
34
+ def create
29
35
  check_not_in_workroom!
30
- validate_name!
36
+ @name = generate_unique_name
31
37
 
32
38
  if !options[:pretend]
33
39
  if workroom_exists?
@@ -36,24 +42,75 @@ module Workroom
36
42
  end
37
43
 
38
44
  if workroom_path.exist?
39
- raise_error DirExistsError, "Workroom directory '#{workroom_path}' already exists!"
45
+ raise_error DirExistsError,
46
+ "Workroom directory '#{display_path(workroom_path)}' already exists!"
40
47
  end
41
48
  end
42
49
 
43
50
  create_workroom
51
+ update_config(:add)
44
52
  run_setup_script
45
53
 
46
54
  say
47
- say "Workroom '#{name}' created successfully at #{workroom_path}.", :green
55
+ say "Workroom '#{name}' created successfully at #{display_path(workroom_path)}.", :green
56
+
57
+ if @setup_result
58
+ say 'Setup script output:', :blue
59
+ say @setup_result
60
+ end
61
+
62
+ say
63
+ end
64
+
65
+ desc 'list|l|ls', 'List all workrooms for the current project'
66
+ def list
67
+ data = config.read
68
+ project_path, project = find_project(data)
69
+
70
+ # Inside a workroom
71
+ if project && Pathname.pwd.to_s != project_path
72
+ say 'You are already in a workroom.', :yellow
73
+ say "Parent project is at #{display_path(project_path)}"
74
+ return
75
+ end
76
+
77
+ # Inside a parent project
78
+ if project
79
+ workrooms = project['workrooms']
80
+ if !workrooms || workrooms.empty?
81
+ say 'No workrooms found for this project.'
82
+ return
83
+ end
84
+
85
+ list_workrooms(workrooms, project['vcs'])
86
+ return
87
+ end
48
88
 
49
- return if !@setup_result
89
+ # Neither — list all workrooms grouped by parent
90
+ projects_with_workrooms = data.select { |_, p| p['workrooms']&.any? }
91
+ if projects_with_workrooms.empty?
92
+ say 'No workrooms found.'
93
+ return
94
+ end
50
95
 
51
- say 'Setup script output:', :blue
52
- say @setup_result
96
+ projects_with_workrooms.each do |path, proj|
97
+ say "#{display_path(path)}:"
98
+ inside path do
99
+ list_workrooms(proj['workrooms'], proj['vcs'])
100
+ end
101
+ say
102
+ end
53
103
  end
54
104
 
55
- desc 'delete NAME', 'Delete an existing workroom'
56
- def delete(name)
105
+ desc 'delete|d [NAME]', 'Delete an existing workroom'
106
+ method_option :confirm, type: :string,
107
+ desc: 'Skip confirmation if value matches the workroom name'
108
+ def delete(name = nil)
109
+ if !name
110
+ interactive_delete
111
+ return
112
+ end
113
+
57
114
  @name = name
58
115
  check_not_in_workroom!
59
116
  validate_name!
@@ -64,30 +121,19 @@ module Workroom
64
121
  raise_error exception, "#{vcs_label} '#{name}' does not exist!"
65
122
  end
66
123
 
67
- if !yes?("Are you sure you want to delete workroom '#{name}'?")
124
+ if options[:confirm]
125
+ if options[:confirm] != name
126
+ raise_error ArgumentError,
127
+ "--confirm value '#{options[:confirm]}' does not match " \
128
+ "workroom name '#{name}'."
129
+ end
130
+ elsif !yes?("Are you sure you want to delete workroom '#{name}'?")
68
131
  say_error "Aborting. Workroom '#{name}' was not deleted.", :yellow
69
132
  return
70
133
  end
71
134
  end
72
135
 
73
- delete_workroom
74
- cleanup_directory if jj?
75
- run_teardown_script
76
-
77
- say
78
- say "Workroom '#{name}' deleted successfully.", :green
79
-
80
- if !jj?
81
- say
82
- say "Note: Git branch '#{name}' was not deleted."
83
- say " Delete manually with `git branch -D #{name}` if needed."
84
- end
85
-
86
- return if !@teardown_result
87
-
88
- say
89
- say 'Teardown script output:', :blue
90
- say @teardown_result
136
+ delete_by_name(name)
91
137
  end
92
138
 
93
139
  private
@@ -147,8 +193,20 @@ module Workroom
147
193
  raise exception_class, message
148
194
  end
149
195
 
196
+ def config
197
+ @config ||= Config.new
198
+ end
199
+
200
+ def workrooms_dir
201
+ @workrooms_dir ||= config.workrooms_dir
202
+ end
203
+
204
+ def vcs_name
205
+ "workroom/#{name}"
206
+ end
207
+
150
208
  def workroom_path
151
- @workroom_path ||= Pathname.pwd.join("../#{name}")
209
+ @workroom_path ||= workrooms_dir.join(name)
152
210
  end
153
211
 
154
212
  def jj?
@@ -163,9 +221,9 @@ module Workroom
163
221
  say_status :repo, 'Detected Git'
164
222
  :git
165
223
  else
166
- say_status :repo, 'No supported VCS detected', :red
224
+ say_status :repo, 'No supported VCS detected in this directory.', :red
167
225
  raise_error UnsupportedVCSError, <<~_
168
- No supported VCS detected. Workroom requires either Jujutsu or Git to manage workspaces.
226
+ No supported VCS detected in this directory. Workroom requires either Git or Jujutsu to manage workspaces.
169
227
  _
170
228
  end
171
229
  end
@@ -179,7 +237,7 @@ module Workroom
179
237
  end
180
238
 
181
239
  def jj_workspace_exists?
182
- jj_workspaces.include?(name)
240
+ jj_workspaces.include?(vcs_name)
183
241
  end
184
242
 
185
243
  def git_worktree_exists?
@@ -230,22 +288,109 @@ module Workroom
230
288
  def check_not_in_workroom!
231
289
  return if !Pathname.pwd.join('.Workroom').exist?
232
290
 
233
- say_status :create, name, :red
234
291
  raise_error InWorkroomError, <<~_
235
292
  Looks like you are already in a workroom. Run this command from the root of your main development directory, not from within an existing workroom.
236
293
  _
237
294
  end
238
295
 
296
+ def interactive_delete
297
+ check_not_in_workroom!
298
+
299
+ data = config.read
300
+ _, project = find_project(data)
301
+
302
+ if !project || !project['workrooms'] || project['workrooms'].empty?
303
+ say 'No workrooms found for this project.'
304
+ return
305
+ end
306
+
307
+ workrooms = project['workrooms']
308
+ prompt = TTY::Prompt.new
309
+ selected = prompt.multi_select(
310
+ 'Select workrooms to delete:',
311
+ workrooms.keys
312
+ )
313
+
314
+ if selected.empty?
315
+ say_error 'Aborting. No workrooms were selected.', :yellow
316
+ return
317
+ end
318
+
319
+ names_list = selected.map { |n| "'#{n}'" }.join(', ')
320
+ if !yes?("Are you sure you want to delete #{selected.size} workroom(s): #{names_list}?")
321
+ say_error 'Aborting. No workrooms were deleted.', :yellow
322
+ return
323
+ end
324
+
325
+ selected.each { |n| delete_by_name(n) }
326
+ end
327
+
328
+ def delete_by_name(selected_name)
329
+ @name = selected_name
330
+ @workroom_path = nil
331
+
332
+ delete_workroom
333
+ cleanup_directory if jj?
334
+ update_config(:remove)
335
+ run_teardown_script
336
+
337
+ say "Workroom '#{name}' deleted successfully.", :green
338
+
339
+ if !jj?
340
+ say
341
+ say "Note: Git branch '#{vcs_name}' was not deleted."
342
+ say " Delete manually with `git branch -D #{vcs_name}` if needed."
343
+ end
344
+
345
+ return if !@teardown_result
346
+
347
+ say
348
+ say 'Teardown script output:', :blue
349
+ say @teardown_result
350
+ say
351
+ end
352
+
353
+ def generate_unique_name
354
+ generator = NameGenerator.new
355
+ last_name = nil
356
+
357
+ 5.times do
358
+ last_name = generator.generate
359
+ if !workroom_exists_for?(last_name) && !workroom_path_for(last_name).exist?
360
+ return last_name
361
+ end
362
+ end
363
+
364
+ loop do
365
+ candidate = "#{last_name}-#{rand(10..99)}"
366
+ if !workroom_exists_for?(candidate) && !workroom_path_for(candidate).exist?
367
+ return candidate
368
+ end
369
+ end
370
+ end
371
+
372
+ def workroom_exists_for?(candidate)
373
+ @name = candidate
374
+ @workroom_path = nil
375
+ workroom_exists?
376
+ end
377
+
378
+ def workroom_path_for(candidate)
379
+ workrooms_dir.join(candidate)
380
+ end
381
+
239
382
  def create_workroom
383
+ FileUtils.mkdir_p(workrooms_dir) if !workrooms_dir.exist?
384
+
240
385
  if testing?
241
386
  FileUtils.copy('./', workroom_path)
242
387
  return
243
388
  end
244
389
 
245
390
  if jj?
246
- run "jj workspace add #{workroom_path}"
391
+ run "jj workspace add #{workroom_path} --name #{vcs_name}"
247
392
  else
248
- run "git worktree add -b #{name} #{workroom_path}"
393
+ run "git worktree add -b #{vcs_name} #{workroom_path}"
249
394
  end
250
395
  end
251
396
 
@@ -256,7 +401,7 @@ module Workroom
256
401
  end
257
402
 
258
403
  if jj?
259
- run "jj workspace forget #{name}"
404
+ run "jj workspace forget #{vcs_name}"
260
405
  else
261
406
  run "git worktree remove #{workroom_path} --force"
262
407
  end
@@ -268,6 +413,16 @@ module Workroom
268
413
  remove_dir(workroom_path, verbose:)
269
414
  end
270
415
 
416
+ def update_config(action)
417
+ return if options[:pretend]
418
+
419
+ if action == :add
420
+ config.add_workroom Pathname.pwd.to_s, name, workroom_path.to_s, vcs
421
+ else
422
+ config.remove_workroom Pathname.pwd.to_s, name
423
+ end
424
+ end
425
+
271
426
  def run(command, config = {})
272
427
  if !config[:force] && testing?
273
428
  raise TestError, "Command execution blocked during testing: `#{command}`"
@@ -290,5 +445,47 @@ module Workroom
290
445
  def testing?
291
446
  ENV['WORKROOM_TEST'] == '1'
292
447
  end
448
+
449
+ def display_path(path)
450
+ path.to_s.sub(/\A#{Regexp.escape(Dir.home)}/, '~')
451
+ end
452
+
453
+ def list_workrooms(workrooms, vcs)
454
+ rows = workrooms.map do |name, info|
455
+ warnings = workroom_warnings(name, info, vcs)
456
+ row = [shell.set_color(name, :bold), shell.set_color(display_path(info['path']), :black)]
457
+ row << shell.set_color("[#{warnings.join(', ')}]", :yellow) if warnings.any?
458
+ row
459
+ end
460
+ print_table rows, indent: 2
461
+ end
462
+
463
+ # Find the project for the current directory. If pwd is a project in the config, return it
464
+ # directly. Otherwise, check if pwd is a workroom path under any project.
465
+ def find_project(data)
466
+ pwd = Pathname.pwd.to_s
467
+ return [pwd, data[pwd]] if data.key?(pwd)
468
+
469
+ data.each do |project_path, project|
470
+ workrooms = project['workrooms'] || {}
471
+ return [project_path, project] if workrooms.any? { |_, info| info['path'] == pwd }
472
+ end
473
+
474
+ [pwd, nil]
475
+ end
476
+
477
+ def workroom_warnings(name, info, stored_vcs)
478
+ warnings = []
479
+ warnings << 'directory not found' if !Dir.exist?(info['path'])
480
+ if !testing?
481
+ vcs_missing = if stored_vcs == 'jj'
482
+ !jj_workspaces.include?("workroom/#{name}")
483
+ elsif stored_vcs == 'git'
484
+ git_worktrees.none? { |path| File.basename(path) == name }
485
+ end
486
+ warnings << "#{stored_vcs} workspace not found" if vcs_missing
487
+ end
488
+ warnings
489
+ end
293
490
  end
294
491
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ module Workroom
6
+ class Config
7
+ CONFIG_DIR = File.expand_path('~/.config/workroom')
8
+ DEFAULT_WORKROOMS_DIR = '~/workrooms'
9
+
10
+ def config_path
11
+ @config_path ||= File.join(CONFIG_DIR, 'config.json')
12
+ end
13
+
14
+ def read
15
+ return {} if !File.exist?(config_path)
16
+
17
+ JSON.parse(File.read(config_path))
18
+ end
19
+
20
+ def write(data)
21
+ dir = File.dirname(config_path)
22
+ FileUtils.mkdir_p(dir)
23
+ File.write(config_path, JSON.pretty_generate(data))
24
+ end
25
+
26
+ def add_workroom(parent_path, name, workroom_path, vcs)
27
+ update do |data|
28
+ data[parent_path] ||= { 'vcs' => vcs.to_s, 'workrooms' => {} }
29
+ data[parent_path]['vcs'] = vcs.to_s
30
+ data[parent_path]['workrooms'][name] = { 'path' => workroom_path }
31
+ end
32
+ end
33
+
34
+ def remove_workroom(parent_path, name)
35
+ update do |data|
36
+ return if !data[parent_path]
37
+
38
+ data[parent_path]['workrooms'].delete(name)
39
+ data.delete(parent_path) if data[parent_path]['workrooms'].empty?
40
+ end
41
+ end
42
+
43
+ def workrooms_dir
44
+ Pathname.new(File.expand_path(read['workrooms_dir'] || DEFAULT_WORKROOMS_DIR))
45
+ end
46
+
47
+ def workrooms_dir=(path)
48
+ update { |data| data['workrooms_dir'] = path }
49
+ end
50
+
51
+ private
52
+
53
+ def update
54
+ data = read
55
+ yield data
56
+ write data
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Workroom
4
+ class NameGenerator
5
+ ADJECTIVES = %w[
6
+ agile amber apt azure bold brave bright brisk calm cedar clear cold cool
7
+ coral crisp cyan damp dark dawn deep deft dry dusk dusty easy even fair
8
+ fast firm flat fond free fresh full glad gold good gray green hale happy
9
+ hazy high idle jade keen kind lark last lean left light lime long lost
10
+ loud lush mild mint misty mossy neat nice noble north novel oak odd opal
11
+ open pale peak pine pink plum proud pure quick quiet rapid rare red rich
12
+ ripe rosy ruby safe sage salt sharp shy silk slim slow smart snowy soft
13
+ solid south stark steel still stout sunny sure swift tall tame teal thin
14
+ tidy trim true vivid warm west wide wild wise young
15
+ ].freeze
16
+
17
+ NOUNS = %w[
18
+ acre arch aspen badge bank bark basin bay beach bear birch blade blaze
19
+ bloom bolt bone bow brace brass breeze brick bridge brook brush canopy
20
+ cape cave cedar chain chime cliff cloud clover colt coop coral core
21
+ cove crane creek crest crow curve dale dawn deer delta dew dock dove
22
+ drake drift dune dusk eagle edge elm ember fawn feather fern field finch
23
+ fjord flame flask flint float flora flute fog font forge fox frost gate
24
+ glade glen globe gorge grain grove gulf gust hare haven hawk hazel
25
+ heath hedge heron hill hollow horn inlet isle ivy jade jewel knoll
26
+ lake larch lark latch laurel leaf ledge light lilac lily linen lodge
27
+ loft lynx maple marsh meadow mesa mint mirror mist moon moss mound
28
+ nest north oak opal orbit orion otter palm pass path peak pearl
29
+ pebble perch petal pine pixel plume pond pool porch prism quail
30
+ quarry quartz rail rain raven reef ridge river robin rock root rose
31
+ ruby sage sand scope seal seed shade shell shore silk sky slate
32
+ slope smoke snow south spark spire spoke spring spruce star stem
33
+ stone stork storm strand surf swift tern thyme tide timber tower
34
+ trail tree vale vault vine vista wand ward wave west wheat willow
35
+ wind wing wolf wren yard
36
+ ].freeze
37
+
38
+ def generate
39
+ "#{ADJECTIVES.sample}-#{NOUNS.sample}"
40
+ end
41
+ end
42
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Workroom
4
- VERSION = '0.1.0'
4
+ VERSION = '1.0.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: workroom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Moss
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '1.5'
26
+ - !ruby/object:Gem::Dependency
27
+ name: tty-prompt
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.23'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.23'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: zeitwerk
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -51,7 +65,9 @@ files:
51
65
  - bin/workroom
52
66
  - lib/workroom.rb
53
67
  - lib/workroom/commands.rb
68
+ - lib/workroom/config.rb
54
69
  - lib/workroom/engine.rb
70
+ - lib/workroom/name_generator.rb
55
71
  - lib/workroom/version.rb
56
72
  homepage: https://github.com/joelmoss/workroom
57
73
  licenses: