summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorhut <hut@lavabit.com>2009-04-08 00:00:00 +0200
committerhut <hut@lavabit.com>2009-04-08 00:00:00 +0200
commitadfea091f816cc2f4007e99b6b2be35a821857da (patch)
treebab1dc55cda8e2da2f8168d2d85278734b26f75e
the first usable version.v0.1.0
I was not using git yet, this was a simple backup
-rw-r--r--ChangeLog9
-rw-r--r--code/extensions.rb176
-rw-r--r--code/fm.rb357
-rw-r--r--code/keys.rb323
-rw-r--r--code/old_fm.rb233
-rw-r--r--code/types.rb26
-rwxr-xr-xfm41
-rw-r--r--interface/ncurses.rb182
8 files changed, 1347 insertions, 0 deletions
diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 00000000..bb331506
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,9 @@
+Version 0.1
+
+2009-04-08 hut
+ Well-running Version with some bugs here and there.
+ Starting new minor version
+
+2009-04-03 hut
+ Project was created.
+
diff --git a/code/extensions.rb b/code/extensions.rb
new file mode 100644
index 00000000..db238614
--- /dev/null
+++ b/code/extensions.rb
@@ -0,0 +1,176 @@
+class Directory
+ def initialize(path)
+ @path = path
+ @pos = 0
+ @pointed_file = ''
+ @width = 0
+ refresh
+ end
+
+ attr_reader(:path, :files, :pos, :width, :infos)
+
+ def pos=(x)
+ @pos = x
+ @pointed_file = @files[x]
+ resize
+ end
+
+ def pointed_file=(x)
+ if @files.include?(x)
+ @pointed_file = x
+ @pos = @files.index(x)
+ else
+ self.pos = 0
+ end
+ resize
+ end
+
+ def size() @files.size end
+
+ def resize()
+ pos = Fm.get_offset(self, lines)
+ if @files.empty?
+ @width = 0
+ else
+ @width = 0
+ @files[pos, lines-2].each_with_index do |fn, ix|
+ ix += pos
+# log File.basename(fn) + @infos[ix]
+ sz = File.basename(fn).size + @infos[ix].size + 2
+ @width = sz if @width < sz
+ end
+# @width = @files[pos,lines-2].map{|x| File.basename(x).size}.max
+ end
+ end
+
+ def get_file_infos()
+ @infos = []
+ @files.each do |fn|
+ if File.directory?(fn)
+ begin
+ sz = Dir.entries(fn).size - 2
+ rescue
+ sz = "?"
+ end
+ @infos << "#{sz}"
+ else
+ if File.size?(fn)
+ @infos << " #{File.size(fn).bytes 2}"
+ else
+ @infos << ""
+ end
+ end
+ end
+ end
+
+ def refresh()
+ glob = Dir.new(@path).to_a.sort!
+ if OPTIONS['hidden']
+ glob -= ['.', '..', 'lost+found']
+ else
+ glob.reject!{|x| x[0] == ?. or x == 'lost+found'}
+ end
+ if glob.empty?
+ glob = ['.']
+ end
+ glob.map!{|x| File.join(@path, x)}
+ dirs = glob.select{|x| File.directory?(x)}
+ @files = dirs + (glob - dirs)
+
+ get_file_infos
+ resize
+
+ if @pos >= @files.size
+ @pos = @files.size - 1
+ elsif @files.include?(@pointed_file)
+ @pos = @files.index(@pointed_file)
+ end
+ end
+end
+
+
+class File
+ MODES_HASH = {
+ '0' => '---',
+ '1' => '--x',
+ '2' => '-w-',
+ '3' => '-wx',
+ '4' => 'r--',
+ '5' => 'r-x',
+ '6' => 'rw-',
+ '7' => 'rwx'
+ }
+ def self.modestr(f)
+ unless exists?(f)
+ return '----------'
+ end
+
+ if symlink?(f)
+ result = 'l'
+ elsif directory?(f)
+ result = 'd'
+ else
+ result = '-'
+ end
+
+ s = ("%o" % File.stat(f).mode)[-3..-1]
+ for m in s.each_char
+ result << MODES_HASH[m]
+ end
+
+ result
+ end
+end
+
+class Numeric
+ def limit(max, min = 0)
+ self < min ? min : (self > max ? max : self)
+ end
+
+ def bytes n_round = 2
+ n = 1024
+ a = %w(B K M G T Y)
+
+ i = 0
+ flt = self.to_f
+
+ while flt > n and i < a.length - 1
+ flt /= n
+ i += 1
+ end
+
+# flt = flt.round(n_round)
+ r = 10 ** n_round
+ flt *= r
+ flt = flt.round.to_f / r
+ int = flt.to_i
+ flt = int if int == flt
+
+ return flt.to_s + ' ' + a[i]
+ end
+end
+
+class Array
+ def wrap(n)
+ n.times { push shift }
+ end
+end
+
+class String
+ def clear
+ replace String.new
+ end
+ def sh
+ res = dup
+ res.gsub!('\\\\', "\000")
+ res.gsub!(' ', '\\ ')
+ res.gsub!('(', '\\(')
+ res.gsub!(')', '\\)')
+ res.gsub!('*', '\\*')
+ res.gsub!('\'', '\\\'')
+ res.gsub!('"', '\\"')
+ res.gsub!("\000", '\\\\')
+ return res
+ end
+end
+
diff --git a/code/fm.rb b/code/fm.rb
new file mode 100644
index 00000000..307866f8
--- /dev/null
+++ b/code/fm.rb
@@ -0,0 +1,357 @@
+
+OPTIONS = {
+ 'hidden' => false,
+}
+
+module Fm
+ def self.initialize
+ @buffer = ''
+ @pwd = ''
+ @copy = []
+ @ignore_until = nil
+ @trash = File.expand_path('~/.trash')
+ pwd = Dir.getwd
+
+ @memory = {
+ '`' => pwd,
+ '\'' => pwd
+ }
+
+ @fmrc = File.expand_path('~/.fmrc')
+ if (File.exists?(@fmrc))
+ loaded = Marshal.load(File.read(@fmrc))
+ if Hash === loaded
+ @memory.update(loaded)
+ end
+ end
+
+ @memory['0'] = pwd
+
+ @dirs = Hash.new() do |hash, key|
+ hash[key] = Directory.new(key)
+ end
+
+ @path = [@dirs['/']]
+ enter_dir(Dir.pwd)
+ end
+ attr_reader(:dirs, :pwd)
+
+ VI = "vi -c 'map h :quit<CR>' -c 'map q :quit<CR>' -c 'map H :unmap h<CR>:unmap H<CR>' %s"
+
+ def self.dump
+ remember_dir
+ dumped = Marshal.dump(@memory)
+ File.open(@fmrc, 'w') do |f|
+ f.write(dumped)
+ end
+ end
+
+ def self.rescue_me
+ @buffer = ''
+ sleep 0.2
+ end
+
+ def self.main_loop
+ while true
+ begin
+ draw()
+ rescue Interrupt
+ rescue_me
+ end
+ begin
+ press(geti)
+ rescue Interrupt
+ rescue_me
+ end
+ end
+ end
+
+ def self.current_path() @pwd.path end
+
+ def self.enter_dir(dir)
+ dir = File.expand_path(dir)
+
+ oldpath = @path.dup
+
+ # NOTE: @dirs[unknown] is not nil but Directory.new(unknown)
+ @path = [@dirs['/']]
+ unless dir == '/'
+ dir.slice(0)
+ accumulated = '/'
+ for part in dir.split('/')
+ unless part.empty?
+ accumulated = File.join(accumulated, part)
+ @path << @dirs[accumulated]
+ end
+ end
+ end
+ @pwd = @path.last
+ set_title "fm: #{@pwd.path}"
+
+ if @path.size < oldpath.size
+ @pwd.pos = @pwd.files.index(oldpath.last.path) || 0
+ end
+
+ i = 0
+
+ @path.each_with_index do |p, i|
+ p.refresh
+ unless i == @path.size - 1
+ p.pointed_file = @path[i+1].path
+ end
+ end
+
+ Dir.chdir(@pwd.path)
+ end
+
+ def self.currentfile
+ @dirs[@currentdir][1][@dirs[@currentdir][0] || 0]
+ end
+ def self.currentfile() @pwd.files.at(@pwd.pos) end
+
+ def self.get_offset(dir, max)
+ pos = dir.pos
+ len = dir.files.size
+ max -= 2
+ if len <= max or pos < max/2
+ return 0
+ elsif pos >= (len - max/2)
+ return len - max
+ else
+ return pos - max/2
+ end
+ end
+
+ COLUMNS = 4
+
+ def self.get_boundaries(column)
+ cols = Interface.cols # to cache
+ case column
+ when 0
+ return 0, cols / 8 - 1
+
+ when 1
+ q = cols / 8
+ return q, q
+
+ when 2
+ q = cols / 4
+ w = @path.last.width.limit(cols/2, cols/8) + 1
+ return q, w
+
+ when 3
+ l = cols / 4 + 1
+ l += @path.last.width.limit(cols/2, cols/8)
+
+ return l, cols - l
+
+ end
+ end
+
+ def self.put_directory(c, d)
+ l = 0
+ if d
+ infos = (c == COLUMNS - 2)
+ left, wid = get_boundaries(c)
+
+ offset = get_offset(d, lines)
+ (lines - 1).times do |l|
+ lpo = l + offset
+ bg = -1
+ break if (f = d.files[lpo]) == nil
+
+ dir = false
+ if File.symlink?(f)
+ bld = true
+ if File.exists?(f)
+ clr = [6, bg]
+ else
+ clr = [1, bg]
+ end
+ dir = File.directory?(f)
+ elsif File.directory?(f)
+ bld = true
+ dir = true
+ clr = [4, bg]
+ elsif File.executable?(f)
+ bld = true
+ clr = [2, bg]
+ else
+ bld = false
+ clr = [7, bg]
+ end
+
+ fn = File.basename(f)
+ if infos
+ myinfo = " #{d.infos[lpo]} "
+ str = fn[0, wid-1].ljust(wid)
+ if str.size > myinfo.size
+ str[-myinfo.size..-1] = myinfo
+ yes = true
+ else
+ yes = false
+ end
+ puti l+1, left, str
+ if dir and yes
+ args = l+1, left+wid-myinfo.size, myinfo.size, *clr
+ color_bold_at(*args)
+ end
+ else
+ puti l+1, left, fn[0, wid-1].ljust(wid+1)
+ end
+
+ args = l+1, left, fn.size.limit(wid-1), *clr
+
+ if d.pos == lpo
+ color_reverse_at(*args)
+ else
+ if bld then color_bold_at(*args) else color_at(*args) end
+ end
+ end
+ end
+
+ column_clear(c, l)
+ end
+
+ def self.column_clear(n, from=0)
+ color(-1,-1)
+ left, wid = get_boundaries(n)
+ (from -1).upto(lines) do |l|
+ puti l+2, left, ' ' * (wid)
+ end
+ end
+
+ def self.column_put_file(n, file)
+ m = lines - 2
+ i = 0
+ color 7
+ bold false
+ File.open(file, 'r') do |f|
+ check = true
+ left, wid = get_boundaries(n)
+ f.lines.each do |l|
+ if check
+ check = false
+ break unless l.each_char.all? {|x| x[0] > 0 and x[0] < 128}
+ end
+ puti i+1, left, l.gsub("\t"," ")[0, wid-1].ljust(wid)
+ i += 1
+ break if i == m
+ end
+ end
+ column_clear(n, i)
+ end
+
+ def self.draw
+ bold false
+ @cur_y = get_boundaries(COLUMNS-2)[0]
+ @pwd.get_file_infos
+
+ s1 = " "
+ s2 = "#{@path.last.path}#{"/" unless @path.size == 1}"
+ f = currentfile
+ s3 = "#{f ? File.basename(f) : ''}"
+
+ puti 0, (s1 + s2 + s3).ljust(cols)
+
+ bg = -1
+ color_at 0, 0, -1, 7, bg
+ color_at 0, 0, s1.size, 7, bg
+ color_at 0, s1.size, s2.size, 6, bg
+ color_at 0, s1.size + s2.size, s3.size, 5, bg
+
+ bold false
+
+ f = currentfile
+ begin
+ if File.directory?(f)
+ put_directory(3, @dirs[currentfile])
+ else
+ column_put_file(3, currentfile)
+ end
+ rescue
+ column_clear(3)
+ end
+
+ pos_constant = @path.size - COLUMNS + 1
+
+ (COLUMNS - 1).times do |c|
+ pos = pos_constant + c
+
+ if pos >= 0
+ put_directory(c, @path[pos])
+ else
+ column_clear(c)
+ end
+ end
+
+ bold false
+ color -1, -1
+ puti -1, "#@buffer #{@pwd.pos+1},#{@pwd.size},#{@path.size} ".rjust(cols)
+ more = ''
+ if File.symlink?(currentfile)
+ more = "#{File.readlink(currentfile)}"
+ end
+ puti -1, " #{Time.now.strftime("%H:%M:%S %a %b %d")} #{File.modestr(currentfile)} #{more}"
+
+ color_at -1, 23, 10, (File.writable?(currentfile) ? 6 : 5), -1
+ if more
+ color_at -1, 34, more.size, (File.exists?(currentfile) ? 6 : 1), -1
+ end
+
+ movi(@pwd.pos + 1 - get_offset(@pwd, lines), @cur_y)
+ end
+
+ def self.enter_dir_safely(dir)
+ dir = File.expand_path(dir)
+ if File.exists?(dir) and File.directory?(dir)
+ olddir = @pwd.path
+ begin
+ enter_dir(dir)
+ return true
+ rescue
+ enter_dir(olddir)
+ return false
+ end
+ end
+ end
+
+ def self.move_to_trash!(fn)
+ unless File.exists?(@trash)
+ Dir.mkdir(@trash)
+ end
+ new_path = File.join(@trash, File.basename(fn))
+
+ closei
+ system('mv','-v', fn, new_path)
+ starti
+
+ return new_path
+ end
+
+ def self.in_trash?(fn)
+ fn[0,@trash.size] == @trash
+ end
+
+ def self.move_to_trash(fn)
+ if fn and File.exists?(fn)
+ if File.directory?(fn)
+ if !in_trash?(fn) and Dir.entries(fn).size > 2
+ return move_to_trash!(fn)
+ else
+ Dir.rmdir(fn) rescue nil
+ end
+ elsif File.symlink?(fn)
+ File.delete(fn)
+ else
+ if !in_trash?(fn) and File.size?(fn)
+ return move_to_trash!(fn)
+ else
+ File.delete(fn)
+ end
+ end
+ end
+ return nil
+ end
+end
+
diff --git a/code/keys.rb b/code/keys.rb
new file mode 100644
index 00000000..0ce660a9
--- /dev/null
+++ b/code/keys.rb
@@ -0,0 +1,323 @@
+module Fm
+ # ALL combinations of multiple keys (without the last letter)
+ # or regexps which match combinations need to be in here!
+ COMBS = %w(
+ g d y c Z rmdi t
+ /[m`']/ /[f/!].*/
+ /(cw|cd|mv).*/
+ /m(k(d(i(r(.*)?)?)?)?)?/
+ /r(e(n(a(m(e(.*)?)?)?)?)?)?/
+ )
+
+ # Create a regular expression which detects these combos
+ ary = []
+ for token in COMBS
+ if token =~ /^\/(.*)\/$/
+ ary << $1
+ elsif token.size > 0
+ ary << token.each_char.map {|t| "(?:#{t}" }.join +
+ (')?' * (token.size - 1)) + ')'
+ end
+ end
+ REGX = Regexp.new('^(?:' + ary.uniq.join('|') + ')$')
+
+ def self.ignore_keys_for(t)
+ @ignore_until = Time.now + t
+ end
+
+ def self.search(str, offset=0, backwards=false)
+ rx = Regexp.new(str, Regexp::IGNORECASE)
+
+ ary = @pwd.files.dup
+ ary.wrap(@pwd.pos + offset)
+
+ ary.reverse! if backwards
+
+ for f in ary
+ g = File.basename(f)
+ if g =~ rx
+ @pwd.pointed_file = f
+ break
+ end
+ end
+ end
+
+ def self.hints(str)
+ rx = Regexp.new(str, Regexp::IGNORECASE)
+
+ ary = @pwd.files.dup
+ ary.wrap(@pwd.pos)
+
+ n = 0
+ pointed = false
+ for f in ary
+ g = File.basename(f)
+ if g =~ rx
+ unless pointed
+ @pwd.pointed_file = f
+ pointed = true
+ end
+ n += 1
+ end
+ end
+
+ return n
+ end
+
+ def self.remember_dir
+ @memory["`"] = @memory["'"] = @pwd.path
+ end
+
+ def self.press(key)
+ return if @ignore_until and Time.now < @ignore_until
+
+ @ignore_until = nil
+
+ case @buffer << key
+
+ when '<redraw>'
+ closei
+ starti
+
+ when 'j'
+ if @pwd.size == 0
+ @pwd.pos = 0
+ elsif @pwd.pos >= @pwd.size - 1
+ @pwd.pos = @pwd.size - 1
+ else
+ @pwd.pos += 1
+ end
+
+ when 's'
+ closei
+ system('clear')
+ system('ls', '--color=auto', '--group-directories-first')
+ system('bash')
+ @pwd.refresh
+ starti
+
+ when 'J'
+ (lines/2).times { press 'j' }
+
+ when 'K'
+ (lines/2).times { press 'k' }
+
+ when 'cp', 'yy'
+ @copy = [currentfile]
+
+ when 'n'
+ search(@search_string, 1)
+
+ when 'x'
+ fork {
+ sleep 1
+ Ncurses.ungetch(104)
+ }
+
+ when 'N'
+ search(@search_string, 0, true)
+
+ when 'fh'
+ @buffer.clear
+ press('h')
+
+ when /^f(.+)$/
+ str = $1
+ if @buffer =~ /^(.*).<bs>$/
+ @buffer = $1
+ elsif str =~ /^\s?(.*)(L|;|<cr>|<esc>)$/
+ @buffer = ''
+ @search_string = $1
+ press('l') if $2 == ';' or $2 == 'L'
+ else
+ test = hints(str)
+ if test == 1
+ @buffer = ''
+ press('l')
+ ignore_keys_for 0.5
+ elsif test == 0
+ @buffer = ''
+ ignore_keys_for 1
+ end
+ end
+
+ when /^\/(.+)$/
+ str = $1
+ if @buffer =~ /^(.*).<bs>$/
+ @buffer = $1
+ elsif str =~ /^\s?(.*)(L|;|<cr>|<esc>)$/
+ @buffer = ''
+ @search_string = $1
+ press('l') if $2 == ';' or $2 == 'L'
+ else
+ search(str)
+ end
+
+ when /^mkdir(.*)$/
+ str = $1
+ if @buffer =~ /^(.*).<bs>$/
+ @buffer = $1
+ elsif str =~ /^\s?(.*)(<cr>|<esc>)$/
+ @buffer = ''
+ if $2 == '<cr>'
+ closei
+ system('mkdir', $1)
+ starti
+ @pwd.refresh
+ end
+ end
+
+ when /^!(.+)$/
+ str = $1
+ if @buffer =~ /^(.*).<bs>$/
+ @buffer = $1
+ elsif str =~ /^(\!?)(.*)(<cr>|<esc>)$/
+ @buffer = ''
+ if $3 == '<cr>'
+ closei
+ system("bash", "-c", $2)
+ gets unless $1.empty?
+ starti
+ @pwd.refresh
+ end
+ end
+
+ when /^cd(.+)$/
+ str = $1
+ if @buffer =~ /^(.*).<bs>$/
+ @buffer = $1
+ elsif str =~ /^\s?(.*)(<cr>|<esc>)$/
+ @buffer = ''
+ if $2 == '<cr>'
+ remember_dir
+ enter_dir_safely($1)
+ end
+ end
+
+ when /^(?:mv|cw|rename)(.+)$/
+ str = $1
+ if @buffer =~ /^(.*).<bs>$/
+ @buffer = $1
+ elsif str =~ /^\s?(.*)(<cr>|<esc>)$/
+ @buffer = ''
+ if $2 == '<cr>'
+ closei
+ system('mv', '-v', currentfile, $1)
+ starti
+ @pwd.refresh
+ end
+ end
+
+ when 'th'
+ OPTIONS['hidden'] ^= true
+ @pwd.refresh
+
+ when 'rmdir'
+ cf = currentfile
+ if cf and File.exists?(cf)
+ if File.directory?(cf)
+ system('rm', '-r', cf)
+ @pwd.refresh
+ end
+ end
+
+ when 'p'
+ unless @copy.empty?
+ closei
+ system('cp','-v',*(@copy+[@pwd.path]))
+ starti
+ @pwd.refresh
+ end
+
+ when /^[`'](.)$/
+ if dir = @memory[$1] and not @pwd.path == dir
+ remember_dir
+ enter_dir_safely(dir)
+ end
+
+ when /^m(.)$/
+ @memory[$1] = @pwd.path
+
+ when 'gg'
+ @pwd.pos = 0
+
+ when 'dd'
+ new_path = move_to_trash(currentfile)
+ @copy = [new_path] if new_path
+ @pwd.refresh
+
+ when 'dD'
+ cf = currentfile
+ if cf and File.exists?(cf)
+ if File.directory?(cf)
+ Dir.delete(cf) rescue nil
+ else
+ File.delete(cf) rescue nil
+ end
+ @pwd.refresh
+ end
+
+ when 'g0'
+ remember_dir
+ enter_dir('/')
+
+ when 'gh'
+ remember_dir
+ enter_dir('~')
+
+ when 'gu'
+ remember_dir
+ enter_dir('/usr')
+
+ when 'ge'
+ remember_dir
+ enter_dir('/etc')
+
+ when 'gm'
+ remember_dir
+ enter_dir('/media')
+
+ when 'gt'
+ remember_dir
+ enter_dir('~/.trash')
+
+ when 'G'
+ @pwd.pos = @pwd.size - 1
+
+ when 'k'
+ @pwd.pos -= 1
+ @pwd.pos = 0 if @pwd.pos < 0
+
+ when '<bs>', 'h', 'H'
+ enter_dir(@buffer=='H' ? '..' : @path[-2].path) unless @path.size == 1
+
+ when 'E'
+ cf = currentfile
+ unless cf.nil? or enter_dir_safely(cf)
+ closei
+ system VI % cf
+ starti
+ end
+
+ when '<cr>', 'l', ';', 'L'
+ cf = currentfile
+ unless cf.nil? or enter_dir_safely(cf)
+ handler, wait = getfilehandler(currentfile)
+ if handler
+ closei
+ system(handler)
+ if @buffer == 'L'
+ gets
+ end
+ starti
+ end
+ end
+
+ when 'q', 'ZZ', "\004"
+ exit
+
+ end
+
+ @buffer = '' unless @buffer == '' or @buffer =~ REGX
+ end
+end
diff --git a/code/old_fm.rb b/code/old_fm.rb
new file mode 100644
index 00000000..6d868ebb
--- /dev/null
+++ b/code/old_fm.rb
@@ -0,0 +1,233 @@
+class Directory
+ def initialize(path)
+ @path = path
+ @pos = 0
+ refresh
+ end
+
+ attr_reader(:path, :files)
+ attr_accessor(:pos)
+
+ def refresh()
+ @files = Dir::glob(File::join(path, '*')).sort!
+ end
+ def self.current()
+ Fm.current_dir()
+ end
+end
+
+module Fm
+ def self.initialize
+ @key = ''
+ @dirs = {}
+ @current_dir = ''
+ enter_dir(Dir.getwd)
+ end
+
+ def self.current_path() self.current_dir.path end
+
+ attr_reader(:dirs, :current_dir)
+
+# {
+# "/" => [ 2,
+# ["usr", "home", "root", "etc"] ],
+# ...
+# }
+
+
+ def self.getfilehandler(file)
+ bn = File.basename(file)
+ case bn
+ when /\.(avi|mpg|flv)$/
+ "mplayer #{file} >> /dev/null"
+ when /\.(jpe?g|png)$/
+ "feh '#{file}'"
+ when /\.m3u$/
+ "cmus-remote -c && cmus-remote -P #{file} && cmus-remote -C 'set play_library=false' && sleep 0.3 && cmus-remote -n"
+ end
+ end
+
+ def self.enter_dir(dir)
+ dir = File.expand_path(dir)
+ olddirs = @dirs.dup
+ @dirs = {}
+
+ cur = 0
+ got = ""
+ ary = dir.split('/')
+ if ary == []; ary = [''] end
+ ["", *ary].each do |folder|
+ got = File.join(got, folder)
+ cur = olddirs.has_key?(got) ? olddirs[got][0] : 0
+ @dirs[got] = [cur, Dir.glob(File.join(got, '*')).sort]
+ end
+
+ # quick fix, sets the cursor correctly when entering ".."
+ if @dirs.size < olddirs.size
+ @dirs[@currentdir] = olddirs[@currentdir]
+ @dirs[got][0] = @dirs[got][1].index(@currentdir) || 0
+ end
+
+# log @dirs
+
+ @currentdir = got
+# @dirs[dir] = Dir[File.join(dir, '*')]
+ Dir.chdir(got)
+
+# log(@dirs)
+ end
+
+ def self.cursor() @dirs[@currentdir][0] end
+ def self.cursor=(x) @dirs[@currentdir][0] = x end
+
+ def self.currentdir() @dirs[@currentdir][1] end
+
+ def self.currentfile
+ @dirs[@currentdir][1][@dirs[@currentdir][0] || 0]
+ end
+
+ def self.get_offset(dir, max)
+ pos = dir[0]
+ len = dir[1].size
+ max -= 2
+ if len <= max or pos < max/2
+ return 0
+ elsif pos > (len - max/2)
+ return len - max
+ else
+ return pos - max/2
+ end
+ end
+
+ def self.put_directory(c, d)
+ l = 0
+ unless d == nil
+ offset = get_offset(d, lines)
+ (lines - 1).times do |l|
+ lpo = l + offset
+ break if (f = d[1][lpo]) == nil
+
+ if File.symlink?(f)
+ color(3)
+ elsif File.directory?(f)
+ color(4)
+ elsif File.executable?(f)
+ color(2)
+ else
+ color(7)
+ end
+ puti l+1, c*@wid, File.basename(f).ljust(@wid-1)[0, @wid]
+ end
+ end
+
+ column_clear(c, l)
+ end
+
+ def self.column_clear(n, from=0)
+ (from -1).upto(lines) do |l|
+ puti l+2, (n * @wid), ' ' * @wid
+ end
+ end
+
+ def self.column_put_file(n, file)
+ m = lines
+ i = 0
+ File.open(file, 'r') do |f|
+ f.lines.each do |l|
+ puti i+1, n * @wid, l.gsub("\t"," ")[0, @wid-1].ljust(@wid-1)
+ i += 1
+ break if i == m
+ end
+ end
+ column_clear(n, i)
+ end
+
+ def self.draw
+ color 7
+ puti 0, 3, "pwd: #{@current_path}".ljust(cols)
+
+ if @dirs.size == 1
+ @temp = [nil, @dirs["/"]]
+ else
+ left = @dirs[@currentdir[0, @currentdir.rindex('/')]]
+ left ||= @dirs['/']
+ @temp = [left, @dirs[@currentdir]]
+ end
+
+ @wid = cols / 3
+ f = currentfile
+ begin
+ if File.directory?(f)
+ put_directory(2, [0, Dir.glob(File.join(f, '*')).sort])
+ else
+ column_put_file(2, currentfile)
+ end
+ rescue
+ column_clear(2)
+ end
+
+ 2.times do |c|
+ put_directory(c, @temp[c])
+ end
+
+
+ movi(self.cursor + 1 - get_offset(@dirs[@currentdir], lines), @wid)
+ end
+
+ # ALL combinations of multiple keys have to be in the COMBS array.
+ COMBS = %w(
+ gg
+ )
+ def self.main_loop
+ while true
+ draw
+
+ case @key << geti
+ when 'j'
+ self.cursor += 1
+ self.cursor = currentdir.size - 1 if self.cursor >= currentdir.size
+
+ when 'gg'
+ self.cursor = 0
+
+ when 'gh'
+ enter_dir('~')
+
+ when 'G'
+ self.cursor = currentdir.size - 1
+
+ when 'k'
+ self.cursor -= 1
+ self.cursor = 0 if self.cursor < 0
+
+ when '<bs>', 'h'
+ enter_dir('..') unless @dirs.size == 1
+
+ when '<cr>', 'l'
+ if File.directory?(currentfile || '')
+ begin
+ olddir = @currentdir
+ enter_dir(currentfile)
+ rescue Exception
+ enter_dir olddir
+ end
+ else
+ h = getfilehandler(currentfile)
+ h and system(h)
+ end
+
+ when 'q'
+ break
+ end
+
+ unless @key == '' or COMBS.select{ |x|
+ x.size != @key.size and x.size > @key.size
+ }.map{ |x|
+ x[0, @key.size]
+ }.include? @key
+ @key = ''
+ end
+ end
+ end
+end
+
diff --git a/code/types.rb b/code/types.rb
new file mode 100644
index 00000000..7fc614e9
--- /dev/null
+++ b/code/types.rb
@@ -0,0 +1,26 @@
+module Fm
+ def self.getfilehandler(file)
+ bn = File.basename(file)
+ case bn
+ when /\.(avi|mpe?g|flv|mkv|ogm|mov|mp4|wmv|vob|php|divx?|mp3|ogg)$/i
+ return "mplayer -fs #{file.sh}", false
+ when /\.(jpe?g|png)$/i
+ return "feh #{file.sh}", false
+ when /\.(pdf)$/i
+ return "evince #{file.sh}"
+ when /\.(txt)$/i
+ return VI % file.sh
+ when /\.wav$/i
+ return "aplay -q #{file.sh}"
+ when /\.m3u$/i
+ return "cmus-remote -c && cmus-remote -P #{file} && cmus-remote -C 'set play_library=false' && sleep 0.3 && cmus-remote -n", false
+ end
+
+ if File.executable?(file)
+ return "#{file.sh}", true
+ end
+
+ return VI % file.sh
+ end
+end
+
diff --git a/fm b/fm
new file mode 100755
index 00000000..7133192f
--- /dev/null
+++ b/fm
@@ -0,0 +1,41 @@
+#!/usr/bin/ruby
+
+def File::resolve_symlink( path = __FILE__ )
+ path = readlink(path) while symlink?(path)
+ expand_path(path)
+end
+
+def require_from_here ( *list )
+ require File.join( FM_DIR, *list )
+end
+
+$: << FM_DIR = File::dirname(File::resolve_symlink)
+
+require 'ftools'
+require 'pp'
+
+require_from_here 'interface/ncurses.rb'
+require_from_here 'code/fm.rb'
+require_from_here 'code/keys.rb'
+require_from_here 'code/types.rb'
+require_from_here 'code/extensions.rb'
+include Interface
+
+ERROR_STREAM = File.open('/tmp/errorlog', 'a')
+def log(obj)
+ $stdout = ERROR_STREAM
+ pp obj
+ $stdout.flush
+ $stdout = STDOUT
+ obj
+end
+
+END {
+ closei
+ Fm.dump
+ ERROR_STREAM.close
+}
+
+Fm.initialize
+Fm.main_loop
+
diff --git a/interface/ncurses.rb b/interface/ncurses.rb
new file mode 100644
index 00000000..0d2659e4
--- /dev/null
+++ b/interface/ncurses.rb
@@ -0,0 +1,182 @@
+require 'ncurses'
+
+module Interface
+ def self.keytable(key)
+ case key
+ when 12
+ '<redraw>'
+ when ?\n
+ '<cr>'
+ when ?\b, Ncurses::KEY_BACKSPACE
+ '<bs>'
+ when ?\e
+ '<esc>'
+ when ?\t
+ '<tab>'
+ when 32
+ ' '
+ when 0..127
+ key.chr
+ else
+ ''
+ end
+ end
+
+# def key c#{{{
+# case c
+# when 12
+# :redraw
+# when ?\n
+# :enter
+# when ?\b, Ncurses::KEY_BACKSPACE
+# :backspace
+# when 32
+# :space
+# when ?\t
+# :tab
+# when Ncurses::KEY_BTAB
+# :TAB
+# when ?\e
+# :escape
+# when 0..127
+# c
+# when Ncurses::KEY_F1..Ncurses::KEY_F30
+# ('F' + (c-Ncurses::KEY_F1+1).to_s).to_sym
+# when Ncurses::KEY_HOME
+# :home
+# when Ncurses::KEY_END
+# :end
+# when Ncurses::KEY_RESIZE
+# :resize
+# when Ncurses::KEY_DC
+# :delete
+# when Ncurses::KEY_ENTER
+# ?\n
+# when Ncurses::KEY_RIGHT
+# :right
+# when Ncurses::KEY_LEFT
+# :left
+# when Ncurses::KEY_UP
+# :up
+# when Ncurses::KEY_DOWN
+# :down
+# when Ncurses::KEY_NPAGE
+# :pagedown
+# when Ncurses::KEY_PPAGE
+# :pageup
+# when Ncurses::KEY_IC
+# :insert
+# else
+## c
+# :error
+# end
+# end#}}}
+
+ def self.included(this)
+ @@window = Ncurses.initscr
+ starti
+ end
+
+ def starti
+ @@screen = Ncurses.stdscr
+ @@screen.keypad(true)
+ Ncurses.start_color
+ Ncurses.use_default_colors
+
+# Ncurses.cbreak
+ Ncurses.noecho
+ Ncurses.curs_set 0
+ Ncurses.halfdelay(1000)
+ @@colortable = []
+ end
+
+ def closei
+ Ncurses.echo
+ Ncurses.cbreak
+ Ncurses.curs_set 1
+ Ncurses.endwin
+ end
+
+ def geti
+ Interface::keytable(Ncurses.getch)
+ end
+
+ def set_title(x)
+# closei
+ print "\e]2;#{x}\007"
+# system('echo', '-n', '-e', '"\e2;' + x + '\007"')
+# starti
+ end
+
+ def lines
+ Ncurses.LINES
+ end
+
+ def cols
+ Ncurses.COLS
+ end
+
+ def movi(y=0, x=0)
+ y < 0 and y += lines
+ Ncurses.move(y, x)
+ end
+
+ def puti *args
+ case args.size
+ when 1
+ Ncurses.addstr(args[0].to_s)
+ when 2
+ if (y = args[0]) < 0 then y += Ncurses.LINES end
+ Ncurses.mvaddstr(y, 0, args[1].to_s)
+ when 3
+ if (y = args[0]) < 0 then y += Ncurses.LINES end
+ Ncurses.mvaddstr(y, args[1], args[2].to_s)
+ end
+ end
+
+ def color(fg = -1, bg = -1)
+ Ncurses.color_set(get_color(fg,bg), nil)
+ end
+
+ def color_at y, x=0, len=-1, fg=-1, bg=-1
+ if y < 0 then y += Ncurses.LINES end
+ Ncurses.mvchgat(y, x, len, 0, get_color(fg, bg), nil)
+ end
+
+ def color_bold_at y, x=0, len=-1, fg=-1, bg=-1
+