Class: RosettAi::Desktop::DbusClient

Inherits:
Object
  • Object
show all
Defined in:
lib/rosett_ai/desktop/dbus_client.rb

Overview

D-Bus client for communicating with be.neatnerds.rosettai service.

SAFETY: This client is designed to NEVER raise exceptions to callers. All methods return safe default values on failure. This is critical for GUI stability - a D-Bus failure should never crash the app.

All GTK4/Qt6 desktop apps use this client instead of calling Ruby Rosett-AI modules directly. This ensures the D-Bus service is the single source of truth.

Constant Summary collapse

BUS_NAME =
'be.neatnerds.rosettai'
OBJECT_PATH =
'/be/neatnerds/rosett-ai'
MANAGER_IFACE =
'be.neatnerds.rosettai.Manager'
FOCUS_IFACE =
'be.neatnerds.rosettai.FocusMonitor'
SAFE_STATUS =

Default safe return values

{ 'error' => 'Service not available', 'version' => 'unknown' }.freeze
SAFE_FOCUS =
['', ''].freeze
SAFE_COMPILE_RESULT =
'Error: D-Bus service not available'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeDbusClient

Returns a new instance of DbusClient.



33
34
35
36
37
38
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 33

def initialize
  @bus = nil
  @object = nil
  @connected = false
  @last_error = nil
end

Instance Attribute Details

#busObject (readonly)

Returns the value of attribute bus.



31
32
33
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 31

def bus
  @bus
end

#last_errorObject (readonly)

Returns the value of attribute last_error.



31
32
33
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 31

def last_error
  @last_error
end

Instance Method Details

#compileString

Run compilation via D-Bus. SAFETY: Never raises, returns error string on failure.

Returns:

  • (String)

    compilation result or error message



106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 106

def compile
  GuiLogger.dbus_call(:compile, interface: MANAGER_IFACE)
  return SAFE_COMPILE_RESULT unless safe_connect

  @object.default_iface = MANAGER_IFACE
  result = @object.Compile
  output = result.is_a?(Array) ? result.first : result.to_s
  GuiLogger.dbus_call(:compile, status: :success, result_length: output.length)
  output
rescue StandardError => e
  log_error('Compile failed', e)
  "Error: #{e.message}"
end

#connectBoolean

Connect to the D-Bus session bus and get service proxy.

Returns:

  • (Boolean)

    true if connected successfully



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 43

def connect
  return true if @connected

  GuiLogger.dbus_call(:connect, bus_name: BUS_NAME)
  @last_error = nil
  @bus = ::DBus::SessionBus.instance
  service = @bus.service(BUS_NAME)
  @object = service.object(OBJECT_PATH)
  @object.introspect
  @connected = true
  GuiLogger.dbus_call(:connect, status: :success)
  true
rescue ::DBus::Error => e
  @last_error = e.message
  log_error('D-Bus connection failed', e)
  @connected = false
  false
rescue StandardError => e
  @last_error = e.message
  log_error('Unexpected error during D-Bus connection', e)
  @connected = false
  false
end

#connected?Boolean

Check if connected to D-Bus service.

Returns:

  • (Boolean)


70
71
72
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 70

def connected?
  @connected
end

#current_focusArray<String>

Get current focus from FocusMonitor interface. SAFETY: Never raises, returns empty array on failure.

Returns:

  • (Array<String>)

    [app_id, window_title]



140
141
142
143
144
145
146
147
148
149
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 140

def current_focus
  return SAFE_FOCUS.dup unless safe_connect

  @object.default_iface = FOCUS_IFACE
  result = @object.GetCurrentFocus
  result.is_a?(Array) && result.size >= 2 ? result : SAFE_FOCUS.dup
rescue StandardError => e
  log_error('GetCurrentFocus failed', e)
  SAFE_FOCUS.dup
end

#disconnectObject

Disconnect from D-Bus. SAFETY: Never raises.



221
222
223
224
225
226
227
228
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 221

def disconnect
  @connected = false
  @object = nil
  @bus = nil
  @last_error = nil
rescue StandardError => e
  log_error('Disconnect failed', e)
end

#list_enginesArray<Hash>

List available engines from D-Bus service. SAFETY: Never raises, returns empty array on failure.

Returns:

  • (Array<Hash>)

    engine hashes with :name and :display_name keys



189
190
191
192
193
194
195
196
197
198
199
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 189

def list_engines
  return [] unless safe_connect

  @object.default_iface = MANAGER_IFACE
  result = @object.ListEngines
  tuples = result.is_a?(Array) ? result.first : []
  (tuples || []).map { |name, display| { name: name, display_name: display } }
rescue StandardError => e
  log_error('ListEngines failed', e)
  []
end

#on_context_changed {|context_name| ... } ⇒ Boolean

Subscribe to ContextChanged signal. SAFETY: Never raises, silently fails if service unavailable.

Yields:

  • (context_name)

    called when context changes

Returns:

  • (Boolean)

    true if subscription succeeded



173
174
175
176
177
178
179
180
181
182
183
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 173

def on_context_changed(&block)
  return false unless safe_connect
  return false unless block

  @object.default_iface = MANAGER_IFACE
  @object.on_signal('ContextChanged', &block)
  true
rescue StandardError => e
  log_error('ContextChanged subscription failed', e)
  false
end

#on_focus_changed {|app_id, title| ... } ⇒ Boolean

Subscribe to FocusChanged signal. SAFETY: Never raises, silently fails if service unavailable.

Yields:

  • (app_id, title)

    called when focus changes

Returns:

  • (Boolean)

    true if subscription succeeded



156
157
158
159
160
161
162
163
164
165
166
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 156

def on_focus_changed(&block)
  return false unless safe_connect
  return false unless block

  @object.default_iface = FOCUS_IFACE
  @object.on_signal('FocusChanged', &block)
  true
rescue StandardError => e
  log_error('FocusChanged subscription failed', e)
  false
end

#service_available?Boolean

Check if the D-Bus service is available. SAFETY: Never raises, returns false on any error.

Returns:

  • (Boolean)


78
79
80
81
82
83
84
85
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 78

def service_available?
  return false unless connect

  @bus.service(BUS_NAME).exists?
rescue StandardError => e
  log_error('Service availability check failed', e)
  false
end

#set_config(key, value) ⇒ Boolean

Update a configuration key via D-Bus. SAFETY: Never raises, returns false on failure.

Parameters:

  • key (String)

    configuration key (e.g. 'default_engine')

  • value (String)

    new value

Returns:

  • (Boolean)

    true if successful



207
208
209
210
211
212
213
214
215
216
217
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 207

def set_config(key, value)
  return false unless safe_connect

  @object.default_iface = MANAGER_IFACE
  result = @object.SetConfig(key, value.to_s)
  output = result.is_a?(Array) ? result.first : result.to_s
  !output.start_with?('Error:')
rescue StandardError => e
  log_error('SetConfig failed', e)
  false
end

#statusHash

Get service status from Manager interface. SAFETY: Never raises, returns safe default hash on error.

Returns:

  • (Hash)

    status dictionary



91
92
93
94
95
96
97
98
99
100
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 91

def status
  return SAFE_STATUS.dup unless safe_connect

  @object.default_iface = MANAGER_IFACE
  result = @object.GetStatus
  result.is_a?(Array) ? result.first : SAFE_STATUS.dup
rescue StandardError => e
  log_error('GetStatus failed', e)
  SAFE_STATUS.merge('error' => e.message)
end

#switch_context(context_name) ⇒ Boolean

Switch AI context. SAFETY: Never raises, returns false on failure.

Parameters:

  • context_name (String)

    name of context to switch to

Returns:

  • (Boolean)

    true if successful



125
126
127
128
129
130
131
132
133
134
# File 'lib/rosett_ai/desktop/dbus_client.rb', line 125

def switch_context(context_name)
  return false unless safe_connect

  @object.default_iface = MANAGER_IFACE
  @object.SwitchContext(context_name)
  true
rescue StandardError => e
  log_error('SwitchContext failed', e)
  false
end