##// END OF EJS Templates
Workbook.current & match_h...
Ville M. Vainio -
Show More
@@ -1,409 +1,431 b''
1 """ ILeo - Leo plugin for IPython
1 """ ILeo - Leo plugin for IPython
2
2
3
3
4 """
4 """
5 import IPython.ipapi
5 import IPython.ipapi
6 import IPython.genutils
6 import IPython.genutils
7 import IPython.generics
7 import IPython.generics
8 from IPython.hooks import CommandChainDispatcher
8 from IPython.hooks import CommandChainDispatcher
9 import re
9 import re
10 import UserDict
10 import UserDict
11 from IPython.ipapi import TryNext
11 from IPython.ipapi import TryNext
12
12
13 ip = IPython.ipapi.get()
13 ip = IPython.ipapi.get()
14 leo = ip.user_ns['leox']
14 leo = ip.user_ns['leox']
15 c,g = leo.c, leo.g
15 c,g = leo.c, leo.g
16
16
17 # will probably be overwritten by user, but handy for experimentation early on
17 # will probably be overwritten by user, but handy for experimentation early on
18 ip.user_ns['c'] = c
18 ip.user_ns['c'] = c
19 ip.user_ns['g'] = g
19 ip.user_ns['g'] = g
20
20
21
21
22 from IPython.external.simplegeneric import generic
22 from IPython.external.simplegeneric import generic
23 import pprint
23 import pprint
24
24
25 def es(s):
25 def es(s):
26 g.es(s, tabName = 'IPython')
26 g.es(s, tabName = 'IPython')
27 pass
27 pass
28
28
29 @generic
29 @generic
30 def format_for_leo(obj):
30 def format_for_leo(obj):
31 """ Convert obj to string representiation (for editing in Leo)"""
31 """ Convert obj to string representiation (for editing in Leo)"""
32 return pprint.pformat(obj)
32 return pprint.pformat(obj)
33
33
34 @format_for_leo.when_type(list)
34 @format_for_leo.when_type(list)
35 def format_list(obj):
35 def format_list(obj):
36 return "\n".join(str(s) for s in obj)
36 return "\n".join(str(s) for s in obj)
37
37
38 attribute_re = re.compile('^[a-zA-Z_][a-zA-Z0-9_]*$')
38 attribute_re = re.compile('^[a-zA-Z_][a-zA-Z0-9_]*$')
39 def valid_attribute(s):
39 def valid_attribute(s):
40 return attribute_re.match(s)
40 return attribute_re.match(s)
41
41
42 def all_cells():
42 def all_cells():
43 d = {}
43 d = {}
44 for p in c.allNodes_iter():
44 for p in c.allNodes_iter():
45 h = p.headString()
45 h = p.headString()
46 if h.startswith('@a '):
46 if h.startswith('@a '):
47 d[h.lstrip('@a ').strip()] = p.parent().copy()
47 d[h.lstrip('@a ').strip()] = p.parent().copy()
48 elif not valid_attribute(h):
48 elif not valid_attribute(h):
49 continue
49 continue
50 d[h] = p.copy()
50 d[h] = p.copy()
51 return d
51 return d
52
52
53
53
54
54
55 def eval_node(n):
55 def eval_node(n):
56 body = n.b
56 body = n.b
57 if not body.startswith('@cl'):
57 if not body.startswith('@cl'):
58 # plain python repr node, just eval it
58 # plain python repr node, just eval it
59 return ip.ev(n.b)
59 return ip.ev(n.b)
60 # @cl nodes deserve special treatment - first eval the first line (minus cl), then use it to call the rest of body
60 # @cl nodes deserve special treatment - first eval the first line (minus cl), then use it to call the rest of body
61 first, rest = body.split('\n',1)
61 first, rest = body.split('\n',1)
62 tup = first.split(None, 1)
62 tup = first.split(None, 1)
63 # @cl alone SPECIAL USE-> dump var to user_ns
63 # @cl alone SPECIAL USE-> dump var to user_ns
64 if len(tup) == 1:
64 if len(tup) == 1:
65 val = ip.ev(rest)
65 val = ip.ev(rest)
66 ip.user_ns[n.h] = val
66 ip.user_ns[n.h] = val
67 es("%s = %s" % (n.h, repr(val)[:20] ))
67 es("%s = %s" % (n.h, repr(val)[:20] ))
68 return val
68 return val
69
69
70 cl, hd = tup
70 cl, hd = tup
71
71
72 xformer = ip.ev(hd.strip())
72 xformer = ip.ev(hd.strip())
73 es('Transform w/ %s' % repr(xformer))
73 es('Transform w/ %s' % repr(xformer))
74 return xformer(rest, n)
74 return xformer(rest, n)
75
75
76 class LeoNode(object, UserDict.DictMixin):
76 class LeoNode(object, UserDict.DictMixin):
77 """ Node in Leo outline
77 """ Node in Leo outline
78
78
79 Most important attributes (getters/setters available:
79 Most important attributes (getters/setters available:
80 .v - evaluate node, can also be alligned
80 .v - evaluate node, can also be alligned
81 .b, .h - body string, headline string
81 .b, .h - body string, headline string
82 .l - value as string list
82 .l - value as string list
83
83
84 Also supports iteration,
84 Also supports iteration,
85
85
86 setitem / getitem (indexing):
86 setitem / getitem (indexing):
87 wb.foo['key'] = 12
87 wb.foo['key'] = 12
88 assert wb.foo['key'].v == 12
88 assert wb.foo['key'].v == 12
89
89
90 Note the asymmetry on setitem and getitem! Also other
90 Note the asymmetry on setitem and getitem! Also other
91 dict methods are available.
91 dict methods are available.
92
92
93 .ipush() - run push-to-ipython
93 .ipush() - run push-to-ipython
94
94
95 """
95 """
96 def __init__(self,p):
96 def __init__(self,p):
97 self.p = p.copy()
97 self.p = p.copy()
98
98
99 def __str__(self):
100 return "<LeoNode %s>" % str(self.p)
101
102 __repr__ = __str__
103
99 def __get_h(self): return self.p.headString()
104 def __get_h(self): return self.p.headString()
100 def __set_h(self,val):
105 def __set_h(self,val):
101 print "set head",val
106 print "set head",val
102 c.beginUpdate()
107 c.beginUpdate()
103 try:
108 try:
104 c.setHeadString(self.p,val)
109 c.setHeadString(self.p,val)
105 finally:
110 finally:
106 c.endUpdate()
111 c.endUpdate()
107
112
108 h = property( __get_h, __set_h, doc = "Node headline string")
113 h = property( __get_h, __set_h, doc = "Node headline string")
109
114
110 def __get_b(self): return self.p.bodyString()
115 def __get_b(self): return self.p.bodyString()
111 def __set_b(self,val):
116 def __set_b(self,val):
112 print "set body",val
117 print "set body",val
113 c.beginUpdate()
118 c.beginUpdate()
114 try:
119 try:
115 c.setBodyString(self.p, val)
120 c.setBodyString(self.p, val)
116 finally:
121 finally:
117 c.endUpdate()
122 c.endUpdate()
118
123
119 b = property(__get_b, __set_b, doc = "Nody body string")
124 b = property(__get_b, __set_b, doc = "Nody body string")
120
125
121 def __set_val(self, val):
126 def __set_val(self, val):
122 self.b = format_for_leo(val)
127 self.b = format_for_leo(val)
123
128
124 v = property(lambda self: eval_node(self), __set_val, doc = "Node evaluated value")
129 v = property(lambda self: eval_node(self), __set_val, doc = "Node evaluated value")
125
130
126 def __set_l(self,val):
131 def __set_l(self,val):
127 self.b = '\n'.join(val )
132 self.b = '\n'.join(val )
128 l = property(lambda self : IPython.genutils.SList(self.b.splitlines()),
133 l = property(lambda self : IPython.genutils.SList(self.b.splitlines()),
129 __set_l, doc = "Node value as string list")
134 __set_l, doc = "Node value as string list")
130
135
131 def __iter__(self):
136 def __iter__(self):
132 """ Iterate through nodes direct children """
137 """ Iterate through nodes direct children """
133
138
134 return (LeoNode(p) for p in self.p.children_iter())
139 return (LeoNode(p) for p in self.p.children_iter())
135
140
136 def __children(self):
141 def __children(self):
137 d = {}
142 d = {}
138 for child in self:
143 for child in self:
139 head = child.h
144 head = child.h
140 tup = head.split(None,1)
145 tup = head.split(None,1)
141 if len(tup) > 1 and tup[0] == '@k':
146 if len(tup) > 1 and tup[0] == '@k':
142 d[tup[1]] = child
147 d[tup[1]] = child
143 continue
148 continue
144
149
145 if not valid_attribute(head):
150 if not valid_attribute(head):
146 d[head] = child
151 d[head] = child
147 continue
152 continue
148 return d
153 return d
149 def keys(self):
154 def keys(self):
150 d = self.__children()
155 d = self.__children()
151 return d.keys()
156 return d.keys()
152 def __getitem__(self, key):
157 def __getitem__(self, key):
153 """ wb.foo['Some stuff'] Return a child node with headline 'Some stuff'
158 """ wb.foo['Some stuff'] Return a child node with headline 'Some stuff'
154
159
155 If key is a valid python name (e.g. 'foo'), look for headline '@k foo' as well
160 If key is a valid python name (e.g. 'foo'), look for headline '@k foo' as well
156 """
161 """
157 key = str(key)
162 key = str(key)
158 d = self.__children()
163 d = self.__children()
159 return d[key]
164 return d[key]
160 def __setitem__(self, key, val):
165 def __setitem__(self, key, val):
161 """ You can do wb.foo['My Stuff'] = 12 to create children
166 """ You can do wb.foo['My Stuff'] = 12 to create children
162
167
163 This will create 'My Stuff' as a child of foo (if it does not exist), and
168 This will create 'My Stuff' as a child of foo (if it does not exist), and
164 do .v = 12 assignment.
169 do .v = 12 assignment.
165
170
166 Exception:
171 Exception:
167
172
168 wb.foo['bar'] = 12
173 wb.foo['bar'] = 12
169
174
170 will create a child with headline '@k bar', because bar is a valid python name
175 will create a child with headline '@k bar', because bar is a valid python name
171 and we don't want to crowd the WorkBook namespace with (possibly numerous) entries
176 and we don't want to crowd the WorkBook namespace with (possibly numerous) entries
172 """
177 """
173 key = str(key)
178 key = str(key)
174 d = self.__children()
179 d = self.__children()
175 if key in d:
180 if key in d:
176 d[key].v = val
181 d[key].v = val
177 return
182 return
178
183
179 if not valid_attribute(key):
184 if not valid_attribute(key):
180 head = key
185 head = key
181 else:
186 else:
182 head = '@k ' + key
187 head = '@k ' + key
183 p = c.createLastChildNode(self.p, head, '')
188 p = c.createLastChildNode(self.p, head, '')
184 LeoNode(p).v = val
189 LeoNode(p).v = val
185 def __delitem__(self,key):
190
186 pass
187 def ipush(self):
191 def ipush(self):
188 """ Does push-to-ipython on the node """
192 """ Does push-to-ipython on the node """
189 push_from_leo(self)
193 push_from_leo(self)
194
190 def go(self):
195 def go(self):
191 """ Set node as current node (to quickly see it in Outline) """
196 """ Set node as current node (to quickly see it in Outline) """
192 c.beginUpdate()
197 c.beginUpdate()
193 try:
198 try:
194 c.setCurrentPosition(self.p)
199 c.setCurrentPosition(self.p)
195 finally:
200 finally:
196 c.endUpdate()
201 c.endUpdate()
197
202
203 def script(self):
204 """ Method to get the 'tangled' contents of the node
205
206 (parse @others, << section >> references etc.)
207 """
208 return g.getScript(c,self.p,useSelectedText=False,useSentinels=False)
209
198 def __get_uA(self):
210 def __get_uA(self):
199 p = self.p
211 p = self.p
200 # Create the uA if necessary.
212 # Create the uA if necessary.
201 if not hasattr(p.v.t,'unknownAttributes'):
213 if not hasattr(p.v.t,'unknownAttributes'):
202 p.v.t.unknownAttributes = {}
214 p.v.t.unknownAttributes = {}
203
215
204 d = p.v.t.unknownAttributes.setdefault('ipython', {})
216 d = p.v.t.unknownAttributes.setdefault('ipython', {})
205 return d
217 return d
218
206 uA = property(__get_uA, doc = "Access persistent unknownAttributes of node")
219 uA = property(__get_uA, doc = "Access persistent unknownAttributes of node")
207
220
208
221
209 class LeoWorkbook:
222 class LeoWorkbook:
210 """ class for 'advanced' node access
223 """ class for 'advanced' node access
211
224
212 Has attributes for all "discoverable" nodes. Node is discoverable if it
225 Has attributes for all "discoverable" nodes. Node is discoverable if it
213 either
226 either
214
227
215 - has a valid python name (Foo, bar_12)
228 - has a valid python name (Foo, bar_12)
216 - is a parent of an anchor node (if it has a child '@a foo', it is visible as foo)
229 - is a parent of an anchor node (if it has a child '@a foo', it is visible as foo)
217
230
218 """
231 """
219 def __getattr__(self, key):
232 def __getattr__(self, key):
220 if key.startswith('_') or key == 'trait_names' or not valid_attribute(key):
233 if key.startswith('_') or key == 'trait_names' or not valid_attribute(key):
221 raise AttributeError
234 raise AttributeError
222 cells = all_cells()
235 cells = all_cells()
223 p = cells.get(key, None)
236 p = cells.get(key, None)
224 if p is None:
237 if p is None:
225 p = add_var(key)
238 p = add_var(key)
226
239
227 return LeoNode(p)
240 return LeoNode(p)
228
241
229 def __str__(self):
242 def __str__(self):
230 return "<LeoWorkbook>"
243 return "<LeoWorkbook>"
231 def __setattr__(self,key, val):
244 def __setattr__(self,key, val):
232 raise AttributeError("Direct assignment to workbook denied, try wb.%s.v = %s" % (key,val))
245 raise AttributeError("Direct assignment to workbook denied, try wb.%s.v = %s" % (key,val))
233
246
234 __repr__ = __str__
247 __repr__ = __str__
235
248
236 def __iter__(self):
249 def __iter__(self):
237 """ Iterate all (even non-exposed) nodes """
250 """ Iterate all (even non-exposed) nodes """
238 cells = all_cells()
251 cells = all_cells()
239 return (LeoNode(p) for p in c.allNodes_iter())
252 return (LeoNode(p) for p in c.allNodes_iter())
240
253
254 current = property(lambda self: LeoNode(c.currentPosition()), doc = "Currently selected node")
255
256 def match_h(self, regex):
257 cmp = re.compile(regex)
258 for node in self:
259 if re.match(cmp, node.h, re.IGNORECASE):
260 yield node
261 return
262
241 ip.user_ns['wb'] = LeoWorkbook()
263 ip.user_ns['wb'] = LeoWorkbook()
242
264
243
265
244
266
245 @IPython.generics.complete_object.when_type(LeoWorkbook)
267 @IPython.generics.complete_object.when_type(LeoWorkbook)
246 def workbook_complete(obj, prev):
268 def workbook_complete(obj, prev):
247 return all_cells().keys()
269 return all_cells().keys() + [s for s in prev if not s.startswith('_')]
248
270
249
271
250 def add_var(varname):
272 def add_var(varname):
251 c.beginUpdate()
273 c.beginUpdate()
252 try:
274 try:
253 p2 = g.findNodeAnywhere(c,varname)
275 p2 = g.findNodeAnywhere(c,varname)
254 if p2:
276 if p2:
255 return
277 return
256
278
257 rootpos = g.findNodeAnywhere(c,'@ipy-results')
279 rootpos = g.findNodeAnywhere(c,'@ipy-results')
258 if not rootpos:
280 if not rootpos:
259 rootpos = c.currentPosition()
281 rootpos = c.currentPosition()
260 p2 = rootpos.insertAsLastChild()
282 p2 = rootpos.insertAsLastChild()
261 c.setHeadString(p2,varname)
283 c.setHeadString(p2,varname)
262 return p2
284 return p2
263 finally:
285 finally:
264 c.endUpdate()
286 c.endUpdate()
265
287
266 def add_file(self,fname):
288 def add_file(self,fname):
267 p2 = c.currentPosition().insertAfter()
289 p2 = c.currentPosition().insertAfter()
268
290
269 push_from_leo = CommandChainDispatcher()
291 push_from_leo = CommandChainDispatcher()
270
292
271 def expose_ileo_push(f, prio = 0):
293 def expose_ileo_push(f, prio = 0):
272 push_from_leo.add(f, prio)
294 push_from_leo.add(f, prio)
273
295
274 def push_ipython_script(node):
296 def push_ipython_script(node):
275 """ Execute the node body in IPython, as if it was entered in interactive prompt """
297 """ Execute the node body in IPython, as if it was entered in interactive prompt """
276 c.beginUpdate()
298 c.beginUpdate()
277 try:
299 try:
278 ohist = ip.IP.output_hist
300 ohist = ip.IP.output_hist
279 hstart = len(ip.IP.input_hist)
301 hstart = len(ip.IP.input_hist)
280 script = g.getScript(c,node.p,useSelectedText=False,forcePythonSentinels=False,useSentinels=False)
302 script = node.script()
281
303
282 script = g.splitLines(script + '\n')
304 script = g.splitLines(script + '\n')
283
305
284 ip.runlines(script)
306 ip.runlines(script)
285
307
286 has_output = False
308 has_output = False
287 for idx in range(hstart,len(ip.IP.input_hist)):
309 for idx in range(hstart,len(ip.IP.input_hist)):
288 val = ohist.get(idx,None)
310 val = ohist.get(idx,None)
289 if val is None:
311 if val is None:
290 continue
312 continue
291 has_output = True
313 has_output = True
292 inp = ip.IP.input_hist[idx]
314 inp = ip.IP.input_hist[idx]
293 if inp.strip():
315 if inp.strip():
294 es('In: %s' % (inp[:40], ))
316 es('In: %s' % (inp[:40], ))
295
317
296 es('<%d> %s' % (idx, pprint.pformat(ohist[idx],width = 40)))
318 es('<%d> %s' % (idx, pprint.pformat(ohist[idx],width = 40)))
297
319
298 if not has_output:
320 if not has_output:
299 es('ipy run: %s (%d LL)' %( node.h,len(script)))
321 es('ipy run: %s (%d LL)' %( node.h,len(script)))
300 finally:
322 finally:
301 c.endUpdate()
323 c.endUpdate()
302
324
303 # this should be the LAST one that will be executed, and it will never raise TryNext
325 # this should be the LAST one that will be executed, and it will never raise TryNext
304 expose_ileo_push(push_ipython_script, 1000)
326 expose_ileo_push(push_ipython_script, 1000)
305
327
306 def eval_body(body):
328 def eval_body(body):
307 try:
329 try:
308 val = ip.ev(body)
330 val = ip.ev(body)
309 except:
331 except:
310 # just use stringlist if it's not completely legal python expression
332 # just use stringlist if it's not completely legal python expression
311 val = IPython.genutils.SList(body.splitlines())
333 val = IPython.genutils.SList(body.splitlines())
312 return val
334 return val
313
335
314 def push_plain_python(node):
336 def push_plain_python(node):
315 if not node.h.endswith('P'):
337 if not node.h.endswith('P'):
316 raise TryNext
338 raise TryNext
317 script = g.getScript(c,node.p,useSelectedText=False,forcePythonSentinels=False,useSentinels=False)
339 script = node.script()
318 lines = script.count('\n')
340 lines = script.count('\n')
319 try:
341 try:
320 exec script in ip.user_ns
342 exec script in ip.user_ns
321 except:
343 except:
322 print " -- Exception in script:\n"+script + "\n --"
344 print " -- Exception in script:\n"+script + "\n --"
323 raise
345 raise
324 es('ipy plain: %s (%d LL)' % (node.h,lines))
346 es('ipy plain: %s (%d LL)' % (node.h,lines))
325
347
326 expose_ileo_push(push_plain_python, 100)
348 expose_ileo_push(push_plain_python, 100)
327
349
328 def push_cl_node(node):
350 def push_cl_node(node):
329 """ If node starts with @cl, eval it
351 """ If node starts with @cl, eval it
330
352
331 The result is put to root @ipy-results node
353 The result is put to root @ipy-results node
332 """
354 """
333 if not node.b.startswith('@cl'):
355 if not node.b.startswith('@cl'):
334 raise TryNext
356 raise TryNext
335
357
336 p2 = g.findNodeAnywhere(c,'@ipy-results')
358 p2 = g.findNodeAnywhere(c,'@ipy-results')
337 val = node.v
359 val = node.v
338 if p2:
360 if p2:
339 es("=> @ipy-results")
361 es("=> @ipy-results")
340 LeoNode(p2).v = val
362 LeoNode(p2).v = val
341 es(val)
363 es(val)
342
364
343 expose_ileo_push(push_cl_node,100)
365 expose_ileo_push(push_cl_node,100)
344
366
345 def push_position_from_leo(p):
367 def push_position_from_leo(p):
346 push_from_leo(LeoNode(p))
368 push_from_leo(LeoNode(p))
347
369
348 ip.user_ns['leox'].push = push_position_from_leo
370 ip.user_ns['leox'].push = push_position_from_leo
349
371
350 def leo_f(self,s):
372 def leo_f(self,s):
351 """ open file(s) in Leo
373 """ open file(s) in Leo
352
374
353 Takes an mglob pattern, e.g. '%leo *.cpp' or %leo 'rec:*.cpp'
375 Takes an mglob pattern, e.g. '%leo *.cpp' or %leo 'rec:*.cpp'
354 """
376 """
355 import os
377 import os
356 from IPython.external import mglob
378 from IPython.external import mglob
357
379
358 files = mglob.expand(s)
380 files = mglob.expand(s)
359 c.beginUpdate()
381 c.beginUpdate()
360 try:
382 try:
361 for fname in files:
383 for fname in files:
362 p = g.findNodeAnywhere(c,'@auto ' + fname)
384 p = g.findNodeAnywhere(c,'@auto ' + fname)
363 if not p:
385 if not p:
364 p = c.currentPosition().insertAfter()
386 p = c.currentPosition().insertAfter()
365
387
366 p.setHeadString('@auto ' + fname)
388 p.setHeadString('@auto ' + fname)
367 if os.path.isfile(fname):
389 if os.path.isfile(fname):
368 c.setBodyString(p,open(fname).read())
390 c.setBodyString(p,open(fname).read())
369 c.selectPosition(p)
391 c.selectPosition(p)
370 finally:
392 finally:
371 c.endUpdate()
393 c.endUpdate()
372
394
373 ip.expose_magic('leo',leo_f)
395 ip.expose_magic('leo',leo_f)
374
396
375 def leoref_f(self,s):
397 def leoref_f(self,s):
376 """ Quick reference for ILeo """
398 """ Quick reference for ILeo """
377 import textwrap
399 import textwrap
378 print textwrap.dedent("""\
400 print textwrap.dedent("""\
379 %leo file - open file in leo
401 %leo file - open file in leo
380 wb.foo.v - eval node foo (i.e. headstring is 'foo' or '@ipy foo')
402 wb.foo.v - eval node foo (i.e. headstring is 'foo' or '@ipy foo')
381 wb.foo.v = 12 - assign to body of node foo
403 wb.foo.v = 12 - assign to body of node foo
382 wb.foo.b - read or write the body of node foo
404 wb.foo.b - read or write the body of node foo
383 wb.foo.l - body of node foo as string list
405 wb.foo.l - body of node foo as string list
384
406
385 for el in wb.foo:
407 for el in wb.foo:
386 print el.v
408 print el.v
387
409
388 """
410 """
389 )
411 )
390 ip.expose_magic('leoref',leoref_f)
412 ip.expose_magic('leoref',leoref_f)
391
413
392 def show_welcome():
414 def show_welcome():
393 print "------------------"
415 print "------------------"
394 print "Welcome to Leo-enabled IPython session!"
416 print "Welcome to Leo-enabled IPython session!"
395 print "Try %leoref for quick reference."
417 print "Try %leoref for quick reference."
396 import IPython.platutils
418 import IPython.platutils
397 IPython.platutils.set_term_title('ILeo')
419 IPython.platutils.set_term_title('ILeo')
398 IPython.platutils.freeze_term_title()
420 IPython.platutils.freeze_term_title()
399
421
400 def run_leo_startup_node():
422 def run_leo_startup_node():
401 p = g.findNodeAnywhere(c,'@ipy-startup')
423 p = g.findNodeAnywhere(c,'@ipy-startup')
402 if p:
424 if p:
403 print "Running @ipy-startup nodes"
425 print "Running @ipy-startup nodes"
404 for n in LeoNode(p):
426 for n in LeoNode(p):
405 push_from_leo(n)
427 push_from_leo(n)
406
428
407 run_leo_startup_node()
429 run_leo_startup_node()
408 show_welcome()
430 show_welcome()
409
431
General Comments 0
You need to be logged in to leave comments. Login now