mars_base_10 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/mb10 +28 -0
- data/lib/mars_base_10/action_bar.rb +77 -0
- data/lib/mars_base_10/cli.rb +63 -0
- data/lib/mars_base_10/comm_central.rb +31 -0
- data/lib/mars_base_10/graph_rover.rb +117 -0
- data/lib/mars_base_10/pane.rb +195 -0
- data/lib/mars_base_10/ship.rb +40 -0
- data/lib/mars_base_10/stack.rb +90 -0
- data/lib/mars_base_10/subject.rb +46 -0
- data/lib/mars_base_10/version.rb +5 -0
- data/lib/mars_base_10/viewport.rb +130 -0
- data/lib/mars_base_10.rb +4 -0
- data/mars_base_10.gemspec +40 -0
- metadata +187 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d66886ffab2da6f395505f68090aa88ea77f7e9bb17669c79a562d9298338227
|
4
|
+
data.tar.gz: '084f5515249cad9b00649ccf84743e848e835cc3b234f95608efb6bcd5bf5ab7'
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8c4284d80f4071dfde88bef0f7cf1dfa2f684675a8861b13527740f891bf67bb59e48a6cc95afe627425d1017cd6f355ba8075def3e144ac0066287d90c84ab3
|
7
|
+
data.tar.gz: 46fdbcd3ddae7f293f21bd067c042555ce235daef318f4431edc2d30ea05ce92f0783bf96c7103455f3c41f15f1c2f2f94b8dc5382780d7a6ea4f2fb89e6e052
|
data/bin/mb10
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# require "bundler/setup"
|
5
|
+
# require "mars_base_10/comm_central"
|
6
|
+
|
7
|
+
# begin
|
8
|
+
# cc = MarsBase10::CommCentral.new
|
9
|
+
# cc.activate
|
10
|
+
# ensure
|
11
|
+
# cc.shutdown
|
12
|
+
# end
|
13
|
+
|
14
|
+
lib_path = File.expand_path("../lib", __dir__)
|
15
|
+
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
16
|
+
require "mars_base_10/cli"
|
17
|
+
|
18
|
+
Signal.trap("INT") do
|
19
|
+
warn("\n#{caller.join("\n")}: interrupted")
|
20
|
+
exit(1)
|
21
|
+
end
|
22
|
+
|
23
|
+
begin
|
24
|
+
MarsBase10::CLI.start
|
25
|
+
rescue MarsBase10::CLI::Error => err
|
26
|
+
puts "ERROR: #{err.message}"
|
27
|
+
exit 1
|
28
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MarsBase10
|
4
|
+
class ActionBar
|
5
|
+
attr_accessor :actions
|
6
|
+
|
7
|
+
def initialize(actions:)
|
8
|
+
@actions = actions
|
9
|
+
@viewport = nil
|
10
|
+
@win = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.Default
|
14
|
+
ActionBar.new actions: {'j': 'Move Down', 'k': 'Move Up', 'q': 'Quit'}
|
15
|
+
end
|
16
|
+
|
17
|
+
def actions_first_col
|
18
|
+
(self.width - self.actions_width)/2
|
19
|
+
end
|
20
|
+
|
21
|
+
def actions_width
|
22
|
+
self.actions.values.inject(0) {|acc, item| acc += (3 + 2 + item.length + 2)}
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_action(a_hash)
|
26
|
+
self.actions = Hash[@actions.merge!(a_hash).sort]
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def draw
|
31
|
+
self.window.attron(Curses.color_pair(2))
|
32
|
+
self.window.setpos(0, 0)
|
33
|
+
self.window.addstr("Actions:")
|
34
|
+
self.window.addstr(" " * (self.actions_first_col - 8))
|
35
|
+
|
36
|
+
self.actions.each do |key, value|
|
37
|
+
self.window.attron(Curses::A_REVERSE)
|
38
|
+
self.window.addstr(" #{key} ")
|
39
|
+
self.window.attroff(Curses::A_REVERSE) # if item == self.index
|
40
|
+
self.window.addstr(" #{value} ")
|
41
|
+
end
|
42
|
+
|
43
|
+
self.window.addstr(" " * (self.width - (self.actions_first_col + self.actions_width)))
|
44
|
+
self.window.attroff(Curses.color_pair(2))
|
45
|
+
end
|
46
|
+
|
47
|
+
def display_on(viewport:)
|
48
|
+
@viewport = viewport
|
49
|
+
end
|
50
|
+
|
51
|
+
def first_col
|
52
|
+
0
|
53
|
+
end
|
54
|
+
|
55
|
+
def first_row
|
56
|
+
@viewport.max_rows
|
57
|
+
end
|
58
|
+
|
59
|
+
def height
|
60
|
+
1
|
61
|
+
end
|
62
|
+
|
63
|
+
def remove_action(key)
|
64
|
+
self.actions.delete_if {|k, v| k == key}
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def width
|
69
|
+
@viewport.max_cols
|
70
|
+
end
|
71
|
+
|
72
|
+
def window
|
73
|
+
return @win if @win
|
74
|
+
@win = Curses::Window.new(self.height, self.width, self.first_row, self.first_col)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require "pastel"
|
5
|
+
require "tty-font"
|
6
|
+
|
7
|
+
module MarsBase10
|
8
|
+
# Handle the application command line parsing
|
9
|
+
# and the dispatch to various command objects
|
10
|
+
#
|
11
|
+
# @api public
|
12
|
+
class CLI < Thor
|
13
|
+
class_option :"no-color", type: :boolean, default: false, desc: "Disable colorization in output"
|
14
|
+
|
15
|
+
# Error raised by this runner
|
16
|
+
Error = Class.new(StandardError)
|
17
|
+
|
18
|
+
def self.exit_on_failure?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "help, --help, -h", "Describe available commands or one specific command"
|
23
|
+
map %w[-h --help] => :help
|
24
|
+
def help(*args)
|
25
|
+
font = TTY::Font.new(:standard)
|
26
|
+
pastel = Pastel.new(enabled: !options["no-color"])
|
27
|
+
puts pastel.yellow(font.write("Mars Base 10"))
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "launch [SHIP_CONFIG]", "Start Mars Base 10 connected to the Urbit ship defined in SHIP_CONFIG. (Required)"
|
32
|
+
long_desc <<-DESC
|
33
|
+
The SHIP_CONFIG uses the yaml format defined in the ruby urbit-api gem.
|
34
|
+
see https://github.com/Zaxonomy/urbit-ruby
|
35
|
+
DESC
|
36
|
+
method_option :help, aliases: %w[-h --help], type: :boolean, desc: "Display usage information"
|
37
|
+
def launch(config)
|
38
|
+
if options[:help]
|
39
|
+
invoke :help, ["launch"]
|
40
|
+
else
|
41
|
+
if (config)
|
42
|
+
require_relative "comm_central"
|
43
|
+
begin
|
44
|
+
cc = MarsBase10::CommCentral.new config_filename: config
|
45
|
+
cc.activate
|
46
|
+
ensure
|
47
|
+
cc.shutdown
|
48
|
+
end
|
49
|
+
else
|
50
|
+
raise Error, "A SHIP_CONFIG is required to launch."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
desc "version, --version, -v", "print the version number, then exit"
|
57
|
+
map %w[-v --version] => :version
|
58
|
+
def version
|
59
|
+
require_relative "version"
|
60
|
+
puts "v#{MarsBase10::VERSION}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'urbit/urbit'
|
3
|
+
|
4
|
+
require_relative 'graph_rover'
|
5
|
+
require_relative 'viewport'
|
6
|
+
|
7
|
+
module MarsBase10
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
class CommCentral
|
11
|
+
def initialize(config_filename:)
|
12
|
+
@viewport = Viewport.new
|
13
|
+
@rover = GraphRover.new ship_connection: Urbit.connect(config_file: config_filename),
|
14
|
+
viewport: @viewport
|
15
|
+
end
|
16
|
+
|
17
|
+
def activate
|
18
|
+
self.rover.start
|
19
|
+
end
|
20
|
+
|
21
|
+
def shutdown
|
22
|
+
self.rover.stop
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def rover
|
28
|
+
@rover
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'ship'
|
4
|
+
require_relative 'stack'
|
5
|
+
require_relative 'subject'
|
6
|
+
|
7
|
+
module MarsBase10
|
8
|
+
class GraphRover
|
9
|
+
attr_reader :panes, :ship, :viewport
|
10
|
+
|
11
|
+
def initialize(ship_connection:, viewport:)
|
12
|
+
@ship = Ship.new connection: ship_connection
|
13
|
+
@stack = Stack.new
|
14
|
+
@viewport = viewport
|
15
|
+
@viewport.controller = self
|
16
|
+
|
17
|
+
@panes = []
|
18
|
+
|
19
|
+
# The graph list is a fixed width, variable height (full screen) pane on the left.
|
20
|
+
@graph_list_pane = @viewport.add_pane width_pct: 0.3
|
21
|
+
@graph_list_pane.viewing subject: @ship.graph_names
|
22
|
+
|
23
|
+
# The node list is a variable width, fixed height pane in the upper right.
|
24
|
+
@node_list_pane = @viewport.add_variable_width_pane at_col: @graph_list_pane.last_col,
|
25
|
+
height_pct: 0.5
|
26
|
+
@node_list_pane.viewing subject: @ship.node_list
|
27
|
+
|
28
|
+
# The single node viewer is a variable width, variable height pane in the lower right.
|
29
|
+
@node_view_pane = @viewport.add_variable_both_pane at_row: @node_list_pane.last_row,
|
30
|
+
at_col: @graph_list_pane.last_col
|
31
|
+
@node_view_pane.viewing subject: @ship.node
|
32
|
+
|
33
|
+
self.viewport.action_bar = ActionBar.Default.add_action({'i': 'Inspect'})
|
34
|
+
self.viewport.activate pane: @graph_list_pane
|
35
|
+
self.resync
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Called by a pane in this controller for bubbling a key press up
|
40
|
+
#
|
41
|
+
def send(key:)
|
42
|
+
case key
|
43
|
+
when 'd' # (D)ive
|
44
|
+
begin
|
45
|
+
if @node_view_pane.subject.contents[4].include?('true')
|
46
|
+
self.viewport.action_bar.add_action({'p': 'Pop Out'})
|
47
|
+
resource = @graph_list_pane.current_subject
|
48
|
+
node_index = @node_list_pane.current_subject
|
49
|
+
@stack.push(resource)
|
50
|
+
@node_list_pane.clear
|
51
|
+
@node_list_pane.subject.contents = self.ship.fetch_node_children resource: resource, index: node_index
|
52
|
+
@node_list_pane.index = 0
|
53
|
+
end
|
54
|
+
end
|
55
|
+
when 'i' # (I)nspect
|
56
|
+
begin
|
57
|
+
self.viewport.activate pane: @node_list_pane
|
58
|
+
self.viewport.action_bar = ActionBar.Default.add_action({'d': 'Dive In', 'g': 'Graph List'})
|
59
|
+
end
|
60
|
+
when 'g' # (G)raph View
|
61
|
+
unless @graph_list_pane.active?
|
62
|
+
self.viewport.activate pane: @graph_list_pane
|
63
|
+
end
|
64
|
+
when 'p' # (P)op
|
65
|
+
begin
|
66
|
+
if (resource = @stack.pop)
|
67
|
+
@node_list_pane.clear
|
68
|
+
@node_list_pane.subject.contents = self.ship.fetch_node_list(resource: resource)
|
69
|
+
@node_list_pane.index = 0
|
70
|
+
end
|
71
|
+
if (@stack.length == 0)
|
72
|
+
self.viewport.action_bar.remove_action(:p)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
self.resync
|
77
|
+
end
|
78
|
+
|
79
|
+
def start
|
80
|
+
self.viewport.open
|
81
|
+
end
|
82
|
+
|
83
|
+
def stop
|
84
|
+
self.viewport.close
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def resync
|
90
|
+
self.resync_node_view(self.resync_node_list)
|
91
|
+
end
|
92
|
+
|
93
|
+
def resync_node_list
|
94
|
+
resource = @graph_list_pane.current_subject
|
95
|
+
if @graph_list_pane == self.viewport.active_pane
|
96
|
+
@node_list_pane.subject.title = "Nodes of #{resource}"
|
97
|
+
@node_list_pane.clear
|
98
|
+
@node_list_pane.subject.first_row = 0
|
99
|
+
@node_list_pane.subject.contents = self.ship.fetch_node_list resource: resource
|
100
|
+
end
|
101
|
+
resource
|
102
|
+
end
|
103
|
+
|
104
|
+
def resync_node_view(resource)
|
105
|
+
node_index = @node_list_pane.current_subject
|
106
|
+
@node_view_pane.subject.title = "Node #{self.short_index node_index}"
|
107
|
+
@node_view_pane.clear
|
108
|
+
@node_view_pane.subject.contents = self.ship.fetch_node_contents(resource: resource, index: node_index)
|
109
|
+
end
|
110
|
+
|
111
|
+
def short_index(index)
|
112
|
+
return "" if index.nil?
|
113
|
+
tokens = index.split('.')
|
114
|
+
"#{tokens[0]}..#{tokens[tokens.size - 2]}.#{tokens[tokens.size - 1]}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'curses'
|
3
|
+
|
4
|
+
module MarsBase10
|
5
|
+
class Pane
|
6
|
+
attr_accessor :draw_row, :draw_col, :index, :latch, :subject
|
7
|
+
attr_reader :height_pct, :left_edge_col, :top_row, :viewport, :width_pct
|
8
|
+
|
9
|
+
def initialize(viewport:, at_row:, at_col:, height_pct: 1, width_pct: 1)
|
10
|
+
@top_row = at_row
|
11
|
+
@left_edge_col = at_col
|
12
|
+
@height_pct = height_pct
|
13
|
+
@index = 0
|
14
|
+
@latch = -1
|
15
|
+
@subject = nil
|
16
|
+
@win = nil
|
17
|
+
@viewport = viewport
|
18
|
+
@width_pct = width_pct
|
19
|
+
end
|
20
|
+
|
21
|
+
def active?
|
22
|
+
self == self.viewport.active_pane
|
23
|
+
end
|
24
|
+
|
25
|
+
def clear
|
26
|
+
self.prepare_for_writing_contents
|
27
|
+
(0..(self.last_row - 1)).each do |item|
|
28
|
+
self.window.setpos(self.draw_row, self.draw_col)
|
29
|
+
self.window.addstr("")
|
30
|
+
self.window.clrtoeol
|
31
|
+
self.draw_row += 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def current_subject
|
36
|
+
self.subject.at index: self.index
|
37
|
+
end
|
38
|
+
|
39
|
+
def draw
|
40
|
+
self.prepare_for_writing_contents
|
41
|
+
|
42
|
+
(0..(self.max_contents_rows - 1)).each do |item|
|
43
|
+
self.window.setpos(self.draw_row, self.draw_col)
|
44
|
+
# The string here is the gutter followed by the window contents. improving the gutter is tbd.
|
45
|
+
self.window.attron(Curses::A_REVERSE) if item == self.index
|
46
|
+
self.window.addstr("#{"%02d" % item} #{self.subject.at index: item}")
|
47
|
+
self.window.attroff(Curses::A_REVERSE) # if item == self.index
|
48
|
+
self.window.clrtoeol
|
49
|
+
self.draw_row += 1
|
50
|
+
end
|
51
|
+
|
52
|
+
self.draw_border
|
53
|
+
end
|
54
|
+
|
55
|
+
def draw_border
|
56
|
+
self.window.attron(Curses.color_pair(1) | Curses::A_BOLD) if self.active?
|
57
|
+
self.window.box
|
58
|
+
self.draw_title
|
59
|
+
self.window.attroff(Curses.color_pair(1) | Curses::A_BOLD) if self.active?
|
60
|
+
end
|
61
|
+
|
62
|
+
def draw_title
|
63
|
+
self.window.setpos(0, 2)
|
64
|
+
self.window.addstr(" #{self.subject.title} (#{self.subject.rows} total) ")
|
65
|
+
end
|
66
|
+
|
67
|
+
def first_col
|
68
|
+
1
|
69
|
+
end
|
70
|
+
|
71
|
+
def first_row
|
72
|
+
1
|
73
|
+
end
|
74
|
+
|
75
|
+
def gutter_width
|
76
|
+
4
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# This is the _relative_ last column, e.g. the width of the pane in columns.
|
81
|
+
#
|
82
|
+
def last_col
|
83
|
+
[(self.viewport.max_cols * self.width_pct).floor, self.min_column_width].max
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# This is the _relative_ last row, e.g. the height of the pane in columns.
|
88
|
+
#
|
89
|
+
def last_row
|
90
|
+
(self.viewport.max_rows * self.height_pct).floor
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# The pane is latched if it has consumed 1 key 0-9 and is awaiting the next key.
|
95
|
+
#
|
96
|
+
def latched?
|
97
|
+
self.latch != -1
|
98
|
+
end
|
99
|
+
|
100
|
+
def max_contents_rows
|
101
|
+
self.subject.rows
|
102
|
+
end
|
103
|
+
|
104
|
+
def min_column_width
|
105
|
+
self.gutter_width + self.subject.cols + self.right_pad
|
106
|
+
end
|
107
|
+
|
108
|
+
def prepare_for_writing_contents
|
109
|
+
self.draw_row = self.first_row
|
110
|
+
self.draw_col = self.first_col
|
111
|
+
end
|
112
|
+
|
113
|
+
#
|
114
|
+
# process blocks and waits for a keypress.
|
115
|
+
#
|
116
|
+
# this method handles only the "default" keypresses which all controllers/subjects
|
117
|
+
# must support. Any unrecognized key is bubbled to the controller for more specific
|
118
|
+
# handling.
|
119
|
+
#
|
120
|
+
def process
|
121
|
+
key = self.window.getch.to_s
|
122
|
+
case key
|
123
|
+
when 'j'
|
124
|
+
self.set_row(self.index + 1)
|
125
|
+
when 'k'
|
126
|
+
self.set_row(self.index - 1)
|
127
|
+
when 'q'
|
128
|
+
exit 0
|
129
|
+
when ('0'..'9')
|
130
|
+
if self.latched?
|
131
|
+
self.set_row((self.latch * 10) + key.to_i)
|
132
|
+
self.latch = -1
|
133
|
+
else
|
134
|
+
self.latch = key.to_i
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Always send the key to the controller for additional processing...
|
139
|
+
self.viewport.controller.send key: key
|
140
|
+
end
|
141
|
+
|
142
|
+
def right_pad
|
143
|
+
2
|
144
|
+
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# this is a no-op if the index is out of range
|
148
|
+
#
|
149
|
+
def set_row(i)
|
150
|
+
self.subject.scroll_limit = [self.last_row - 1, self.max_contents_rows].min
|
151
|
+
|
152
|
+
if (i < 0)
|
153
|
+
self.subject.scroll_up
|
154
|
+
i = 0
|
155
|
+
end
|
156
|
+
|
157
|
+
# If we've reached the end of the content, it's a no-op.
|
158
|
+
if (i >= self.max_contents_rows)
|
159
|
+
i -= 1
|
160
|
+
end
|
161
|
+
|
162
|
+
if (i >= self.last_row - 2)
|
163
|
+
self.subject.scroll_down
|
164
|
+
i -= 1
|
165
|
+
end
|
166
|
+
|
167
|
+
self.index = i # if (i <= self.max_contents_rows) && (i >= 0)
|
168
|
+
end
|
169
|
+
|
170
|
+
def viewing(subject:)
|
171
|
+
@subject = subject
|
172
|
+
end
|
173
|
+
|
174
|
+
def window
|
175
|
+
return @win if @win
|
176
|
+
@win = Curses::Window.new(self.last_row, self.last_col, self.top_row, self.left_edge_col)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
class VariableBothPane < Pane
|
181
|
+
def last_col
|
182
|
+
self.viewport.max_cols - self.left_edge_col
|
183
|
+
end
|
184
|
+
|
185
|
+
def last_row
|
186
|
+
self.viewport.max_rows - self.top_row
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class VariableWidthPane < Pane
|
191
|
+
def last_col
|
192
|
+
self.viewport.max_cols - self.left_edge_col
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'subject'
|
4
|
+
|
5
|
+
module MarsBase10
|
6
|
+
class Ship
|
7
|
+
def initialize(connection:)
|
8
|
+
@ship = connection
|
9
|
+
end
|
10
|
+
|
11
|
+
def graph_names
|
12
|
+
Subject.new title: 'Graphs', contents: @ship.graph_names
|
13
|
+
end
|
14
|
+
|
15
|
+
def node
|
16
|
+
Subject.new title: 'Node', contents: []
|
17
|
+
end
|
18
|
+
|
19
|
+
def node_list
|
20
|
+
Subject.new title: 'Node List', contents: []
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch_node(resource:, index:)
|
24
|
+
@ship.graph(resource: resource).node(index: index)
|
25
|
+
end
|
26
|
+
|
27
|
+
def fetch_node_children(resource:, index:)
|
28
|
+
self.fetch_node(resource: resource, index: index).children.map {|node| node.index}.sort
|
29
|
+
end
|
30
|
+
|
31
|
+
def fetch_node_contents(resource:, index:)
|
32
|
+
return [] unless (n = self.fetch_node(resource: resource, index: index))
|
33
|
+
n.to_pretty_array
|
34
|
+
end
|
35
|
+
|
36
|
+
def fetch_node_list(resource:)
|
37
|
+
@ship.graph(resource: resource).newest_nodes(count: 20).map {|node| node.index}.sort
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# Original code by Ale Miralles
|
4
|
+
# https://gist.github.com/amiralles/197b4ed1e7034d0e3f79b92ec76a5a80
|
5
|
+
#
|
6
|
+
# pop() wasn't implemented correctly, though. fixed that.
|
7
|
+
#
|
8
|
+
module MarsBase10
|
9
|
+
class Stack
|
10
|
+
class Node
|
11
|
+
attr_accessor :next, :data
|
12
|
+
def initialize data
|
13
|
+
self.data = data
|
14
|
+
self.next = nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_accessor :head, :tail, :length
|
19
|
+
|
20
|
+
# Initialize an empty stack.
|
21
|
+
# Complexity: O(1).
|
22
|
+
def initialize
|
23
|
+
self.head = nil
|
24
|
+
self.tail = nil
|
25
|
+
self.length = 0
|
26
|
+
end
|
27
|
+
|
28
|
+
# Pops all elements from the stack.
|
29
|
+
# Complexity O(n).
|
30
|
+
def clear
|
31
|
+
while peek
|
32
|
+
pop
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Enumerator (common ruby idiom).
|
37
|
+
# Loops over the stack (from head to tail) yielding one item at a time.
|
38
|
+
# Complexity: yield next element is O(1),
|
39
|
+
# yield all elements is O(n).
|
40
|
+
def each
|
41
|
+
return nil unless block_given?
|
42
|
+
|
43
|
+
current = self.head
|
44
|
+
while current
|
45
|
+
yield current
|
46
|
+
current = current.next
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the element that's at the top of the stack without removing it.
|
51
|
+
# Complexity O(1).
|
52
|
+
def peek
|
53
|
+
self.head
|
54
|
+
end
|
55
|
+
|
56
|
+
# Prints the contents of the stack.
|
57
|
+
# Complexity: O(n).
|
58
|
+
def print
|
59
|
+
if self.length == 0
|
60
|
+
puts "empty"
|
61
|
+
else
|
62
|
+
self.each { |node| puts node.data }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Removes the element that's at the top of the stack.
|
67
|
+
# Complexity O(1).
|
68
|
+
def pop
|
69
|
+
return nil unless self.length > 0
|
70
|
+
n = self.head
|
71
|
+
self.head = self.head.next
|
72
|
+
self.tail = nil if self.length == 1
|
73
|
+
self.length -= 1
|
74
|
+
n.data
|
75
|
+
end
|
76
|
+
|
77
|
+
# Inserts a new element into the stack.
|
78
|
+
# Complexity O(1).
|
79
|
+
def push data
|
80
|
+
node = Node.new data
|
81
|
+
if length == 0
|
82
|
+
self.tail = node
|
83
|
+
end
|
84
|
+
|
85
|
+
node.next = self.head
|
86
|
+
self.head = node
|
87
|
+
self.length += 1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MarsBase10
|
4
|
+
class Subject
|
5
|
+
attr_accessor :first_row, :scroll_limit, :title
|
6
|
+
|
7
|
+
def initialize(title: 'Untitled', contents:)
|
8
|
+
@contents = contents
|
9
|
+
@first_row = 0
|
10
|
+
@title = title
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns the item at: the index: relative to the first_row.
|
14
|
+
def at(index:)
|
15
|
+
self.contents[self.first_row + index]
|
16
|
+
end
|
17
|
+
|
18
|
+
def cols
|
19
|
+
return @cols if @cols
|
20
|
+
@cols = @contents.inject(0) {|a, n| n.length > a ? n.length : a}
|
21
|
+
end
|
22
|
+
|
23
|
+
def contents
|
24
|
+
@contents
|
25
|
+
end
|
26
|
+
|
27
|
+
def contents=(a_contents_array)
|
28
|
+
@rows = nil
|
29
|
+
$cols = nil
|
30
|
+
@contents = a_contents_array
|
31
|
+
end
|
32
|
+
|
33
|
+
def rows
|
34
|
+
return @rows if @rows
|
35
|
+
@rows = @contents.size
|
36
|
+
end
|
37
|
+
|
38
|
+
def scroll_down
|
39
|
+
self.first_row = [self.first_row + 1, (self.rows - self.scroll_limit)].min
|
40
|
+
end
|
41
|
+
|
42
|
+
def scroll_up
|
43
|
+
self.first_row = [self.first_row - 1, 0].max
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'curses'
|
3
|
+
|
4
|
+
require_relative 'action_bar'
|
5
|
+
require_relative 'pane'
|
6
|
+
|
7
|
+
module MarsBase10
|
8
|
+
class Viewport
|
9
|
+
attr_accessor :controller
|
10
|
+
attr_reader :panes, :win
|
11
|
+
|
12
|
+
CURSOR_INVISIBLE = 0
|
13
|
+
CURSOR_VISIBLE = 1
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
Curses.init_screen
|
17
|
+
Curses.curs_set(CURSOR_INVISIBLE)
|
18
|
+
Curses.noecho # Do not echo characters typed by the user.
|
19
|
+
|
20
|
+
Curses.start_color if Curses.has_colors?
|
21
|
+
Curses.init_pair(1, Curses::COLOR_RED, Curses::COLOR_BLACK)
|
22
|
+
Curses.init_pair(2, Curses::COLOR_BLACK, Curses::COLOR_CYAN)
|
23
|
+
|
24
|
+
@active_pane = nil
|
25
|
+
@controller = nil
|
26
|
+
|
27
|
+
@action_bar = nil
|
28
|
+
@panes = []
|
29
|
+
|
30
|
+
# this is the whole visible drawing surface.
|
31
|
+
# we don't ever draw on this, but we need it for reference.
|
32
|
+
@win = Curses::Window.new 0, 0, 0, 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def action_bar
|
36
|
+
return @action_bar unless @action_bar.nil?
|
37
|
+
# Make a default action bar. Only movement for now.
|
38
|
+
self.action_bar = ActionBar.new actions: {'j': 'Move Down', 'k': 'Move Up', 'q': 'Quit'}
|
39
|
+
end
|
40
|
+
|
41
|
+
def action_bar=(an_action_bar)
|
42
|
+
@action_bar = an_action_bar
|
43
|
+
@action_bar.display_on viewport: self
|
44
|
+
@action_bar
|
45
|
+
end
|
46
|
+
|
47
|
+
def activate(pane:)
|
48
|
+
@active_pane = pane
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# This is the pane in the Viewport which is actively accepting keyboard input.
|
53
|
+
#
|
54
|
+
def active_pane
|
55
|
+
@active_pane
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Adds a new drawable area (Pane) to the viewport.
|
60
|
+
# By default it is anchored to the top left. (min_row, min_col)
|
61
|
+
# and full screen. (height and width 100%)
|
62
|
+
#
|
63
|
+
def add_pane(at_row: self.min_row, at_col: self.min_col, height_pct: 1, width_pct: 1)
|
64
|
+
p = MarsBase10::Pane.new viewport: self,
|
65
|
+
at_row: at_row,
|
66
|
+
at_col: at_col,
|
67
|
+
height_pct: height_pct,
|
68
|
+
width_pct: width_pct
|
69
|
+
@panes << p
|
70
|
+
@active_pane = p
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_variable_width_pane(at_row: self.min_row, at_col: self.min_col, height_pct:)
|
74
|
+
p = VariableWidthPane.new viewport: self,
|
75
|
+
at_row: at_row,
|
76
|
+
at_col: at_col,
|
77
|
+
height_pct: height_pct
|
78
|
+
@panes << p
|
79
|
+
p
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Adds a new variable width drawable area (VariableBothPane) to the
|
84
|
+
# right-hand side of the viewport.
|
85
|
+
#
|
86
|
+
# The caller must specify the upper left corner (at_row, at_col) but
|
87
|
+
# after that it will automatically adjust its width based upon how
|
88
|
+
# many columns the left pane(s) use.
|
89
|
+
#
|
90
|
+
def add_variable_both_pane(at_row: self.min_row, at_col: self.min_col)
|
91
|
+
p = VariableBothPane.new viewport: self,
|
92
|
+
at_row: at_row,
|
93
|
+
at_col: at_col
|
94
|
+
@panes << p
|
95
|
+
p
|
96
|
+
end
|
97
|
+
|
98
|
+
def close
|
99
|
+
Curses.close_screen
|
100
|
+
end
|
101
|
+
|
102
|
+
def max_cols
|
103
|
+
self.win.maxx
|
104
|
+
end
|
105
|
+
|
106
|
+
def max_rows
|
107
|
+
self.win.maxy - 1
|
108
|
+
end
|
109
|
+
|
110
|
+
def min_col
|
111
|
+
0
|
112
|
+
end
|
113
|
+
|
114
|
+
def min_row
|
115
|
+
0
|
116
|
+
end
|
117
|
+
|
118
|
+
def open
|
119
|
+
loop do
|
120
|
+
self.panes.each do |pane|
|
121
|
+
pane.draw
|
122
|
+
pane.window.refresh
|
123
|
+
end
|
124
|
+
self.action_bar.draw
|
125
|
+
self.action_bar.window.refresh
|
126
|
+
self.active_pane.process
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/lib/mars_base_10.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/mars_base_10/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "mars_base_10"
|
7
|
+
spec.version = MarsBase10::VERSION
|
8
|
+
spec.license = "MIT"
|
9
|
+
spec.authors = ["Daryl Richter"]
|
10
|
+
spec.email = ["daryl@ngzax.com"]
|
11
|
+
spec.homepage = "https://www.ngzax.com"
|
12
|
+
spec.summary = "This is the urbit console you have been waiting for"
|
13
|
+
spec.description = "A keyboard maximalist, curses-based, urbit terminal ui. It uses the (also in development) ruby airlock."
|
14
|
+
|
15
|
+
if spec.respond_to?(:metadata=)
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/Zaxonomy/mars-base-10"
|
18
|
+
spec.metadata["changelog_uri"] = "https://github.com/Zaxonomy/mars-base-10/CHANGELOG.md"
|
19
|
+
end
|
20
|
+
|
21
|
+
spec.required_ruby_version = ">= 2.7.0"
|
22
|
+
|
23
|
+
spec.files = Dir.glob("lib{.rb,/**/*}", File::FNM_DOTMATCH).reject {|f| File.directory?(f) }
|
24
|
+
spec.files += %w[mars_base_10.gemspec] # include the gemspec itself because warbler breaks w/o it
|
25
|
+
|
26
|
+
spec.bindir = "bin"
|
27
|
+
spec.executables = %w[mb10]
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_dependency "curses", "~> 1.4"
|
31
|
+
spec.add_dependency "pastel", "~> 0.8"
|
32
|
+
spec.add_dependency "sorted_set", "~> 1.0"
|
33
|
+
spec.add_dependency "thor", "~> 1.1"
|
34
|
+
spec.add_dependency "tty-font", "~> 0.5"
|
35
|
+
spec.add_dependency "urbit-api", "~> 0.2.1"
|
36
|
+
|
37
|
+
spec.add_development_dependency "pry", "~> 0.13"
|
38
|
+
spec.add_development_dependency "rspec", "~> 3.10"
|
39
|
+
spec.add_development_dependency "rubocop", "~> 1.7"
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mars_base_10
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daryl Richter
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-11-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: curses
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pastel
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.8'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.8'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sorted_set
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: thor
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.1'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: tty-font
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.5'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.5'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: urbit-api
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.2.1
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.2.1
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.13'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.13'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '3.10'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '3.10'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '1.7'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '1.7'
|
139
|
+
description: A keyboard maximalist, curses-based, urbit terminal ui. It uses the (also
|
140
|
+
in development) ruby airlock.
|
141
|
+
email:
|
142
|
+
- daryl@ngzax.com
|
143
|
+
executables:
|
144
|
+
- mb10
|
145
|
+
extensions: []
|
146
|
+
extra_rdoc_files: []
|
147
|
+
files:
|
148
|
+
- bin/mb10
|
149
|
+
- lib/mars_base_10.rb
|
150
|
+
- lib/mars_base_10/action_bar.rb
|
151
|
+
- lib/mars_base_10/cli.rb
|
152
|
+
- lib/mars_base_10/comm_central.rb
|
153
|
+
- lib/mars_base_10/graph_rover.rb
|
154
|
+
- lib/mars_base_10/pane.rb
|
155
|
+
- lib/mars_base_10/ship.rb
|
156
|
+
- lib/mars_base_10/stack.rb
|
157
|
+
- lib/mars_base_10/subject.rb
|
158
|
+
- lib/mars_base_10/version.rb
|
159
|
+
- lib/mars_base_10/viewport.rb
|
160
|
+
- mars_base_10.gemspec
|
161
|
+
homepage: https://www.ngzax.com
|
162
|
+
licenses:
|
163
|
+
- MIT
|
164
|
+
metadata:
|
165
|
+
homepage_uri: https://www.ngzax.com
|
166
|
+
source_code_uri: https://github.com/Zaxonomy/mars-base-10
|
167
|
+
changelog_uri: https://github.com/Zaxonomy/mars-base-10/CHANGELOG.md
|
168
|
+
post_install_message:
|
169
|
+
rdoc_options: []
|
170
|
+
require_paths:
|
171
|
+
- lib
|
172
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
173
|
+
requirements:
|
174
|
+
- - ">="
|
175
|
+
- !ruby/object:Gem::Version
|
176
|
+
version: 2.7.0
|
177
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - ">="
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
requirements: []
|
183
|
+
rubygems_version: 3.1.6
|
184
|
+
signing_key:
|
185
|
+
specification_version: 4
|
186
|
+
summary: This is the urbit console you have been waiting for
|
187
|
+
test_files: []
|