mui 0.1.0 → 0.3.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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +163 -0
  3. data/CHANGELOG.md +448 -0
  4. data/README.md +309 -6
  5. data/docs/_config.yml +56 -0
  6. data/docs/configuration.md +301 -0
  7. data/docs/getting-started.md +140 -0
  8. data/docs/index.md +55 -0
  9. data/docs/jobs.md +297 -0
  10. data/docs/keybindings.md +229 -0
  11. data/docs/plugins.md +285 -0
  12. data/docs/syntax-highlighting.md +149 -0
  13. data/exe/mui +1 -2
  14. data/lib/mui/autocmd.rb +66 -0
  15. data/lib/mui/buffer.rb +275 -0
  16. data/lib/mui/buffer_word_cache.rb +131 -0
  17. data/lib/mui/buffer_word_completer.rb +77 -0
  18. data/lib/mui/color_manager.rb +136 -0
  19. data/lib/mui/color_scheme.rb +63 -0
  20. data/lib/mui/command_completer.rb +30 -0
  21. data/lib/mui/command_context.rb +90 -0
  22. data/lib/mui/command_history.rb +89 -0
  23. data/lib/mui/command_line.rb +167 -0
  24. data/lib/mui/command_registry.rb +44 -0
  25. data/lib/mui/completion_renderer.rb +84 -0
  26. data/lib/mui/completion_state.rb +58 -0
  27. data/lib/mui/config.rb +58 -0
  28. data/lib/mui/editor.rb +395 -0
  29. data/lib/mui/error.rb +29 -0
  30. data/lib/mui/file_completer.rb +51 -0
  31. data/lib/mui/floating_window.rb +161 -0
  32. data/lib/mui/handler_result.rb +107 -0
  33. data/lib/mui/highlight.rb +22 -0
  34. data/lib/mui/highlighters/base.rb +23 -0
  35. data/lib/mui/highlighters/search_highlighter.rb +27 -0
  36. data/lib/mui/highlighters/selection_highlighter.rb +48 -0
  37. data/lib/mui/highlighters/syntax_highlighter.rb +107 -0
  38. data/lib/mui/input.rb +17 -0
  39. data/lib/mui/insert_completion_renderer.rb +92 -0
  40. data/lib/mui/insert_completion_state.rb +77 -0
  41. data/lib/mui/job.rb +81 -0
  42. data/lib/mui/job_manager.rb +113 -0
  43. data/lib/mui/key_code.rb +30 -0
  44. data/lib/mui/key_handler/base.rb +187 -0
  45. data/lib/mui/key_handler/command_mode.rb +511 -0
  46. data/lib/mui/key_handler/insert_mode.rb +323 -0
  47. data/lib/mui/key_handler/motions/motion_handler.rb +56 -0
  48. data/lib/mui/key_handler/normal_mode.rb +552 -0
  49. data/lib/mui/key_handler/operators/base_operator.rb +134 -0
  50. data/lib/mui/key_handler/operators/change_operator.rb +179 -0
  51. data/lib/mui/key_handler/operators/delete_operator.rb +176 -0
  52. data/lib/mui/key_handler/operators/paste_operator.rb +119 -0
  53. data/lib/mui/key_handler/operators/yank_operator.rb +127 -0
  54. data/lib/mui/key_handler/search_mode.rb +191 -0
  55. data/lib/mui/key_handler/visual_line_mode.rb +20 -0
  56. data/lib/mui/key_handler/visual_mode.rb +402 -0
  57. data/lib/mui/key_handler/window_command.rb +112 -0
  58. data/lib/mui/key_handler.rb +16 -0
  59. data/lib/mui/key_notation_parser.rb +152 -0
  60. data/lib/mui/key_sequence.rb +67 -0
  61. data/lib/mui/key_sequence_buffer.rb +85 -0
  62. data/lib/mui/key_sequence_handler.rb +163 -0
  63. data/lib/mui/key_sequence_matcher.rb +79 -0
  64. data/lib/mui/layout/calculator.rb +15 -0
  65. data/lib/mui/layout/leaf_node.rb +33 -0
  66. data/lib/mui/layout/node.rb +29 -0
  67. data/lib/mui/layout/split_node.rb +132 -0
  68. data/lib/mui/line_renderer.rb +173 -0
  69. data/lib/mui/mode.rb +13 -0
  70. data/lib/mui/mode_manager.rb +186 -0
  71. data/lib/mui/motion.rb +139 -0
  72. data/lib/mui/plugin.rb +35 -0
  73. data/lib/mui/plugin_manager.rb +106 -0
  74. data/lib/mui/register.rb +110 -0
  75. data/lib/mui/screen.rb +103 -0
  76. data/lib/mui/search_completer.rb +50 -0
  77. data/lib/mui/search_input.rb +40 -0
  78. data/lib/mui/search_state.rb +121 -0
  79. data/lib/mui/selection.rb +55 -0
  80. data/lib/mui/status_line_renderer.rb +40 -0
  81. data/lib/mui/syntax/language_detector.rb +106 -0
  82. data/lib/mui/syntax/lexer_base.rb +106 -0
  83. data/lib/mui/syntax/lexers/c_lexer.rb +127 -0
  84. data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
  85. data/lib/mui/syntax/lexers/go_lexer.rb +205 -0
  86. data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
  87. data/lib/mui/syntax/lexers/javascript_lexer.rb +197 -0
  88. data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
  89. data/lib/mui/syntax/lexers/ruby_lexer.rb +114 -0
  90. data/lib/mui/syntax/lexers/rust_lexer.rb +148 -0
  91. data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -0
  92. data/lib/mui/syntax/token.rb +42 -0
  93. data/lib/mui/syntax/token_cache.rb +91 -0
  94. data/lib/mui/tab_bar_renderer.rb +87 -0
  95. data/lib/mui/tab_manager.rb +96 -0
  96. data/lib/mui/tab_page.rb +35 -0
  97. data/lib/mui/terminal_adapter/base.rb +92 -0
  98. data/lib/mui/terminal_adapter/curses.rb +164 -0
  99. data/lib/mui/terminal_adapter.rb +4 -0
  100. data/lib/mui/themes/default.rb +315 -0
  101. data/lib/mui/undo_manager.rb +83 -0
  102. data/lib/mui/undoable_action.rb +175 -0
  103. data/lib/mui/unicode_width.rb +100 -0
  104. data/lib/mui/version.rb +1 -1
  105. data/lib/mui/window.rb +201 -0
  106. data/lib/mui/window_manager.rb +256 -0
  107. data/lib/mui/wrap_cache.rb +40 -0
  108. data/lib/mui/wrap_helper.rb +84 -0
  109. data/lib/mui.rb +171 -2
  110. metadata +123 -5
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module HandlerResult
5
+ # Base class for handler results with common attributes and default implementations
6
+ class Base
7
+ attr_reader :mode, :message
8
+
9
+ def initialize(mode: nil, message: nil, quit: false, pending_sequence: false)
10
+ @mode = mode
11
+ @message = message
12
+ @quit = quit
13
+ @pending_sequence = pending_sequence
14
+ freeze
15
+ end
16
+
17
+ def quit?
18
+ @quit
19
+ end
20
+
21
+ # True when waiting for more keys in a multi-key sequence
22
+ def pending_sequence?
23
+ @pending_sequence
24
+ end
25
+
26
+ def start_selection?
27
+ false
28
+ end
29
+
30
+ def line_mode?
31
+ false
32
+ end
33
+
34
+ def clear_selection?
35
+ false
36
+ end
37
+
38
+ def toggle_line_mode?
39
+ false
40
+ end
41
+ end
42
+
43
+ # Result for NormalMode - handles visual mode start
44
+ class NormalModeResult < Base
45
+ def initialize(mode: nil, message: nil, quit: false, pending_sequence: false, start_selection: false, line_mode: false, group_started: false)
46
+ @start_selection = start_selection
47
+ @line_mode = line_mode
48
+ @group_started = group_started
49
+ super(mode:, message:, quit:, pending_sequence:)
50
+ end
51
+
52
+ def start_selection?
53
+ @start_selection
54
+ end
55
+
56
+ def line_mode?
57
+ @line_mode
58
+ end
59
+
60
+ def group_started?
61
+ @group_started
62
+ end
63
+ end
64
+
65
+ # Result for VisualMode - handles selection clear and line mode toggle
66
+ class VisualModeResult < Base
67
+ def initialize(mode: nil, message: nil, quit: false, pending_sequence: false, clear_selection: false, toggle_line_mode: false, group_started: false)
68
+ @clear_selection = clear_selection
69
+ @toggle_line_mode = toggle_line_mode
70
+ @group_started = group_started
71
+ super(mode:, message:, quit:, pending_sequence:)
72
+ end
73
+
74
+ def clear_selection?
75
+ @clear_selection
76
+ end
77
+
78
+ def toggle_line_mode?
79
+ @toggle_line_mode
80
+ end
81
+
82
+ def group_started?
83
+ @group_started
84
+ end
85
+ end
86
+
87
+ # Result for InsertMode - uses base functionality only
88
+ class InsertModeResult < Base
89
+ end
90
+
91
+ # Result for CommandMode - uses base functionality only
92
+ class CommandModeResult < Base
93
+ end
94
+
95
+ # Result for SearchMode - handles search execution
96
+ class SearchModeResult < Base
97
+ def initialize(mode: nil, message: nil, quit: false, pending_sequence: false, cancelled: false)
98
+ @cancelled = cancelled
99
+ super(mode:, message:, quit:, pending_sequence:)
100
+ end
101
+
102
+ def cancelled?
103
+ @cancelled
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ class Highlight
5
+ attr_reader :start_col, :end_col, :style, :priority
6
+
7
+ def initialize(start_col:, end_col:, style:, priority:)
8
+ @start_col = start_col
9
+ @end_col = end_col
10
+ @style = style
11
+ @priority = priority
12
+ end
13
+
14
+ def overlaps?(other)
15
+ start_col <= other.end_col && end_col >= other.start_col
16
+ end
17
+
18
+ def <=>(other)
19
+ [start_col, -priority] <=> [other.start_col, -other.priority]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Highlighters
5
+ class Base
6
+ PRIORITY_SYNTAX = 100
7
+ PRIORITY_SELECTION = 200
8
+ PRIORITY_SEARCH = 300
9
+
10
+ def initialize(color_scheme)
11
+ @color_scheme = color_scheme
12
+ end
13
+
14
+ def highlights_for(_row, _line, _options = {})
15
+ raise Mui::MethodNotOverriddenError, "#{self.class}#highlights_for must be implemented"
16
+ end
17
+
18
+ def priority
19
+ 0
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Highlighters
5
+ class SearchHighlighter < Base
6
+ def highlights_for(row, _line, options = {})
7
+ search_state = options[:search_state]
8
+ buffer = options[:buffer]
9
+ return [] unless search_state&.has_pattern?
10
+
11
+ matches = search_state.matches_for_row(row, buffer:)
12
+ matches.map do |match|
13
+ Highlight.new(
14
+ start_col: match[:col],
15
+ end_col: match[:end_col],
16
+ style: :search_highlight,
17
+ priority:
18
+ )
19
+ end
20
+ end
21
+
22
+ def priority
23
+ PRIORITY_SEARCH
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Highlighters
5
+ class SelectionHighlighter < Base
6
+ def highlights_for(row, line, options = {})
7
+ selection = options[:selection]
8
+ return [] unless selection
9
+
10
+ range = selection.normalized_range
11
+ return [] if row < range[:start_row] || row > range[:end_row]
12
+
13
+ if selection.line_mode
14
+ line_mode_highlights(line)
15
+ else
16
+ char_mode_highlights(row, line, range)
17
+ end
18
+ end
19
+
20
+ def priority
21
+ PRIORITY_SELECTION
22
+ end
23
+
24
+ private
25
+
26
+ def line_mode_highlights(line)
27
+ [Highlight.new(
28
+ start_col: 0,
29
+ end_col: [line.length - 1, 0].max,
30
+ style: :visual_selection,
31
+ priority:
32
+ )]
33
+ end
34
+
35
+ def char_mode_highlights(row, line, range)
36
+ start_col = row == range[:start_row] ? range[:start_col] : 0
37
+ end_col = row == range[:end_row] ? range[:end_col] : [line.length - 1, 0].max
38
+
39
+ [Highlight.new(
40
+ start_col:,
41
+ end_col:,
42
+ style: :visual_selection,
43
+ priority:
44
+ )]
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Highlighters
5
+ # Provides syntax highlighting based on language-specific lexers
6
+ class SyntaxHighlighter < Base
7
+ # Maps token types to ColorScheme style names
8
+ # Note: identifier and operator are excluded to reduce highlight count
9
+ # (they typically use the same color as normal text)
10
+ TOKEN_STYLE_MAP = {
11
+ keyword: :syntax_keyword,
12
+ string: :syntax_string,
13
+ comment: :syntax_comment,
14
+ number: :syntax_number,
15
+ symbol: :syntax_symbol,
16
+ constant: :syntax_constant,
17
+ preprocessor: :syntax_preprocessor,
18
+ char: :syntax_string,
19
+ instance_variable: :syntax_instance_variable,
20
+ global_variable: :syntax_global_variable,
21
+ method_call: :syntax_method_call,
22
+ type: :syntax_type,
23
+ macro: :syntax_keyword, # Rust macros (println!, vec!, etc.)
24
+ regex: :syntax_string # JavaScript/TypeScript regex literals
25
+ }.freeze
26
+
27
+ def initialize(color_scheme, buffer: nil)
28
+ super(color_scheme)
29
+ @buffer = buffer
30
+ setup_lexer
31
+ end
32
+
33
+ # Update the buffer and reset the lexer
34
+ def buffer=(new_buffer)
35
+ @buffer = new_buffer
36
+ setup_lexer
37
+ end
38
+
39
+ # Generate highlights for a line
40
+ # TODO: Refactor to reduce complexity (Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity)
41
+ def highlights_for(row, line, options = {})
42
+ return [] unless @lexer
43
+ return [] unless Mui.config.get(:syntax)
44
+
45
+ buffer_lines = options[:buffer]&.lines || @buffer&.lines || []
46
+ tokens = @token_cache.tokens_for(row, line, buffer_lines)
47
+
48
+ tokens.filter_map do |token|
49
+ style = style_for_token_type(token.type)
50
+ next unless style && @color_scheme[style]
51
+
52
+ Highlight.new(
53
+ start_col: token.start_col,
54
+ end_col: token.end_col,
55
+ style:,
56
+ priority:
57
+ )
58
+ end
59
+ end
60
+
61
+ def priority
62
+ PRIORITY_SYNTAX
63
+ end
64
+
65
+ # Invalidate cache from a specific row onwards
66
+ # Called when buffer content changes
67
+ def invalidate_from(row)
68
+ @token_cache&.invalidate(row)
69
+ end
70
+
71
+ # Clear the entire cache
72
+ def clear_cache
73
+ @token_cache&.clear
74
+ end
75
+
76
+ # Prefetch tokens for lines around the visible area
77
+ def prefetch(visible_start, visible_end)
78
+ return unless @lexer && @token_cache && @buffer
79
+ return unless Mui.config.get(:syntax)
80
+
81
+ @token_cache.prefetch(visible_start, visible_end, @buffer.lines)
82
+ end
83
+
84
+ # Check if this highlighter is active (has a lexer)
85
+ def active?
86
+ !@lexer.nil?
87
+ end
88
+
89
+ private
90
+
91
+ def setup_lexer
92
+ @lexer = nil
93
+ @token_cache = nil
94
+ return unless @buffer
95
+
96
+ @lexer = Syntax::LanguageDetector.lexer_for_file(@buffer.name)
97
+ return unless @lexer
98
+
99
+ @token_cache = Syntax::TokenCache.new(@lexer)
100
+ end
101
+
102
+ def style_for_token_type(type)
103
+ TOKEN_STYLE_MAP[type]
104
+ end
105
+ end
106
+ end
107
+ end
data/lib/mui/input.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ class Input
5
+ def initialize(adapter:)
6
+ @adapter = adapter
7
+ end
8
+
9
+ def read
10
+ @adapter.getch
11
+ end
12
+
13
+ def read_nonblock
14
+ @adapter.getch_nonblock
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Renders completion popup for Insert mode (LSP completions)
5
+ class InsertCompletionRenderer
6
+ MAX_VISIBLE_ITEMS = 10
7
+
8
+ def initialize(screen, color_scheme)
9
+ @screen = screen
10
+ @color_scheme = color_scheme
11
+ end
12
+
13
+ def render(completion_state, cursor_row, cursor_col)
14
+ return unless completion_state.active?
15
+
16
+ items = completion_state.items
17
+ selected_index = completion_state.selected_index
18
+
19
+ # Calculate visible window
20
+ visible_start, visible_end = calculate_visible_range(items.length, selected_index)
21
+ visible_items = items[visible_start...visible_end]
22
+
23
+ # Calculate popup dimensions
24
+ max_width = calculate_max_width(visible_items)
25
+ popup_height = visible_items.length
26
+
27
+ # Calculate position (popup appears below the cursor)
28
+ popup_row = cursor_row + 1
29
+ popup_col = cursor_col
30
+
31
+ # If popup would go below screen, show above cursor instead
32
+ popup_row = cursor_row - popup_height if popup_row + popup_height > @screen.height - 1
33
+
34
+ # Ensure popup stays within screen bounds
35
+ popup_col = [@screen.width - max_width - 1, popup_col].min
36
+ popup_col = [0, popup_col].max
37
+ popup_row = [0, popup_row].max
38
+
39
+ # Render each visible item
40
+ visible_items.each_with_index do |item, i|
41
+ actual_index = visible_start + i
42
+ is_selected = actual_index == selected_index
43
+
44
+ render_item(
45
+ item,
46
+ popup_row + i,
47
+ popup_col,
48
+ max_width,
49
+ is_selected
50
+ )
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def calculate_visible_range(total_count, selected_index)
57
+ return [0, total_count] if total_count <= MAX_VISIBLE_ITEMS
58
+
59
+ # Try to center the selected item
60
+ half = MAX_VISIBLE_ITEMS / 2
61
+ start_index = selected_index - half
62
+ start_index = [0, start_index].max
63
+ end_index = start_index + MAX_VISIBLE_ITEMS
64
+ end_index = [total_count, end_index].min
65
+ start_index = end_index - MAX_VISIBLE_ITEMS
66
+
67
+ [start_index, end_index]
68
+ end
69
+
70
+ def calculate_max_width(items)
71
+ return 0 if items.empty?
72
+
73
+ items.map { |item| display_width(item_label(item)) }.max + 2 # +2 for padding
74
+ end
75
+
76
+ def item_label(item)
77
+ item[:label] || item.to_s
78
+ end
79
+
80
+ def display_width(text)
81
+ UnicodeWidth.string_width(text)
82
+ end
83
+
84
+ def render_item(item, row, col, width, selected)
85
+ style_key = selected ? :completion_popup_selected : :completion_popup
86
+ style = @color_scheme[style_key]
87
+ label = item_label(item)
88
+ padded_text = " #{label}".ljust(width)
89
+ @screen.put_with_style(row, col, padded_text, style)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Manages Insert mode completion state for LSP completions
5
+ class InsertCompletionState
6
+ attr_reader :items, :selected_index, :prefix, :original_items
7
+
8
+ def initialize
9
+ reset
10
+ end
11
+
12
+ def reset
13
+ @items = []
14
+ @original_items = []
15
+ @selected_index = 0
16
+ @prefix = ""
17
+ end
18
+
19
+ def active?
20
+ !@items.empty?
21
+ end
22
+
23
+ def start(items, prefix: "")
24
+ @original_items = items.dup
25
+ @items = items
26
+ @selected_index = 0
27
+ @prefix = prefix
28
+ end
29
+
30
+ # Update prefix and filter items based on new prefix
31
+ def update_prefix(new_prefix)
32
+ return if new_prefix == @prefix
33
+
34
+ @prefix = new_prefix
35
+ @items = @original_items.select do |item|
36
+ label = item[:label] || item[:insert_text] || ""
37
+ label.downcase.start_with?(new_prefix.downcase)
38
+ end
39
+ @selected_index = 0
40
+ end
41
+
42
+ def select_next
43
+ return unless active?
44
+
45
+ @selected_index = (@selected_index + 1) % @items.length
46
+ end
47
+
48
+ def select_previous
49
+ return unless active?
50
+
51
+ @selected_index = (@selected_index - 1) % @items.length
52
+ end
53
+
54
+ def current_item
55
+ return nil unless active?
56
+
57
+ @items[@selected_index]
58
+ end
59
+
60
+ # Returns the text to insert for the current item
61
+ def insert_text
62
+ return nil unless current_item
63
+
64
+ item = current_item
65
+ # Prefer textEdit.newText if available (text_edit value has string keys from LSP)
66
+ item.dig(:text_edit, "newText") || item[:insert_text] || item[:label] || item.to_s
67
+ end
68
+
69
+ # Returns the textEdit range if available
70
+ def text_edit_range
71
+ return nil unless current_item
72
+
73
+ # text_edit value has string keys from LSP
74
+ current_item.dig(:text_edit, "range")
75
+ end
76
+ end
77
+ end
data/lib/mui/job.rb ADDED
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Represents a single asynchronous job
5
+ class Job
6
+ # Job status constants
7
+ module Status
8
+ PENDING = :pending
9
+ RUNNING = :running
10
+ COMPLETED = :completed
11
+ FAILED = :failed
12
+ CANCELLED = :cancelled
13
+ end
14
+
15
+ attr_reader :id, :status, :result, :error
16
+
17
+ def initialize(id, &block)
18
+ @id = id
19
+ @block = block
20
+ @status = Status::PENDING
21
+ @result = nil
22
+ @error = nil
23
+ @cancelled = false
24
+ @mutex = Mutex.new
25
+ end
26
+
27
+ def run
28
+ @mutex.synchronize do
29
+ return if @cancelled
30
+
31
+ @status = Status::RUNNING
32
+ end
33
+
34
+ begin
35
+ @result = @block.call
36
+ @mutex.synchronize do
37
+ @status = @cancelled ? Status::CANCELLED : Status::COMPLETED
38
+ end
39
+ rescue StandardError => e
40
+ @mutex.synchronize do
41
+ @error = e
42
+ @status = Status::FAILED
43
+ end
44
+ end
45
+ end
46
+
47
+ def cancel
48
+ @mutex.synchronize do
49
+ return false if @status == Status::COMPLETED || @status == Status::FAILED
50
+
51
+ @cancelled = true
52
+ @status = Status::CANCELLED if @status == Status::PENDING
53
+ true
54
+ end
55
+ end
56
+
57
+ def pending?
58
+ @status == Status::PENDING
59
+ end
60
+
61
+ def running?
62
+ @status == Status::RUNNING
63
+ end
64
+
65
+ def completed?
66
+ @status == Status::COMPLETED
67
+ end
68
+
69
+ def failed?
70
+ @status == Status::FAILED
71
+ end
72
+
73
+ def cancelled?
74
+ @status == Status::CANCELLED
75
+ end
76
+
77
+ def finished?
78
+ completed? || failed? || cancelled?
79
+ end
80
+ end
81
+ end