Class: Rfd::Controller

Inherits:
Object
  • Object
show all
Includes:
Commands, FileOps, Viewer
Defined in:
lib/rfd/controller.rb

Constant Summary

Constants included from FileOps

FileOps::SORT_KEYS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Viewer

#color_pair_from_name, #edit, #format_video_metadata_from_hash, #play_audio, #preview, #render_code_result, #render_pdf_result, #render_preview_result, #render_successful_preview, #render_text_result, #render_video_metadata_only, #render_video_result, #text_attr_from_names, #update_preview, #view, #view_image

Methods included from FileOps

#cd, #chmod, #chown, #clipboard, #cp, #delete, #fetch_items_from_filesystem_or_zip, #grep, #ls, #mkdir, #mv, #paste, #popd, #rename, #sort, #sort_items_according_to_current_direction, #symlink, #touch, #touch_t, #trash, #unarchive, #yank, #zip

Methods included from Rfd::Commands::Other

#C, #O, #q

Methods included from Rfd::Commands::Viewing

#/, #P, #o, #s

Methods included from Rfd::Commands::FileOperations

#S, #a, #c, #m, #r, #space, #w

Methods included from Rfd::Commands::Navigation

#-, #backspace, #enter

Constructor Details

#initializeController

:nodoc:



12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/rfd/controller.rb', line 12

def initialize
  @main = MainWindow.new
  @header_l = HeaderLeftWindow.new
  @header_r = HeaderRightWindow.new
  @command_line = CommandLineWindow.new
  @debug = DebugWindow.new if ENV['DEBUG']
  @direction, @dir_history, @last_command, @times, @yanked_items, @sub_window = nil, [], nil, nil, nil, nil
  @preview_enabled = true  # Preview is shown by default

  # Start preview server for async video preview
  start_preview_server
end

Instance Attribute Details

#command_lineObject (readonly)

Returns the value of attribute command_line.



9
10
11
# File 'lib/rfd/controller.rb', line 9

def command_line
  @command_line
end

#current_dirObject (readonly)

Returns the value of attribute current_dir.



9
10
11
# File 'lib/rfd/controller.rb', line 9

def current_dir
  @current_dir
end

#current_pageObject (readonly)

Returns the value of attribute current_page.



9
10
11
# File 'lib/rfd/controller.rb', line 9

def current_page
  @current_page
end

#current_rowObject (readonly)

Returns the value of attribute current_row.



9
10
11
# File 'lib/rfd/controller.rb', line 9

def current_row
  @current_row
end

#current_zipObject (readonly)

Returns the value of attribute current_zip.



9
10
11
# File 'lib/rfd/controller.rb', line 9

def current_zip
  @current_zip
end

#displayed_itemsObject (readonly)

Returns the value of attribute displayed_items.



9
10
11
# File 'lib/rfd/controller.rb', line 9

def displayed_items
  @displayed_items
end

#header_lObject (readonly)

Returns the value of attribute header_l.



9
10
11
# File 'lib/rfd/controller.rb', line 9

def header_l
  @header_l
end

#header_rObject (readonly)

Returns the value of attribute header_r.



9
10
11
# File 'lib/rfd/controller.rb', line 9

def header_r
  @header_r
end

#itemsObject (readonly)

Returns the value of attribute items.



9
10
11
# File 'lib/rfd/controller.rb', line 9

def items
  @items
end

#mainObject (readonly)

Returns the value of attribute main.



9
10
11
# File 'lib/rfd/controller.rb', line 9

def main
  @main
end

Instance Method Details

#ask(prompt = '(y/n)') ⇒ Object

Let the user answer y or n.

Parameters

  • prompt - Prompt message



350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/rfd/controller.rb', line 350

def ask(prompt = '(y/n)')
  command_line.set_prompt prompt
  command_line.refresh
  Curses.stdscr.timeout = -1  # Blocking mode for user input
  begin
    while (c = Curses.getch)
      next unless [?N, ?Y, ?n, ?y, 3, 27] .include? c  # N, Y, n, y, ^c, esc
      clear_command_line
      break (c == 'y') || (c == 'Y')
    end
  ensure
    Curses.stdscr.timeout = 100  # Restore non-blocking mode
  end
end

#clear_command_lineObject



308
309
310
311
312
313
# File 'lib/rfd/controller.rb', line 308

def clear_command_line
  command_line.writeln 0, ''
  command_line.clear
  command_line.noutrefresh
  print "\e[?25l"  # Hide cursor
end

#close_sub_windowObject

Close the sub window if open



286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/rfd/controller.rb', line 286

def close_sub_window
  if @sub_window
    was_preview = @sub_window.is_a?(PreviewWindow)
    @sub_window.close
    @sub_window = nil
    # Restore preview if it was enabled and we closed a non-preview window
    if @preview_enabled && !was_preview
      @sub_window = PreviewWindow.new(self)
      @sub_window.render
    end
    move_cursor current_row
  end
end

#current_itemObject

The file or directory on which the cursor is on.



172
173
174
# File 'lib/rfd/controller.rb', line 172

def current_item
  items[current_row]
end

#draw_itemsObject

Update the main window with the loaded files and directories. Also update the header.



237
238
239
240
241
242
# File 'lib/rfd/controller.rb', line 237

def draw_items
  main.newpad items
  @displayed_items = items[current_page * max_items, max_items]
  main.display current_page
  header_l.draw_path_and_page_number path: current_dir.path, current: current_page + 1, total: total_pages
end

#draw_marked_itemsObject

Update the header information concerning currently marked files or directories.



270
271
272
273
# File 'lib/rfd/controller.rb', line 270

def draw_marked_items
  items = marked_items
  header_r.draw_marked_items count: items.size, size: items.inject(0) {|sum, i| sum += i.size}
end

#draw_total_itemsObject

Update the header information concerning total files and directories in the current directory.



276
277
278
# File 'lib/rfd/controller.rb', line 276

def draw_total_items
  header_r.draw_total_items count: items.size, size: items.inject(0) {|sum, i| sum += i.size}
end

#find(str) ⇒ Object

Focus at the first file or directory of which name starts with the given String.



215
216
217
218
# File 'lib/rfd/controller.rb', line 215

def find(str)
  index = items.index {|i| i.index > current_row && i.name.start_with?(str)} || items.index {|i| i.name.start_with? str}
  move_cursor index if index
end

#find_reverse(str) ⇒ Object

Focus at the last file or directory of which name starts with the given String.



221
222
223
224
# File 'lib/rfd/controller.rb', line 221

def find_reverse(str)
  index = items.reverse.index {|i| i.index < current_row && i.name.start_with?(str)} || items.reverse.index {|i| i.name.start_with? str}
  move_cursor items.size - index - 1 if index
end

#first_page?Boolean

Current page is the first page?

Returns:

  • (Boolean)


245
246
247
# File 'lib/rfd/controller.rb', line 245

def first_page?
  current_page == 0
end

#get_charObject

Get a char as a String from user input.



301
302
303
304
305
306
# File 'lib/rfd/controller.rb', line 301

def get_char
  Curses.stdscr.timeout = -1  # Blocking mode for user input
  c = Curses.getch
  Curses.stdscr.timeout = 100  # Restore non-blocking mode
  c if (0..255) === c.ord
end

#helpObject



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/rfd/controller.rb', line 372

def help
  lines = HelpGenerator.generate.lines
  h = [lines.size + 2, Curses.lines - 6].min
  w = [lines.map(&:size).max + 4, Curses.cols - 4].min
  y = (Curses.lines - h) / 2
  x = (Curses.cols - w) / 2

  win = Curses::Window.new(h, w, y, x)
  win.bkgdset Curses.color_pair(Curses::COLOR_CYAN)
  Rfd::Window.draw_ncursesw_border(win, h, w)
  win.setpos(0, 2)
  win.addstr(' Help (press any key to close) ')

  win.bkgdset Curses.color_pair(Curses::COLOR_WHITE)
  lines.first(h - 2).each_with_index do |line, i|
    win.setpos(i + 1, 2)
    win.addstr(line.chomp[0, w - 4])
  end
  win.refresh
  Curses.stdscr.timeout = -1
  Curses.getch
  Curses.stdscr.timeout = 100
  win.close
  Rfd::Window.draw_borders
  Curses.refresh
  ls
end

#last_page?Boolean

Do we have more pages?

Returns:

  • (Boolean)


250
251
252
# File 'lib/rfd/controller.rb', line 250

def last_page?
  current_page == total_pages - 1
end

#marked_itemsObject

  • marked files and directories.



177
178
179
# File 'lib/rfd/controller.rb', line 177

def marked_items
  items.select(&:marked?)
end

#max_itemsObject

Number of files or directories that the current main window can show in a page.



232
233
234
# File 'lib/rfd/controller.rb', line 232

def max_items
  main.max_items
end

#maxyObject

Height of the currently active pane.



227
228
229
# File 'lib/rfd/controller.rb', line 227

def maxy
  main.maxy
end

#move_cursor(row = nil) ⇒ Object

Move the cursor to specified row.

The main window and the headers will be updated reflecting the displayed files and directories. The row number can be out of range of the current page.



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/rfd/controller.rb', line 192

def move_cursor(row = nil)
  if row
    if (prev_item = items[current_row])
      main.draw_item prev_item
    end
    page = row / max_items
    switch_page page if page != current_page
    main.activate_pane row / maxy
    @current_row = row
  else
    @current_row = items.size > 2 ? 2 : 0
  end

  item = items[current_row]
  main.draw_item item, current: true
  main.display current_page

  header_l.draw_current_file_info item
  @sub_window.render if @sub_window
  @current_row
end

#move_cursor_by_click(y: nil, x: nil) ⇒ Object



365
366
367
368
369
370
# File 'lib/rfd/controller.rb', line 365

def move_cursor_by_click(y: nil, x: nil)
  if (idx = main.pane_index_at(y: y, x: x))
    row = current_page * max_items + main.maxy * idx + y - main.begy
    move_cursor row if (row >= 0) && (row < items.size)
  end
end

#preview_clientObject



25
26
27
# File 'lib/rfd/controller.rb', line 25

def preview_client
  @preview_client
end

#process_command_line(preset_command: nil, default_argument: nil) ⇒ Object

Accept user input, and directly execute it as a Ruby method call to the controller.

Parameters

  • preset_command - A command that would be displayed at the command line before user input.

  • default_argument - A default argument for the command.



320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/rfd/controller.rb', line 320

def process_command_line(preset_command: nil, default_argument: nil)
  prompt = preset_command ? ":#{preset_command} " : ':'
  command_line.set_prompt prompt
  cmd, *args = command_line.get_command(prompt: prompt, default: default_argument).split(' ')
  if cmd && !cmd.empty?
    ret = self.public_send cmd, *args
    clear_command_line
    ret
  end
rescue Interrupt, Rfd::CommandCancelled
  clear_command_line
end

#process_shell_commandObject

Accept user input, and directly execute it in an external shell.



334
335
336
337
338
339
340
341
342
343
344
# File 'lib/rfd/controller.rb', line 334

def process_shell_command
  command_line.set_prompt ':!'
  cmd = command_line.get_command(prompt: ':!')[1..-1]
  execute_external_command pause: true do
    system cmd
  end
rescue Interrupt, Rfd::CommandCancelled
ensure
  command_line.clear
  command_line.noutrefresh
end

#runObject

The main loop.



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/rfd/controller.rb', line 73

def run
  Curses.stdscr.timeout = 100  # Non-blocking getch with 100ms timeout
  loop do
    begin
      # Check for async preview results
      if @preview_client&.ready?
        if (result = @preview_client.poll_result)
          render_preview_result(result)
          Curses.doupdate
        end
      end

      number_pressed = false
      c = Curses.getch
      next if c.nil? || c == -1  # Timeout, continue loop

      # Let sub_window handle input first if it wants to
      if @sub_window && @sub_window.handle_input(c)
        Curses.doupdate
        next
      end

      ret = case c
      when 10, 13  # enter, return
        enter
      when 27  # ESC
        q
      when ' '  # space
        space
      when 127, Curses::KEY_BACKSPACE, Curses::KEY_DC  # DEL, Backspace, Delete
        backspace
      when Curses::KEY_DOWN
        j
      when Curses::KEY_UP
        k
      when Curses::KEY_LEFT
        h
      when Curses::KEY_RIGHT
        l
      when Curses::KEY_NPAGE  # Page Down
        public_send :'^n'
      when Curses::KEY_PPAGE  # Page Up
        public_send :'^p'
      when Curses::KEY_HOME
        g
      when Curses::KEY_END
        G
      when Curses::KEY_CTRL_A..Curses::KEY_CTRL_Z
        chr = ((c - 1 + 65) ^ 0b0100000).chr
        public_send :"^#{chr}" if respond_to?(:"^#{chr}")
      when ?0..?9
        public_send c
        number_pressed = true
      when ?!..?~
        if respond_to? c
          public_send c
        else
          debug "key: #{c}" if ENV['DEBUG']
        end
      when Curses::KEY_MOUSE
        if (mouse_event = Curses.getmouse)
          case mouse_event.bstate
          when Curses::BUTTON1_CLICKED
            click y: mouse_event.y, x: mouse_event.x
          when Curses::BUTTON1_DOUBLE_CLICKED
            double_click y: mouse_event.y, x: mouse_event.x
          end
        end
      else
        debug "key: #{c}" if ENV['DEBUG']
      end
      Curses.doupdate if ret
      @times = nil unless number_pressed
    rescue StopIteration
      raise
    rescue => e
      Rfd.logger.error e if Rfd.logger
      command_line.show_error e.to_s
      raise if ENV['DEBUG']
    end
  end
ensure
  stop_preview_server
  print "\e[?25h"  # Restore cursor
  Curses.close_screen
end

#selected_itemsObject

Marked files and directories or Array(the current file or directory).

. and .. will not be included.



184
185
186
# File 'lib/rfd/controller.rb', line 184

def selected_items
  ((m = marked_items).any? ? m : Array(current_item)).reject {|i| %w(. ..).include? i.name}
end

#spawn_panes(num) ⇒ Object

Change the number of columns in the main window.



161
162
163
164
# File 'lib/rfd/controller.rb', line 161

def spawn_panes(num)
  main.number_of_panes = num
  @current_row = @current_page = 0
end

#start_preview_serverObject



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/rfd/controller.rb', line 29

def start_preview_server
  return if ENV['RFD_SKIP_PREVIEW_SERVER']

  @preview_socket_path = "/tmp/rfd_preview_#{Process.pid}.sock"
  File.unlink(@preview_socket_path) rescue nil

  # Fork the preview server process
  @preview_server_pid = fork do
    # Detach from terminal and close inherited file descriptors
    $stdin.reopen('/dev/null')
    $stdout.reopen('/dev/null')
    $stderr.reopen('/dev/null')

    $0 = 'rfd-preview-server'
    server = Preview::Server.new(@preview_socket_path)
    server.run
  end

  # In parent: wait for socket and connect client
  sleep 0.1  # Give server time to start
  @preview_client = Preview::Client.new(@preview_socket_path)
  retries = 20
  while retries > 0 && !@preview_client.connected?
    @preview_client.connect
    break if @preview_client.connected?
    sleep 0.05
    retries -= 1
  end
rescue => e
  # If server startup fails, log and continue without async preview
  Rfd.logger&.error("Preview server startup failed: #{e.message}")
  @preview_client = nil
end

#stop_preview_serverObject



63
64
65
66
67
68
69
70
# File 'lib/rfd/controller.rb', line 63

def stop_preview_server
  @preview_client&.close
  if @preview_server_pid
    Process.kill('TERM', @preview_server_pid) rescue nil
    Process.wait(@preview_server_pid) rescue nil
  end
  File.unlink(@preview_socket_path) rescue nil if @preview_socket_path
end

#switch_page(page) ⇒ Object

Move to the given page number.

Parameters

  • page - Target page number



263
264
265
266
267
# File 'lib/rfd/controller.rb', line 263

def switch_page(page)
  main.display (@current_page = page)
  @displayed_items = items[current_page * max_items, max_items]
  header_l.draw_path_and_page_number path: current_dir.path, current: current_page + 1, total: total_pages
end

#timesObject

Number of times to repeat the next command.



167
168
169
# File 'lib/rfd/controller.rb', line 167

def times
  (@times || 1).to_i
end

#toggle_markObject

Swktch on / off marking on the current file or directory.



281
282
283
# File 'lib/rfd/controller.rb', line 281

def toggle_mark
  main.toggle_mark current_item
end

#total_pagesObject

Number of pages in the current directory.



255
256
257
# File 'lib/rfd/controller.rb', line 255

def total_pages
  (items.size - 1) / max_items + 1
end