summary refs log tree commit diff stats
diff options
context:
space:
mode:
-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
+		if y < 0 then y += Ncurses.LINES end
+		Ncurses.mvchgat(y, x, len, Ncurses::A_BOLD, get_color(fg, bg), nil)
+	end
+
+	def color_reverse_at y, x=0, len=-1, fg=-1, bg=-1
+		if y < 0 then y += Ncurses.LINES end
+		Ncurses.mvchgat(y, x, len, Ncurses::A_REVERSE, get_color(fg, bg), nil)
+	end
+
+	def get_color(fg, bg)
+		n = bg+2 + 9*(fg+2)
+		color = @@colortable[n]
+		unless color
+			# create a new pair
+			size = @@colortable.reject{|x| x.nil? }.size + 1
+			Ncurses::init_pair(size, fg, bg)
+			color = @@colortable[n] = size
+		end
+		return color
+	end
+
+	def bold(b = true)
+		if b
+			Ncurses.attron(Ncurses::A_BOLD) 
+		else
+			Ncurses.attroff(Ncurses::A_BOLD) 
+		end
+	end
+	def reverse(b = true)
+		if b
+			Ncurses.attron(Ncurses::A_REVERSE) 
+		else
+			Ncurses.attroff(Ncurses::A_REVERSE) 
+		end
+	end
+end