edouard-clarity 0.9.9
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +34 -0
- data/PostInstall.txt +8 -0
- data/README.rdoc +85 -0
- data/Rakefile +23 -0
- data/bin/clarity +10 -0
- data/config/config.yml.sample +7 -0
- data/lib/clarity.rb +24 -0
- data/lib/clarity/cli.rb +94 -0
- data/lib/clarity/commands/grep_command_builder.rb +57 -0
- data/lib/clarity/commands/hostname_command_builder.rb +7 -0
- data/lib/clarity/commands/tail_command_builder.rb +27 -0
- data/lib/clarity/grep_renderer.rb +37 -0
- data/lib/clarity/process_tree.rb +23 -0
- data/lib/clarity/renderers/log_renderer.rb +36 -0
- data/lib/clarity/server.rb +139 -0
- data/lib/clarity/server/basic_auth.rb +24 -0
- data/lib/clarity/server/chunk_http.rb +59 -0
- data/lib/clarity/server/mime_types.rb +23 -0
- data/public/images/spinner_big.gif +0 -0
- data/public/javascripts/app.js +154 -0
- data/public/stylesheets/app.css +55 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/test/commands/grep_command_builder_test.rb +99 -0
- data/test/commands/tail_command_builder_test.rb +42 -0
- data/test/files/logfile.log +17 -0
- data/test/test_helper.rb +4 -0
- data/test/test_string_scanner.rb +16 -0
- data/views/_header.html.erb +5 -0
- data/views/_toolbar.html.erb +64 -0
- data/views/error.html.erb +11 -0
- data/views/index.html.erb +9 -0
- metadata +163 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
class LogRenderer
|
5
|
+
|
6
|
+
# Thank you to http://daringfireball.net/2009/11/liberal_regex_for_matching_urls
|
7
|
+
#
|
8
|
+
UrlParser = %r{\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))}
|
9
|
+
Prefix = ""
|
10
|
+
Suffix = "<br/>\n"
|
11
|
+
|
12
|
+
def render(line = {})
|
13
|
+
# Escape
|
14
|
+
output = ERB::Util.h(line)
|
15
|
+
|
16
|
+
# Transform urls into html links
|
17
|
+
output.gsub!(UrlParser) do |match|
|
18
|
+
html_link(match)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Return with formatting
|
22
|
+
"#{Prefix}#{output}#{Suffix}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def finalize
|
26
|
+
'</div><hr><p id="done">Done</p></body></html>'
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def html_link(url)
|
32
|
+
uri = URI.parse(url) rescue url
|
33
|
+
"<a href='#{uri}'>#{url}</a>"
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require File.dirname(__FILE__) + '/server/basic_auth'
|
3
|
+
require File.dirname(__FILE__) + '/server/mime_types'
|
4
|
+
require File.dirname(__FILE__) + '/server/chunk_http'
|
5
|
+
require File.dirname(__FILE__) + '/grep_renderer'
|
6
|
+
require File.dirname(__FILE__) + '/process_tree'
|
7
|
+
|
8
|
+
module Clarity
|
9
|
+
class NotFoundError < StandardError; end
|
10
|
+
class NotAuthenticatedError < StandardError; end
|
11
|
+
class InvalidParameterError < StandardError; end
|
12
|
+
|
13
|
+
module Server
|
14
|
+
include EventMachine::HttpServer
|
15
|
+
include Clarity::BasicAuth
|
16
|
+
include Clarity::ChunkHttp
|
17
|
+
|
18
|
+
attr_accessor :required_username, :required_password, :relative_root
|
19
|
+
attr_accessor :log_files
|
20
|
+
|
21
|
+
def self.run(options)
|
22
|
+
|
23
|
+
EventMachine::run do
|
24
|
+
EventMachine.epoll
|
25
|
+
EventMachine::start_server(options[:address], options[:port], self) do |a|
|
26
|
+
a.log_files = options[:log_files]
|
27
|
+
a.required_username = options[:username]
|
28
|
+
a.required_password = options[:password]
|
29
|
+
a.relative_root = options[:relative_root] || ""
|
30
|
+
end
|
31
|
+
|
32
|
+
STDERR.puts "Clarity #{Clarity::VERSION} starting up."
|
33
|
+
STDERR.puts " * listening on #{options[:address]}:#{options[:port]}"
|
34
|
+
|
35
|
+
if options[:user]
|
36
|
+
STDERR.puts " * Running as user #{options[:user]}"
|
37
|
+
EventMachine.set_effective_user(options[:user])
|
38
|
+
end
|
39
|
+
|
40
|
+
STDERR.puts " * Log mask(s): #{options[:log_files].join(', ')}"
|
41
|
+
|
42
|
+
if options[:username].nil? or options[:password].nil?
|
43
|
+
STDERR.puts " * WARNING: No username/password specified. This is VERY insecure."
|
44
|
+
end
|
45
|
+
|
46
|
+
STDERR.puts
|
47
|
+
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def process_http_request
|
53
|
+
authenticate!
|
54
|
+
|
55
|
+
puts "action: #{path}"
|
56
|
+
puts "params: #{params.inspect}"
|
57
|
+
|
58
|
+
@hostname = HostnameCommandBuilder.command
|
59
|
+
|
60
|
+
case path
|
61
|
+
when '/'
|
62
|
+
respond_with(200, welcome_page)
|
63
|
+
|
64
|
+
when '/perform'
|
65
|
+
if params.empty?
|
66
|
+
respond_with(200, welcome_page)
|
67
|
+
else
|
68
|
+
# get command
|
69
|
+
command = case params['tool']
|
70
|
+
when 'grep' then GrepCommandBuilder.new(params).command
|
71
|
+
when 'tail' then TailCommandBuilder.new(params).command
|
72
|
+
else raise InvalidParameterError, "Invalid Tool parameter"
|
73
|
+
end
|
74
|
+
response = respond_with_chunks
|
75
|
+
response.chunk results_page # display page header
|
76
|
+
|
77
|
+
puts "Running: #{command}"
|
78
|
+
|
79
|
+
EventMachine::popen(command, GrepRenderer) do |grepper|
|
80
|
+
@grepper = grepper
|
81
|
+
@grepper.response = response
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
when '/test'
|
86
|
+
response = respond_with_chunks
|
87
|
+
EventMachine::add_periodic_timer(1) do
|
88
|
+
response.chunk "Lorem ipsum dolor sit amet<br/>"
|
89
|
+
response.send_chunks
|
90
|
+
end
|
91
|
+
|
92
|
+
else
|
93
|
+
respond_with(200, public_file(path), :content_type => Mime.for(path))
|
94
|
+
end
|
95
|
+
|
96
|
+
rescue InvalidParameterError => e
|
97
|
+
respond_with(500, error_page(e))
|
98
|
+
rescue NotFoundError => e
|
99
|
+
respond_with(404, "<h1>Not Found</h1>")
|
100
|
+
rescue NotAuthenticatedError => e
|
101
|
+
puts "Could not authenticate user"
|
102
|
+
headers = { "WWW-Authenticate" => %(Basic realm="Clarity")}
|
103
|
+
respond_with(401, "HTTP Basic: Access denied.\n", :content_type => 'text/plain', :headers => headers)
|
104
|
+
end
|
105
|
+
|
106
|
+
def error_page(error)
|
107
|
+
@error = error
|
108
|
+
render "error.html.erb"
|
109
|
+
end
|
110
|
+
|
111
|
+
def welcome_page
|
112
|
+
render "index.html.erb"
|
113
|
+
end
|
114
|
+
|
115
|
+
def results_page
|
116
|
+
render "index.html.erb"
|
117
|
+
end
|
118
|
+
|
119
|
+
def unbind
|
120
|
+
@grepper.close_connection if @grepper
|
121
|
+
close_connection
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def authenticate!
|
127
|
+
login, pass = authentication_data
|
128
|
+
|
129
|
+
if (required_username && required_username != login) || (required_password && required_password != pass)
|
130
|
+
raise NotAuthenticatedError
|
131
|
+
end
|
132
|
+
|
133
|
+
true
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Clarity
|
2
|
+
module BasicAuth
|
3
|
+
|
4
|
+
def decode_credentials(request)
|
5
|
+
Base64.decode64(request).split.last
|
6
|
+
end
|
7
|
+
|
8
|
+
def user_name_and_password(request)
|
9
|
+
decode_credentials(request).split(/:/, 2)
|
10
|
+
end
|
11
|
+
|
12
|
+
def authentication_data
|
13
|
+
headers = @http_headers.split("\000")
|
14
|
+
auth_header = headers.detect {|head| head =~ /Authorization: / }
|
15
|
+
header = auth_header.nil? ? "" : auth_header.split("Authorization: Basic ").last
|
16
|
+
return (user_name_and_password(header) rescue ['', ''])
|
17
|
+
end
|
18
|
+
|
19
|
+
def authenticate!(http_header)
|
20
|
+
raise NotAuthenticatedError unless authenticate(http_header)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Clarity
|
4
|
+
|
5
|
+
module ChunkHttp
|
6
|
+
|
7
|
+
LeadIn = ' ' * 1024
|
8
|
+
|
9
|
+
def respond_with_chunks
|
10
|
+
response = EventMachine::DelegatedHttpResponse.new( self )
|
11
|
+
response.status = 200
|
12
|
+
response.headers['Content-Type'] = 'text/html'
|
13
|
+
response.chunk LeadIn
|
14
|
+
response
|
15
|
+
end
|
16
|
+
|
17
|
+
def respond_with(status, content, options = {})
|
18
|
+
response = EventMachine::DelegatedHttpResponse.new( self )
|
19
|
+
response.headers['Content-Type'] = options.fetch(:content_type, 'text/html')
|
20
|
+
response.headers['Cache-Control'] = 'private, max-age=0'
|
21
|
+
headers = options.fetch(:headers, {})
|
22
|
+
headers.each_pair {|h, v| response.headers[h] = v }
|
23
|
+
response.status = status
|
24
|
+
response.content = content
|
25
|
+
response.send_response
|
26
|
+
end
|
27
|
+
|
28
|
+
def render(view)
|
29
|
+
@toolbar = template("_toolbar.html.erb")
|
30
|
+
@content_for_header = template("_header.html.erb")
|
31
|
+
template(view)
|
32
|
+
end
|
33
|
+
|
34
|
+
def template(filename)
|
35
|
+
content = File.read( File.join(Clarity::Templates, filename) )
|
36
|
+
ERB.new(content).result(binding)
|
37
|
+
end
|
38
|
+
|
39
|
+
def public_file(filename)
|
40
|
+
File.read( File.join(Clarity::Public, filename) )
|
41
|
+
rescue Errno::ENOENT
|
42
|
+
raise NotFoundError
|
43
|
+
end
|
44
|
+
|
45
|
+
def logfiles
|
46
|
+
log_files.map {|f| Dir[f] }.flatten.compact.uniq.select{|f| File.file?(f) }.sort
|
47
|
+
end
|
48
|
+
|
49
|
+
def params
|
50
|
+
ENV['QUERY_STRING'].split('&').inject({}) {|p,s| k,v = s.split('=');p[k.to_s] = CGI.unescape(v.to_s);p}
|
51
|
+
end
|
52
|
+
|
53
|
+
def path
|
54
|
+
ENV["PATH_INFO"]
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Clarity
|
2
|
+
module Mime
|
3
|
+
def self.for(filename)
|
4
|
+
|
5
|
+
content_type = TYPES[File.extname(filename)]
|
6
|
+
content_type || 'text/plain'
|
7
|
+
end
|
8
|
+
|
9
|
+
TYPES = {
|
10
|
+
'.jpg' => 'image/jpg',
|
11
|
+
'.jpeg' => 'image/jpeg',
|
12
|
+
'.gif' => 'image/gif',
|
13
|
+
'.png' => 'image/png',
|
14
|
+
'.bmp' => 'image/bmp',
|
15
|
+
'.bitmap' => 'image/x-ms-bmp',
|
16
|
+
'.js' => 'application/javascript',
|
17
|
+
'.txt' => 'text/plain',
|
18
|
+
'.css' => 'text/css',
|
19
|
+
'.html' => 'text/html',
|
20
|
+
'.htm' => 'text/html'
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
Binary file
|
@@ -0,0 +1,154 @@
|
|
1
|
+
/* ----------------------------------------------------------------------------
|
2
|
+
* Log Search Server JS
|
3
|
+
* by John Tajima
|
4
|
+
* requires jQuery 1.3.2
|
5
|
+
* ----------------------------------------------------------------------------
|
6
|
+
*/
|
7
|
+
|
8
|
+
|
9
|
+
var Search = {
|
10
|
+
search_form : 'search', // domId of the form
|
11
|
+
resultsId : 'results',
|
12
|
+
search_fields: [ 'term1', 'term2', 'term3' ], // domIds of search term fields
|
13
|
+
file_list : 'file-list', // domId of select for logfiles
|
14
|
+
logfiles : {}, // hash of log files
|
15
|
+
past_params : null, // recent request
|
16
|
+
url : '/perform',
|
17
|
+
scroll_fnId : null,
|
18
|
+
|
19
|
+
// initialize Search form
|
20
|
+
// { 'grep': [ log, files, for, grep], 'tail': [ 'log', 'files', 'for', 'tail']}
|
21
|
+
init: function(logfiles, params) {
|
22
|
+
this.logfiles = logfiles;
|
23
|
+
this.past_params = params;
|
24
|
+
|
25
|
+
this.bind_grep_tool();
|
26
|
+
this.bind_tail_tool();
|
27
|
+
this.bind_options();
|
28
|
+
|
29
|
+
if (!this.past_params) return; // return if no prev settings, nothing to set
|
30
|
+
|
31
|
+
// init tool selector
|
32
|
+
(this.past_params['tool'] == 'grep') ? $('#grep-label').trigger('click') : $('#tail-tool').trigger('click');
|
33
|
+
|
34
|
+
// init log file selector
|
35
|
+
$('#'+this.file_list).val(this.past_params['file']);
|
36
|
+
|
37
|
+
// init search fields
|
38
|
+
jQuery.each(this.search_fields, function(){
|
39
|
+
$('#'+this).val(Search.past_params[this]);
|
40
|
+
});
|
41
|
+
|
42
|
+
// advanced options usd?
|
43
|
+
// time was set - so show advanced options
|
44
|
+
if ((this.past_params['sh']) || (this.past_params['eh'])) {
|
45
|
+
this.showAdvanced();
|
46
|
+
if (this.past_params['sh']) {
|
47
|
+
jQuery.each(['sh', 'sm', 'ss'], function(){ $('#'+this).val(Search.past_params[this]) });
|
48
|
+
}
|
49
|
+
if (this.past_params['eh']) {
|
50
|
+
jQuery.each(['eh', 'em', 'es'], function(){ $('#'+this).val(Search.past_params[this]) });
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
},
|
55
|
+
|
56
|
+
// bind option selectors
|
57
|
+
bind_options: function() {
|
58
|
+
$('#auto-scroll').bind('change', function(){
|
59
|
+
Search.autoScroll(this.checked);
|
60
|
+
});
|
61
|
+
$('#auto-scroll').attr('checked', true).trigger('change'); // by default, turn on
|
62
|
+
},
|
63
|
+
|
64
|
+
// bind change grep tool
|
65
|
+
bind_grep_tool: function() {
|
66
|
+
$('#grep-tool').bind('change', function(e){
|
67
|
+
var newlist = ""
|
68
|
+
jQuery.each(Search.logfiles['grep'], function(){
|
69
|
+
newlist += "<option value='" + this + "'>" + this + "</option>\n"
|
70
|
+
});
|
71
|
+
$('#'+Search.file_list).html(newlist);
|
72
|
+
});
|
73
|
+
// watch clicking label as well
|
74
|
+
$('#grep-label').bind('click', function(e){
|
75
|
+
$('#grep-tool').attr('checked', 'checked').val('grep').trigger('change');
|
76
|
+
});
|
77
|
+
},
|
78
|
+
|
79
|
+
|
80
|
+
// bind change tail tool
|
81
|
+
bind_tail_tool: function() {
|
82
|
+
$('#tail-tool').bind('change', function(e){
|
83
|
+
var newlist = ""
|
84
|
+
jQuery.each(Search.logfiles['tail'], function(){
|
85
|
+
newlist += "<option value='" + this + "'>" + this + "</option>\n"
|
86
|
+
});
|
87
|
+
$('#'+ Search.file_list).html(newlist);
|
88
|
+
});
|
89
|
+
// watch clicking label as well
|
90
|
+
$('#tail-label').bind('click', function(e){
|
91
|
+
$('#tail-tool').attr('checked', 'checked').val('tail').trigger('change');
|
92
|
+
});
|
93
|
+
},
|
94
|
+
|
95
|
+
|
96
|
+
// clears the terms fields
|
97
|
+
clear: function() {
|
98
|
+
jQuery.each(this.search_fields, function(){
|
99
|
+
$('#'+this).val("");
|
100
|
+
});
|
101
|
+
},
|
102
|
+
|
103
|
+
showAdvanced: function() {
|
104
|
+
$('#enable-advanced').hide();
|
105
|
+
$('#disable-advanced').show();
|
106
|
+
$('.advanced-options').show();
|
107
|
+
},
|
108
|
+
|
109
|
+
hideAdvanced: function() {
|
110
|
+
this.clearAdvanced();
|
111
|
+
$('#enable-advanced').show();
|
112
|
+
$('#disable-advanced').hide();
|
113
|
+
$('.advanced-options').hide();
|
114
|
+
},
|
115
|
+
|
116
|
+
clearAdvanced: function() {
|
117
|
+
$('#advanced-options input').val("");
|
118
|
+
},
|
119
|
+
|
120
|
+
// gathers form elements and submits to proper url
|
121
|
+
submit: function() {
|
122
|
+
$('#'+this.search_form).submit();
|
123
|
+
$('#'+this.resultsId).html("Sending new query...");
|
124
|
+
},
|
125
|
+
|
126
|
+
//
|
127
|
+
// Misc utitilies
|
128
|
+
//
|
129
|
+
|
130
|
+
autoScroll: function(enabled) {
|
131
|
+
if (enabled == true) {
|
132
|
+
if (this.scroll_fnId) return; // already running
|
133
|
+
|
134
|
+
//console.log("scroll ON!")
|
135
|
+
window._currPos = 0; // init pos
|
136
|
+
this.scroll_fnId = setInterval( function(){
|
137
|
+
if (window._currPos < document.height) {
|
138
|
+
window.scrollTo(0, document.height);
|
139
|
+
window._currPos = document.height;
|
140
|
+
}
|
141
|
+
}, 100 );
|
142
|
+
} else {
|
143
|
+
if (!this.scroll_fnId) return;
|
144
|
+
//console.log("scroll off")
|
145
|
+
if (this.scroll_fnId) {
|
146
|
+
clearInterval(this.scroll_fnId);
|
147
|
+
window._currPost = 0;
|
148
|
+
this.scroll_fnId = null;
|
149
|
+
}
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
};
|
154
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
/* ----------------------------------------------------------------------------
|
2
|
+
* Log Search Server CSS
|
3
|
+
* by John Tajima
|
4
|
+
* ----------------------------------------------------------------------------
|
5
|
+
*/
|
6
|
+
|
7
|
+
/* 960 reset */
|
8
|
+
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}:focus{outline:0}ins{text-decoration:none}del{text-decoration:line-through}table{border-collapse:collapse;border-spacing:0}
|
9
|
+
/* 960 text */
|
10
|
+
body{font:13px/1.5 'Helvetica Neue',Arial,'Liberation Sans',FreeSans,sans-serif}a:focus{outline:1px dotted invert}hr{border:0 #ccc solid;border-top-width:1px;clear:both;height:0}h1{font-size:25px}h2{font-size:23px}h3{font-size:21px}h4{font-size:19px}h5{font-size:17px}h6{font-size:15px}ol{list-style:decimal}ul{list-style:disc}li{margin-left:30px}p,dl,hr,h1,h2,h3,h4,h5,h6,ol,ul,pre,table,address,fieldset{margin-bottom:20px}
|
11
|
+
|
12
|
+
/*
|
13
|
+
* Toolbar Styling
|
14
|
+
*
|
15
|
+
*/
|
16
|
+
|
17
|
+
|
18
|
+
#toolbar { color: #333; font-size: 12px; min-width: 980px; height: 130px;
|
19
|
+
padding: 0 10px; background: #f0f0f0; border-bottom: 2px solid #000;
|
20
|
+
position:fixed; top: 0; margin: 0 -10px; width: 100%;
|
21
|
+
font-family: 'Helvetica Neue',Arial,'Liberation Sans',FreeSans,sans-serif;
|
22
|
+
}
|
23
|
+
#toolbar a { color: blue; background: none; }
|
24
|
+
#toolbar a:hover { background: none; }
|
25
|
+
|
26
|
+
#toolbar #header { height: 20px; padding: 3px 5px; margin: 0 -10px 10px; overflow:hidden; background: #333; border-bottom: 2px solid #222; color: #eee; }
|
27
|
+
#toolbar #header h1 { font-size: 16px; line-height: 20px; }
|
28
|
+
#toolbar #header h1 span { color: #aaa; }
|
29
|
+
#toolbar #header a { text-decoration: none; color: #eee; }
|
30
|
+
#toolbar #header a:hover { text-decoration: none; color: #fff;}
|
31
|
+
#toolbar .small { font-size: 11px; color: #333; font-weight: bold;}
|
32
|
+
#toolbar input.time { width: 20px;}
|
33
|
+
|
34
|
+
table.actions { width: auto; border-collapse: collapse; }
|
35
|
+
table.actions th { font-size: 11px; color: #333; text-align: left; min-width: 100px; padding: 0 5px; }
|
36
|
+
table.actions td { padding: 2px 5px; text-align: left; vertical-align: middle;}
|
37
|
+
table.actions span.note { font-weight: normal; color: #999; }
|
38
|
+
table.actions span.and { font-size: 11px; color: red; font-weight: bold; }
|
39
|
+
table.actions span.label { font-weight: bold; cursor:pointer;}
|
40
|
+
table.actions input[type=text] { width: 200px; padding: 2px; border: 1px solid #aaa;}
|
41
|
+
|
42
|
+
|
43
|
+
div#option-ctrl { position: fixed; z-index: 100; bottom: 0; right: 0; min-width: 60px; min-height: 20px; height: 20px; padding: 5px; border: 1px solid #ddd; background: #f0f0f0; font-size: 11px; color: #000;
|
44
|
+
font-family: 'Helvetica Neue',Arial,'Liberation Sans',FreeSans,sans-serif; }
|
45
|
+
div#option-ctrl ul { list-style:none; margin: 0; padding: 0;}
|
46
|
+
div#option-ctrl ul li { list-style:none; margin: 0 5px 0 0; float:left; }
|
47
|
+
|
48
|
+
|
49
|
+
|
50
|
+
/* output */
|
51
|
+
body { margin: 10px; padding-top: 130px; font-family: 'Monaco', 'Deja Vu Sans Mono', 'Inconsolata' ,'Consolas',monospace; background:#111 none repeat scroll 0 0; color:#fff; font-size:10px;}
|
52
|
+
a { color: #0f0; }
|
53
|
+
a:hover { background-color: #03c; color: white; text-decoration: none; }
|
54
|
+
|
55
|
+
|