#!/usr/bin/env ruby

# diru is a tool for moving around in the users directory
# structure. diru provides input for the shell cd command for entering
# a new directory. However some commands do not produce input for cd.
#
# diru has hub (daemon) which creates new service (server) threads for
# diru clients. Before user can start using diru, a server should be
# created.
#
# diru has server side and client side. The server maintains
# information about users directories and also the state of the
# queries issued by the client. Client is what the end user uses.
#
# diru client command has to be integrated to the currently used
# shell in order to force the shell to change the current
# directory. Lets assume that the shell alias/function that user is
# actually using is "dr" for now. If diru returns success, then the
# diru output should be used as new directory. If diru status is
# failure, then directory should not be changed.
#
# Example of diru use:
#   shell> dr / build src
#
# This command means that the client sends to the server a request for
# directory search. The search starts from Diru root ("/") and the
# rest of the path should include the words "build", and
# "src". If path with this spec is found, the shell directory gets
# updated.
#
# Perform:
#   shell> dr i
#
# for some documentation.
#
# Diru setup:
#
# Start hub (deamon)
# * shell> diru --hub
#
# Open server for client.
# * shell> diru -s
#
# Use client
# * shell> diru -p 41115 -c r
#
#
# Feature list:
# * User root dir (downwards) search
# * Current directory (downwards) dir search
# * Bookmarking directories
# * Left-over search with multiple matches
# * Fast jump to specific common directories
# * Peer dir jump
# * Server state reset
# * Revert mode, unknown commands to cd
#


require 'como'
include Como
require 'drb'
require 'json'
require 'yaml'
require 'fileutils'

# require 'byebug'


Spec.command( 'diru', 'Tero Isannainen', '2017',
    [
        [ :switch,        'hub',      nil,    "H:   Start hub." ],
        [ :opt_single,    'hport',    nil,    "H:   Hub port (default: DIRU_HUB_PORT or 41114)." ],
        [ :switch,        'hkill',    nil,    "H:   Kill hub." ],
        [ :switch,        'nodaemon', nil,    "H:   No daemon for server." ],
        [ :opt_any,       'server',   '-s',   "S:   Open server for user (default: $USER)." ],
        [ :opt_single,    'kill',     '-k',   "S:   Close server." ],
        [ :switch,        'list',     '-l',   "S:   List servers." ],
        [ :opt_single,    'options',  '-o',   "SC:  Options File." ],
        [ :opt_single,    'port',     '-p',   "SC:  Server port for client (default: DIRU_PORT or ~/.diru.prt)." ],
        [ :switch,        'change',   '-c',   "C:   Change dir (i.e. target is to change dir)." ],
        [ :exclusive,     'template', '-t',   "C:   Display Options File template." ],
        [ :exclusive,     'cmddoc',   '-d',   "C:   Display command documentation." ],
        [ :default,       nil,         nil,   "C:   Client commands (try: diru i)." ],
    ] )



# ------------------------------------------------------------
# Common:
# ------------------------------------------------------------


# Diru common features.
module Diru

    # Default port.
    DIRU_HUB_PORT = ENV['DIRU_HUB_PORT'] || 41114


    # See: Diru.load_conf
    def load_conf( conf_file )
        @conf = conf_file.load
    end


    # Error message display.
    def Diru.error( msg )
        STDERR.puts "Diru Error: #{msg}"
        exit( false )
    end


    # Warning message display.
    def Diru.warn( msg )
        STDERR.puts "Diru Warning: #{msg}"
    end


    # List the dir content (default: pwd).
    def Diru.list( dir = '.', glob = '*' )
        Dir.glob( "#{dir}/#{glob}" )
    end


    # List the dir files (default: pwd).
    def Diru.list_files( dir = '.', glob = '*' )
        Diru.list( dir, glob ).select do |i| File.file?(i) == true end
    end


    # Find file entry from directory hierarchy upwards.
    def Diru.find_upper_file( file, dir = Dir.pwd )
        found = Diru.list_files( dir, file )
        if found.empty?
            dir = File.dirname( dir )
            if dir == "/"
                raise RuntimeError, "Could not find file \"#{file}\"!"
            else
                return Diru.find_upper_file( file, dir )
            end
        else
            return found[0]
        end
    end


    # Find file entry from directory hierarchy upwards.
    def Diru.find_an_upper_file( files, dir = Dir.pwd )
        found = nil

        # Process all directory levels.
        while true

            # Process directory level.
            while true
                # Process all files per directory level.
                idx = 0
                files.each do |file|
                    match = Dir.glob( "#{dir}/#{file}" )
                    unless match.empty?
                        found = match[0]
                        break
                    end
                end

                break if found

            end

            break if found

            if dir == "/"
                raise RuntimeError, "Could not find file(s): \"#{files.join("\", \"")}\"!"
            end

            dir = File.dirname( dir )

        end

        found
    end


    def Diru.opts_filename
        if ENV['DIRU_OPTS_FORMAT']
            ".diru.#{ENV['DIRU_OPTS_FORMAT']}"
        else
            ".diru.yml"
        end
    end

end


# Diru configuration file.
class DiruConf

    # Convert YAML config to hash with String keys at highest level.
    def DiruConf.stringify_yaml_keys( entry )
        ret = {}
        entry.each do |k,v|
            if k.is_a? String
                key = k
            else
                key = k.to_s
            end
            ret[ key ] = v
        end
        ret
    end

    def initialize( filename )
        @filename = filename
    end

end


# Diru configuration file in JSON.
class DiruConfJson < DiruConf

    def load
        if File.exist?( @filename )
            JSON.parse( File.read( @filename ) )
        else
            STDERR.puts "Diru Error: Broken JSON, please fix: \"#{@filename}\"..."
            nil
        end
    end

end


# Diru configuration file in YAML.
class DiruConfYaml < DiruConf

    def load
        if File.exist?( @filename )
            DiruConf.stringify_yaml_keys( YAML.load( File.read( @filename ) ) )
        else
            STDERR.puts "Diru Error: Broken YAML, please fix: \"#{@filename}\"..."
            nil
        end
    end

end



# Diru Server State.
class Search

    include Diru

    def initialize( root, user, opts_file = nil )

        # Server root.
        @root = root
        if @root == '/'
            @rootpart = ''
        else
            @rootpart = @root
        end

        # Change directory to DIRU root.
        Dir.chdir( @root )

        # User of server.
        @user = user

        # List of old matches.
        @old = []

        # Numbered bookmarks.
        @book = []

        # Numbered history.
        @hist = []

        # Favorites.
        @fav = {}

        # History limit.
        @histlimit = 20

        # Sync period for options (and data if no @dsync).
        @sync = 10

        # Sync period for data.
        @dsync = nil

        # Options File.
        @opts_file = opts_file

        # Temporary storage (scratch pad).
        @scratch = {}

        # Glob pattern for DB build.
        @glob = "**/*"

        @logging = false
        # if Opt['log'].given
        #     @logging = true
        # end

        # Data update thread.
        @th_data = nil

        # Options update thread.
        @th_opts = nil

        # Lock for DB access.
        @datalock = Mutex.new
        @optslock = Mutex.new


        # Initial data update.
        update_conf
        update_data

        start_th_data
        start_th_opts
    end


    # Return opts_file.
    def opts_file
        @opts_file
    end

    # Return root.
    def root
        @root
    end

    # Return user.
    def user
        @user
    end


    # Add bookmark (if not already).
    def abook( dir )
        idx = @book.index dir
        unless idx
            @book.unshift dir
        end
    end


    # Delete bookmark.
    def dbook( idx )
        @book.delete_at idx
    end


    # Get bookmark.
    def gbook( idx )
        @book[ idx ]
    end


    # Get all bookmarks.
    def book
        @book
    end


    # Reset bookmarks.
    def rbook
        @book = []
    end


    # Save bookmarks to file.
    def savebook( file )
        File.write( file, @book.join("\n") + "\n" )
    end


    # Load bookmarks from file.
    def loadbook( file )
        @book = File.read( file ).split("\n")
    end


    # Add history.
    def ahist( dir )
        if dir != @hist[0]
            if @hist.length >= @histlimit
                @hist = [ dir ] + @hist[ 0..(@histlimit-2) ]
            else
                @hist = [ dir ] + @hist
            end
        end
    end


    # Get history.
    def ghist( idx )
        @hist[ idx ]
    end


    # Get all historys.
    def hist
        @hist
    end


    # Reset history.
    def rhist
        @hist = []
    end


    # Get (and update) favorite.
    def gfav( tag )
        "#{@rootpart}/#{@fav[ tag ]}"
    end


    # Return the whole fav.
    def fav
        @fav
    end


    # Update options.
    def update_conf
        if @opts_file

            return unless load_conf( @opts_file )

            if @conf[ "dsync" ] && @conf[ "dsync" ] >= 0
                @dsync = @conf[ "dsync" ]
            end

            if @conf[ "sync" ] && @conf[ "sync" ] >= 1
                @sync = @conf[ "sync" ]
                @dsync = @sync unless @dsync
            end

            if @dsync == 0
                kill_th_data
            else
                unless @th_data
                    start_th_data
                end
            end

            if @conf[ "hist" ] && ( @conf[ "hist" ] >= 2 )
                @histlimit = @conf[ "hist" ]
            else
                @histlimit = 20
            end

            if @conf[ "favs" ]
                @fav = @conf[ "favs" ]
            end

        end
    end


    # Set scratch dir.
    def settmp( dir, reg = nil )
        @scratch[ reg ] = dir
    end


    # Get scratch dir.
    def gettmp( reg = nil )
        @scratch[ reg ]
    end


    # Reset scratch dir.
    def rtmp
        @scratch = {}
    end


    # Get all scratch regs.
    def scratch
        @scratch
    end


    def abs2rel( path )
        if path == @root
            ''
        else
            rem = "#{@rootpart}/"
            rel = path.dup
            rel[rem] = ''
            rel
        end
    end


    # Match dir/pattern with glob (fnmatch) from DB.
    def match( dir, pattern )
        list = []

        if dir
            begin
                dir = abs2rel( dir )
            rescue
                return []
            end
            r = dir + '*' + pattern
        else
            r = pattern
        end

        @datalock.synchronize do
            @data.each do |i|
                if File.fnmatch( r, i )
                    list.push "#{@rootpart}/#{i}"
                end
            end
        end

        # Update old list.
        @old = list.rotate

        list
    end



    # Match dir/pattern with regex from DB.
    def rematch( dir, pattern )
        list = []

        if dir
            begin
                dir = abs2rel( dir )
            rescue
                return []
            end
            r = dir + '.*' + pattern
        else
            r = pattern
        end

        @datalock.synchronize do
            @data.each do |i|
                if i.match( r )
                    list.push "#{@rootpart}/#{i}"
                end
            end
        end

        # Update old list.
        @old = list.rotate

        list
    end


    # Get next match from old.
    def next
        ret = @old[0]
        @old.rotate!
        ret
    end


    # Make directory.
    def mk_dir( dir )
        rel = abs2rel( dir )
        unless @data.index( rel )
            @data = @data.push( rel ).sort
            FileUtils.mkdir_p dir
        end
    end


    # Make directory in sync.
    def mk_dir_sync( dir )
        @datalock.synchronize do
            mk_dir dir
        end
    end


    # Remove directory.
    def rm_dir( dir )
        rel = abs2rel( dir )
        if @data.index( rel )
            @data.delete( rel )
            FileUtils.rmdir dir
        end
    end


    # Remove directory in sync.
    def rm_dir_sync( dir )
        @datalock.synchronize do
            rm_dir dir
        end
    end


    # Update directory DB.
    #
    # Relative dir data under Project root.
    #   test
    #   test/dir_0
    #   ...
    def update_data
        @data = Dir.glob( @glob ).select{|ent| File.directory?( ent )}
        @data = @data.sort
    end


    # Update DB with MT sync.
    def update_data_sync
        @datalock.synchronize do
            update_data
        end
    end


    # Update DB with MT sync.
    def update_opts_sync
        @optslock.synchronize do
            update_conf
        end
    end


    # Kill search and cleanup resources.
    def kill
        kill_th_data
        kill_th_opts
    end


    # Reset dynamic state.
    def reset
        log "reset"
        @old = []
        @book = []
        @hist = []
        update_data_sync
    end

    # Start data thread.
    def start_th_data
        unless @dsync == 0
            @th_data = Thread.new do
                sleep 1
                loop do
                    # puts "data update..."
                    update_data_sync
                    sleep @dsync
                end
            end
        end
    end


    # Kill data thread.
    def kill_th_data
        Thread.kill( @th_data ) if @th_data
        @th_data = nil
    end


    # Start opts thread.
    def start_th_opts
        @th_opts = Thread.new do
            sleep 1
            loop do
                update_opts_sync
                sleep @sync
            end
        end
    end


    # Kill opts thread.
    def kill_th_opts
        Thread.kill( @th_opts ) if @th_opts
        @th_opts = nil
    end


    # Enable logging.
    def logon
        @logging = true
    end

    # Disable logging.
    def logoff
        @logging = false
    end


    # Log events.
    def log( msg )
        if @logging
            fh = File.open( "#{ENV['HOME']}/.diru.log", "a" )
            fh.puts "#{Time.timestamp()}: #{msg}"
            fh.close
        end
    end
end



# ------------------------------------------------------------
# Server program.
# ------------------------------------------------------------

# Diru hub.
class Hub

    # Start in daemon or direct mode.
    def Hub.start( port, daemon = true )
        if daemon
            p = fork do
                begin
                    Hub.new( port )
                rescue
                    Diru.error "Could not start Hub!"
                    exit( false )
                end
            end
            Process.detach( p )
        else
            begin
                Hub.new( port )
            rescue
                Diru.error "Could not start Hub!"
                exit( false )
            end
        end
    end


    # Server collection.
    @@servers = {}

    attr_reader :port

    def initialize( port )
        @port = port

        # Start the service
        @hub = DRb.start_service( "druby://localhost:#{@port}", self )
        DRb.thread.join
    end


    # Kill hub.
    def kill
        @th = Thread.new do
            STDERR.puts "Diru Hub: Exiting sooooon..."
            sleep 3
            kill_servers
            sleep 3
            STDERR.puts "Diru Hub: Exit done..."
            @hub.stop_service
            exit( false )
        end
    end


    # Create new server and return port for the server.
    def get_server( root, user, opts_file )

        50.times do |i|
            if @@servers[ @port+1+i ] == nil
                port = @port+1+i
                s = Service.new( root, user, port, opts_file )
                @@servers[ port ] = s
                return port
            end
        end

        0
    end


    # List all server ports.
    def list_servers
        @@servers.keys.map{ |i| [ i, @@servers[i].root, @@servers[i].user ] }
    end


    # Kill all servers.
    def kill_servers
        @@servers.keys.each do |s|
            kill_server( s )
        end
        @@servers = {}
    end

    # Kill server with the given port.
    def kill_server( s_port )
        s = @@servers[ s_port ]
        if s
            s.kill
            @@servers.delete( s_port )
        else
            nil
        end
    end

end



# diru client service (server) thread.
class Service

    attr_reader :root
    attr_reader :user

    # Initialize and start service.
    def initialize( root, user, port, opts_file )
        @root = root
        @user = user
        @port = port
        @drb = nil
        @opts_file = opts_file
        start
        self
    end

    # Kill service.
    def kill
        @search.kill
        Thread.kill( @th )
        @drb.stop_service
    end

    # Start service.
    def start
        @th = Thread.new do

            # Create "front" object.
            @search = Search.new( @root, @user, @opts_file )

            # Start the service
            @drb = DRb::DRbServer.new( "druby://localhost:#{@port}", @search )
            @drb.thread.join
        end
    end

end


if Opt['template'].given
    puts %q{---
hist:  20
sync:  10
dsync: 0
favs:
  f: dir_0/dir_0_4/dir_0_4_0
  g: dir_1/dir_1_2
peers:
  - - "(.*/dir_0/.*)/dir_0_2_0"
    - "\\1"
  - - "(.*/dir_0/.*)/dir_0_1_0"
    - "\\1/dir_0_1_1"
}
    exit( false )
end


# Keep in sync with README.
if Opt['cmddoc'].given
    STDERR.puts %q{
Search commands:

* "dr r"           - change to Project root dir.
* "dr r <dir>"     - change to <dir> (somewhere) under Project root dir
                     (glob).
* "dr t <dir>"     - change to <dir> (somewhere) under current dir (glob).
* "dr e <dir>"     - change to <dir> (somewhere) under Project root dir
                     (regexp).

Bookmark commands:

* "dr b"           - display bookmarks.
* "dr b ."         - add current dir to bookmarks.
* "dr b !"         - reset (clear) bookmarks.
* "dr b s <file>"  - store bookmarks to <file>.
* "dr b l <file>"  - load  bookmarks from <file>.
* "dr b d <num>"   - delete bookmark with <num>.
* "dr b <num>"     - change dir to bookmark <num>.

History commands:

* "dr h"           - display history.
* "dr h ."         - add current dir to history.
* "dr h !"         - reset (clear) history.
* "dr h 0"         - reference latest history item.
* "dr h <num>"     - change dir to history <num>.

Scratch Pad commands:

* "dr s ."         - store current dir to Default Scratch Pad.
* "dr s . <reg>"   - store current dir to <reg> in Scratch Pad.
* "dr s <dir>"     - store <dir> to Default Scratch Pad.
* "dr s <d> <r>"   - store <dir> to <reg> in Scratch Pad.
* "dr s"           - change dir to Default Scratch Pad.
* "dr s <reg>"     - change dir to <reg> from Scratch Pad.
* "dr s ="         - display Scratch Pad content.
* "dr s !"         - reset (clear) Scratch Pad.

Misc commands:

* "dr p"           - jump to peer dir, i.e. the peer of current (from options).
* "dr c <cmd>"     - issue RPC command to server (reset etc., see below).
* "dr f"           - list favorites dirs (from options).
* "dr f <dir>"     - change dir to favorite <dir>.
* "dr d m <dir>"   - make directory.
* "dr d r <dir>"   - remove directory.
* "dr i"           - show command info.
* "dr <dir>"       - change to given dir (must be under current).
* "dr"             - change to next "Left-over" directory.

}
    exit( false )
end


hport = nil
if Opt['hport'].given
    hport = Opt['hport'].value.to_i
else
    hport = Diru::DIRU_HUB_PORT
end


if Opt['hub'].given

    # Hub:
    h = Hub.start( hport, !Opt['nodaemon'].given )
    exit( true )
end


if false
    # For hub maintenance (irb):
    require 'drb'
    hub = DRbObject.new( nil, "druby://localhost:41114" )
    hub.list_servers
end


if false
    # Test client
    require 'drb'
    @port = 41115
    @search = DRbObject.new( nil, "druby://localhost:#{@port}" )
end


if Opt['hkill'].given
    begin
        hub = DRbObject.new( nil, "druby://localhost:#{hport}" )
        hub.kill
    rescue
        Diru.error "Could not kill Hub!"
    end
    exit( true )
end


if Opt['server'].given

    # Setup client server.
    hub = DRbObject.new( nil, "druby://localhost:#{hport}" )

    root = nil

    # Define project root, if it is explicitly defined.
    if ENV['DIRU_ROOT']
        root = ENV['DIRU_ROOT']
    else
        # First search for .diru_root_dir file.
        begin
            root = File.dirname( Diru.find_upper_file( '.diru_root_dir' ) )
        rescue
            root = nil
        end
    end

    opts_json = ".diru.json"
    opts_yaml = ".diru.yml"

    opts_filename = nil
    opts_file = nil

    if root
        if Opt['options'].given
            opts_filename = Opt['options'].value
        elsif ENV['DIRU_OPTS']
            opts_filename = ENV['DIRU_OPTS']
        elsif File.exists? "#{ENV['HOME']}/#{opts_json}"
            opts_filename = "#{ENV['HOME']}/#{opts_json}"
        elsif File.exists? "#{ENV['HOME']}/#{opts_yaml}"
            opts_filename = "#{ENV['HOME']}/#{opts_yaml}"
        else
            opts_filename = nil
        end
    else
        # No explicit root, hence derive from options file (if any).
        begin
            file = Diru.find_an_upper_file( [ opts_json, opts_yaml ] )
            root = File.dirname( file )
            opts_filename = file
        rescue
            Diru.error "Could not find user directory root!"
        end
    end

    if opts_filename
        case File.extname( opts_filename )
        when ".json"; opts_file = DiruConfJson.new( opts_filename )
        when ".yml"; opts_file = DiruConfYaml.new( opts_filename )
        else raise RuntimeError, "Wrong Diru configuration file format (i.e. not json or yaml)!"
        end
    else
        raise RuntimeError, "Missing configuration file!"
    end

    if Opt['server'].value.any?
        user = Opt['server'].value[0]
    else
        user = ENV['USER']
    end

    begin
        s_port = hub.get_server( root, user, opts_file )
    rescue
        Diru.error "Access to Hub failed!"
    end

    if s_port == 0
        Diru.error "Could not start server!"
    else
        puts "Using server port: #{s_port}..."
    end

    exit( true )
end


if Opt['kill'].given

    hub = DRbObject.new( nil, "druby://localhost:#{hport}" )
    port = Opt['kill'].value.to_i
    begin
        hub.kill_server port
    rescue
        Diru.error "Access to Hub failed!"
    end

    exit( true )
end


if Opt['list'].given

    hub = DRbObject.new( nil, "druby://localhost:#{hport}" )
    begin
        hub.list_servers.each do |i|
            puts format( "%-12.d  %-12s %s", i[0], i[2], i[1] )
        end
    rescue
        Diru.error "Access to Hub failed!"
    end

    exit( true )
end



# ------------------------------------------------------------
# Client program.
# ------------------------------------------------------------


class Client

    include Diru

    attr_reader   :pwd
    attr_accessor :enable_out

    def initialize( port )

        @port = port

        # Create a DRbObject instance that is connected to server. All methods
        # executed on this object will be executed to the remote one.
        @search = DRbObject.new( nil, "druby://localhost:#{@port}" )

        raise unless load_conf( @search.opts_file )

        @pwd = Dir.pwd

        @enable_out = true

    end


    # Perform cd (i.e. return true) and store current dir to history.
    def do_cd( dir )

        # Store current to history before change.
        @search.ahist( @pwd )

        # Local copy of new dir for command-sequence mode.
        @pwd = dir

        # Directory info for shell, unless disabled for
        # command-sequence mode.
        puts dir if @enable_out

        true
    end


    # No directory change, i.e. return false.
    def no_cd
        false
    end


    # Process command.
    def command( input )

        if input.empty?

            # Refer to last search list.
            do_cd @search.next

        else

            cmd = input[0]
            arg = input[1..-1]

            # Shell special chars to avoid:
            #   * ? [ ] ' " \ $ ; & ( ) | ^ < >

            # Thus allowed:
            #   ! % + , - . / : = @ _ ~
            #   ^ ^   ^   ^ ^ ^ ^ ^ ^

            # Decode user command.
            ret = \
            case cmd

            when 'r';
                if arg.empty?
                    disp @search.root
                else
                    disp @search.match( nil, "*"+arg.join('*')+"*" )
                end

            when 't'; disp @search.match( @pwd, "*"+arg.join('*')+"*" )

            when 'e'; disp @search.rematch( nil, arg.join('.*') )

            when 'q'; disp @search.rematch( nil, arg.join('.*') )

            when 'b';
                if arg[0] == nil
                    lbook( @search.book )
                    no_cd
                elsif arg[0] == '.'
                    @search.abook( @pwd )
                    no_cd
                elsif arg[0] == '!'
                    @search.rbook
                    no_cd
                elsif arg[0] == 's'
                    @search.savebook( arg[1] )
                    no_cd
                elsif arg[0] == 'l'
                    @search.loadbook( arg[1] )
                    no_cd
                elsif arg[0] == 'd'
                    @search.dbook( arg[1].to_i )
                    no_cd
                else
                    disp @search.gbook( arg[0].to_i )
                end

            when 'h';
                if arg[0] == nil
                    lbook( @search.hist )
                    no_cd
                elsif arg[0] == '.'
                    @search.ahist( @pwd )
                    no_cd
                elsif arg[0] == '!'
                    @search.rhist
                    no_cd
                elsif arg[0] == ','
                    disp @search.ghist( 0 )
                else
                    disp @search.ghist( arg[0].to_i )
                end

            when 's';
                if arg[0] == nil
                    # Change: "dr s"
                    disp @search.gettmp
                else
                    if arg[0] == '.'
                        # Store: "dr s ."
                        #    OR: "dr s . <reg>"
                        reg = nil
                        dir = @pwd
                        if arg[1] && arg[1].length == 1
                            reg = arg[1]
                        end
                        disp @search.settmp( dir, reg )
                        no_cd
                    elsif arg[0] == '='
                        lregs( @search.scratch )
                        no_cd
                    elsif arg[0] == '!'
                        @search.rtmp
                        no_cd
                    elsif arg[0].length == 1
                        # Change: "dr s <reg>"
                        disp @search.gettmp( arg[0] )
                    else
                        # Store: "dr s <dir>"
                        #    OR: "dr s <dir> <reg>"
                        reg = nil
                        reg = arg[1] if arg[1]
                        disp @search.settmp( arg[0], reg )
                        no_cd
                    end
                end

            when 'p'; disp peer

            when 'c'; rpc( arg )

            when 'f';
                if arg[0] == nil
                    @search.fav.each do |k,v|
                        STDERR.puts format( "  %-6s %s", k, v )
                    end
                    no_cd
                else
                    fav_map( arg )
                end

            when 'd';
                # Mkdir:
                #   dr d m <dir>
                # Rmdir:
                #   dr d r <dir>
                if arg.length == 2
                    if arg[0] == 'm'
                        abs = File.absolute_path( arg[1] )
                        if abs.match( @search.root )
                            # New subdir to Project.
                            @search.mk_dir_sync( abs )
                        else
                            FileUtils.mkdir_p dir
                        end
                    elsif arg[0] == 'r'
                        abs = File.absolute_path( arg[1] )
                        if abs.match( @search.root )
                            # New subdir to Project.
                            @search.rm_dir_sync( abs )
                        else
                            FileUtils.rmdir dir
                        end
                    end
                end
                no_cd

            when 'i';
                rpc( [ 'doc' ] )

            else

                if Opt['change'].given
                    all = [cmd] + arg
                    do_cd "#{all.join(' ')}"
                else
                    no_cd
                end

            end

            # exit( ret )
            return ret
        end

    end


    # Display directories. One to STDOUT and others to STDERR.
    def disp( resp )
        if resp == nil
            no_cd
        elsif resp.kind_of? Array
            if resp.any?
                if resp.length > 1
                    resp[1..-1].each do |i|
                        STDERR.puts i
                    end
                end
                do_cd resp[0]
            else
                Diru.warn "Dir not found!"
                no_cd
            end
        else
            do_cd resp
        end
    end


    # Display directories without CD.
    def show( resp )
        if resp == nil
            # NOP
        elsif resp.kind_of? Array
            if resp.any?
                resp.each do |i|
                    STDERR.puts i
                end
            end
        else
            STDERR.puts resp
        end
        no_cd
    end


    # Get peer directory. Search list of re-pairs. Match either of the
    # pair, and switch to the pair dir.
    #
    def peer
        # cur = Dir.pwd
        cur = @pwd
        peers = @conf[ "peers" ]
        peers.each do |pair|
            re = Regexp.new( pair[0] )
            if ( re.match( cur ) )
                return cur.sub( re, pair[1] )
            end
        end
        nil
    end


    # Display short command doc.
    def doc
        STDERR.puts "
    r - search from root (or to root)
    t - search from this (current)
    e - search from root (with regexp)
    q - query from root (with regexp)
    b - bookmark access
    h - history access
    s - scratch pad access
    p - peer of current
    c - command (reset, doc etc.)
    f - favorites
    d - directory mutation
    i - short info
    "
    end


    # List bookmarks with index number, in reverse order.
    def lbook( book )
        idx = book.length-1
        (0..idx).to_a.reverse.each do |i|
            STDERR.puts format( "%2d: %s", i, book[i] )
        end
    end


    # List registers (key,value).
    def lregs( regs )
        regs.each do |k,v|
            STDERR.puts format( "  %-2s: %s", k, v )
        end
    end


    # Remove Procedure Call towards server.
    #
    # Example:
    #   shell> dr @ rbook
    #
    def rpc( arg )
        case arg[0]
        when 'reset'; @search.reset
        when 'abook'; @search.abook( @pwd )
        when 'lbook'; lbook( @search.book )
        when 'rbook'; @search.rbook
        when 'doc'; doc
        when 'dsync'; @search.update_data_sync
        when 'sync';  @search.update_opts_sync
        when 'lregs'; @search.update_data_sync
        else return no_cd
        end
        no_cd
    end


    # Direct directory jump.
    def fav_map( arg )
        ret = @search.gfav( arg[0] )
        if ret
            disp ret
        else
            no_cd
        end
    end

end


if Opt['port'].given
    port = Opt['port'].value.to_i
elsif ENV['DIRU_PORT']
    port = ENV['DIRU_PORT'].to_i
elsif File.exist?( "#{ENV['HOME']}/.diru.prt" )
    port = File.read( "#{ENV['HOME']}/.diru.prt" ).to_i
else
    Diru.error "Server port info missing..."
end


# User command content.
input = Opt[nil].value

begin
    client = Client.new( port )
rescue
    Diru.error "Server not available!"
    exit( false )
end


# Collect command-sequence (if any).
cmds = []
si = 0
ei = 0
while ( ei = input.index "," )
    cmds.push input[si...ei]
    si = ei+1
    input = input[si..-1]
end

# Add the tail command, i.e. the only if no "+" used.
cmds.push input

ret = false


# Disable shell output for all dir changes except last.
if cmds.length > 1
    client.enable_out = false
end

cmds.each_with_index do |cmd, i|

    if i == cmds.length-1
        # Enable output for shell for the last dir change.
        client.enable_out = true
    end

    begin
        ret = client.command( cmd )
    rescue
        Diru.error "Command failure!"
        exit( false )
    end

end

exit( ret )