made it work, more or less, with ruby 1.9
[nyuron.git] / classes / neuron.rb
blobf397f6d2651c822f7310cbe2c5ee098ea76ec970
1 ## classes/neuron
3 ## A description of the attributes:
4 ##
5 ##   @rowid (Integer) from 1 to the limit of the SQL database
6 ##   contains an Integer with the Row ID of the neuron in the sql database.
7 ##   
8 ##   @key (String or nil)
9 ##   contains the key, if it exists. Texts usually don't have a key.
10 ##   
11 ##   @type (String) one of { cmd map opt tag txt }
12 ##   depending on the type, the neuron might have a different behaviour
13 ##   
14 ##   @ctime (Integer)
15 ##   the creation time, as an integer time stamp. 0 if not found.
16 ##   
17 ##   @mtime (Integer)
18 ##   the modification time, as an integer time stamp. 0 if not found.
19 ##   
20 ##   @prio (Integer or Float)
21 ##   The priority, for sorting.
22 ##   
23 ##   @tags (Array<String>)
24 ##   A list of strings, the tags that are assigned to this neuron.
25 ##   
26 ##   @value (may be any type)
27 ##   The value is normally a string or nil. Options might use @info2 to specify
28 ##   other types. The main content is here, eg text, command code, setting values
29 ##   
30 ##   @descr (String or nil)
31 ##   If this is set, it might be shown instead of the usual "Type: key => value"
32 ##   
33 ##   @onchg (String or nil)
34 ##   The code which is executed every time the value is changed via value=
35 ##   
36 ##   @sortstring (String)
37 ##   The string by which it will be sorted when using the value keyword.
38 ##   This is the same as @value, except for options, where it's the raw string.
40 ##   @time (false or Time)
41 ##   is false, unless @type is "txt" and @info1 is not nil
43 ##   @bold_on_the_left, @bold_on_the_right (Integer)
44 ##   how many characters from the left or from the right should be made bold?
45 ##   @bold_on_the_left is usually 18,
46 ##   @bold_on_the_right is the width of the string that indicates priority.
47 ## 
49 ## each neuron has generic info-attributes, which contain type-specific data.
50 ## Here is a Guidemap. As you can see,
51 ## there is still plenty of room for additions in the future.
52 ##   Type | Long Name    | @info1               | @info2       |   @info3
53 ##   -----+--------------+----------------------+--------------+------------
54 ##   cmd  | Command      | Tabcompletion Code   |              |
55 ##   map  | Keymap       |                      |              |
56 ##   opt  | Option       | (used to be @onchg)  | Datatype(s)  |
57 ##   tag  | Tag          |                      |              |
58 ##   txt  | Text         | Deadline (timestamp) | Reoccur Code |
59 class Neuron
60         include API
62         ## constructors {{{
64         def initialize(table)
65                 @rowid, @key, @type, @ctime, @mtime, @prio, @tags,
66                         @value, @descr, @onchg, @info1, @info2, @info3 = table
67                 
68                 ## default values
69                 @prio = @prio ? @prio.to_i : 0
70                 @value = @value ? @value : ""
71                 @sortstring = @value
72                 @key = @key ? @key : ""
73                 @rowid = @rowid.to_i
74                 @mtime ||= 0
75                 @ctime ||= 0
76                 @tags = Convert.query_to_tags(@tags)
77                 @raw_value = @value
78                 @tagstring = nil
80                 if option?
81                         @value = Convert.query_to_option(value, @info2)
82                 end
84                 @time = false
85                 if text?
86                         if @tags.include? 'always_now'
87                                 @info1 = $now.to_i - 1
88                                 @time = Time.at(@info1)
89                         elsif @info1 and not @info1.empty?
90                                 @info1 = @info1.to_i
91                                 @time = Time.at(@info1)
92                                 if @info2 and not @info2.empty? and @info1 < $now.to_i and @tags.include?('r')
93                                         update_reoccur_option
94                                 end
95                         end
96                 end
98                 @bold_on_the_left = 18
99                 @bold_on_the_right = 0
100                 @width = API.cols
101         end
103         def self.from_query(condition, *args)
104                 test = Query.get_where(condition, *args)
105                 test ? new(test) : EmptyNeuron
106         end
108         def self.from_rowid(n)
109                 from_query("rowid=?", n) || EmptyNeuron
110         end
112         def self.find(type, key)
113                 from_query("type=? AND key=?", type, key) || EmptyNeuron
114         end
116         ## }}}
117         ## attributes and related stuff {{{
119         PAD_WITH_TYPE = 10
120         PAD_WITHOUT_TIME = 10
121         PAD_WITH_TIME = 18
123         TYPES_MAP = {
124                 "cmd" => "Command",
125                 "map" => "Keymap",
126                 "opt" => "Option",
127                 "tag" => "Tag",
128                 "txt" => "Text"
129         }
132         %w(key type ctime mtime prio descr onchg info1 info2 info3).each do |key|
133                 eval <<-DONE
134                         def #{key}() @#{key} end
135                         def #{key}=(x)
136                                 reset
137                                 @#{key} = x
138                                 Query.set(self, '#{key}', x)
139                         end
140                 DONE
141         end
143         attr_reader(*%w(rowid tags value bold_on_the_left bold_on_the_right sortstring time raw_value tagstring))
144         alias row_id rowid
146         def empty?() false end
148         def tags=(x)
149                 oldtags = @tags
150                 if Array === x
151                         @tags = x
152                 else
153                         @tags = Convert.string_to_tags(x)
154                 end
155                 @tags.uniq!
156                 if oldtags != @tags
157                         reset
158                         Query.set(self, 'tags', Convert.tags_to_query(@tags))
159                 end
160                 update_reoccur_option if @tags.include?('r')
161                 @tags
162         end
164         def update_reoccur_option
165                 if Opt.next_reoccur.nil?
166                         Query.create_opt("next_reoccur", @info1, "int")
167                 elsif Opt.next_reoccur.to_i > @info1 or Opt.next_reoccur.to_i == 0
168                         Opt.next_reoccur = @info1
169                 end
170         end
172         def prio_add(int)
173                 self.prio += int
174         end
176         def tags_add(*args)
177                 self.tags += args
178                 args
179 #               self.tags = (self.tags + args).uniq
180         end
182         def tags_remove(*args)
183                 self.tags -= args
184         end
186         def tags_toggle(*args)
187                 for arg in args
188                         if @tags.include? arg
189                                 tags_remove arg
190                         else
191                                 tags_add arg
192                         end
193                 end
194         end
196         def info1=(x)
197                 reset
198                 if text?
199                         case x
200                         when Time, Numeric
201                                 @info1 = x.to_i
202                         when String
203                                 if x.empty?
204                                         @info1 = nil
205                                 else
206                                         @info1 = x.to_i
207                                 end
208                         when NilClass
209                                 @info1 = nil
210                         end
211                 else
212                         @info1 = x
213                 end
214                 Query.set(self, 'info1', @info1)
215         end
217         def info2=(x)
218                 if text?
219                         update_reoccur_option
220                 end
221                 Query.set(self, 'info2', (@info2 =x))
222         end
224         ## sets the value to arg. onchange-actions and query-to-option
225         ## conversions are handled here.
226         def set_to(arg)
227                 reset
228                 if option? and
229                                 (arg.is_a? String and
230                                 (@info2.nil? or
231                                 String === @info2 and @info2 !~ /^str/))
232                         arg = Convert.query_to_option(arg, @info2)
233                 end
234                 if @onchg and not @onchg.empty?
235                         code = @onchg
236                         if code =~ /^alias (\w+)$/
237                                 ## TODO: @onchg aliases
238                         end
240                         @oldvalue = @value
241                         @value = arg
243                         result = nil
244                         begin
245                                 result = eval(code)
246                         rescue Exception
247                                 Console.raise
248                         end
250                         @value = result unless result.nil?
251                 else
252                         @value = arg
253                 end
254                 @sortstring = @value
256                 Cache << self
257         end
259         ## updates the value in the database.
260         def update_value_in_database()
261                 if option?
262                         query_value = Convert.option_to_query(@value, @info2)
263                 else
264                         query_value = @value
265                 end
267                 Query.set(self, 'value', query_value)
268         end
270         ## use set_to(arg) and if the value has changed, update it in the database.
271         def value=(arg)
272                 oldval = @value
274                 ## don't use destructive operations on @value, pretty please.
275                 ## if its rly necessary, use oldval = @value.dup above
276                 set_to(arg)
278                 if oldval != @value
279                         update_value_in_database()
280                 end
281         end
283         ## TODO: loop detection :(
284         def advance(int)
285                 log "I (#{@value[0..20]}..)ADVANCE BY #{int}"
286                 self.info1 += int
287                 @time = Time.at(@info1)
288                 self.tags += ['r'] unless @tags.include?('r')
289         end
291         def descend(int)
292                 log "I (#{@value[0..20]}..)DESCEND BY #{int}"
293                 self.info1 -= int
294                 @time = Time.at(@info1)
295 #               self.tags += ['r'] unless @tags.include?('r')
296         end
298         def run_code_in_environment(code)
299                 env = Environment.new
300                 env.option = key
301                 env.value = val
302                 env.types = types
303                 env.oldvalue = oldval
305                 result = env.run(code)
306                 val = result unless result.nil?
307         end
309         def is_text?() @type == "txt" end
311         def key?() not (@key.nil? or @key.empty?) end
312         def value?() not (@value.nil? or @value.empty?) end
313         def option?() @type == "opt" end
314         def keymap?() @type == "map" end
315         def command?() @type == "cmd" end
316         def text?() @type == "txt" end
317         def tag?() @type == "tag" end
318         def time?() @time != false end
319         def descr?() @descr and !@descr.empty? end
320         def this() self end
321         def time=(x)
322                 if is_text?
323                         self.info1 = x
324                         update_reoccur_option if @tags.include? 'r'
325                 end
326                 return x
327         end
328         def lines()
329                 return 1 unless text? or command?
330                 self.line(0) unless @line
331                 @line.size
332         end
333         def tabcomp?()
334                 (command?) ? (@info1 and not @info1.empty?) : (false)
335         end
336         def tabcomp()
337                 (command? and @info1) ? (@info1) : ('')
338         end
340         def update()
341                 initialize(Query.get_where("rowid=#{@rowid}"))
342                 reset
343         end
345         def reoccur!()
346                 return unless text?
347                 self.tags -= ['r']
348                 begin
349                         eval(@info2)
350                 rescue Exception
351                         lograise
352                 end
353         end
355         def to_s(wid = nil)
356                 wid ||= API.cols
357                 if @string.nil?
358                         generate_new_string
359                         @width = -1 ## to force a resize
360                 end
361                 if wid != @width
362                         resize(wid)
363                         @line = nil
364                 end
365                 return @string
366         end
368         def reset()
369                 @string = nil
370                 Info.request_draw = true
371         end
373         def delete()
374                 Query.delete(self)
375         end
377         ## }}}
378         ## create string {{{
380         def line(n)
381                 return "..." unless text? or command?
383                 return @line[n] || "<nil>" if @line
385                 @line = []
386                 if descr?
387                         @line << @descr
388                 end
389                 rest = @value.dup.strip
390                 cols = CLI.cols
391                 max = cols - PAD_WITHOUT_TIME
393                 until rest.empty?
394                         if rest[0] == ?\n
395                                 rest.slice!(0)
396                                 @line << "" unless rest.empty?
397                                 next
398                         end
400                         current = rest[0...max]
401                         if ix = current.index("\n")
402                                 current = current[0...ix]
403                                 rest.slice!(0, ix + 1)
404                         else
405                                 rest.slice!(0, max)
406                         end
407                         @line << current.ljust(max)
408                 end
409         end
411         private
412         def generate_new_string
413                 @right = ""
414                 @left = ""
416                 txt = @type == "txt"
418                 ## add tags
419                 if Array === @tags
420                         tmp = @tags.dup
421                         if Array === (hide = Opt.hide_tags_dynamic) and !hide.empty?
422                                 tmp -= hide
423                         end
424                         if Array === (hide = Opt.hide_tags_static) and !hide.empty?
425                                 tmp -= hide
426                         end
427                         @tagstring = tmp.join(" ")
428                         @right << @tagstring
429                 end
431                 ## priority
432                 if Numeric === @prio and @prio != 0
433                         if @prio < -10
434                                 str = " -#{@prio.abs}"
435                         elsif @prio < 0
436                                 str = " #{'-' * @prio.to_i.abs}"
437                         elsif @prio > 10
438                                 str = " +#{@prio}"
439                         elsif @prio > 0
440                                 str = " #{'+' * @prio.to_i}"
441                         end
442                         @right << str
443                         @bold_on_the_right = str.size
444                 else
445                         @bold_on_the_right = 0
446                 end
448                 if txt
449                         if @info1.nil?
450                                 @left << "       "
451                                 @left << (@value.include?("\n") ? "**" : "  ")
452                                 @left << "*  "
453                                 @bold_on_the_left = PAD_WITHOUT_TIME
454                         else
455                                 @left << "#{Convert.time_to_string(@info1).rjust(18)}: "
456                                 @bold_on_the_left = PAD_WITH_TIME
457                         end
458                 else
459                         @bold_on_the_left = PAD_WITH_TYPE
460                         @left << "#{TYPES_MAP[@type].rjust(10) rescue ''}: "
461                 end
463                 if @descr and !@descr.empty? and @type != "txt"
464                         unless @key.nil? or (@key.is_a? String and @key.empty?)
465 #                               @left.slice! -2
466                                 @left << "#@key "
467                                 @bold_on_the_left = @left.size
468                         end
469                         unless @value.nil?
470                                 @left << "#{@descr.nolinebreaks}"
471                         end
472                 else
473                         unless @key.nil? or (String === @key and @key.empty?)
474                                 @left << "#{@key}: "
475                         end
477                         unless @value.nil?
478                                 val = (@value.is_a? String) ? @value.to_s : @value.inspect
479                                 if pos = val.index("\n")
480                                         if @type == 'txt'
481                                                 val = val[0, pos]
482                                         else
483                                                 val = val.nolinebreaks
484                                         end
485                                 end
486                                 @left << val
487                         end
488                 end
489         end
491         private
492         ## don't regenerate the whole string if the window is resized.
493         ## There are two seperate parts, a left and a right part of the string,
494         ## and this method puts them together.
495         def resize(new_width)
496                 @width = new_width
497                 
498                 if @width < @right.size + 30
499                         ## favour left side, so you can see shit when the terminal is small
500                         left  = @left [0..@width]
501                         right = @right[0..@width-left.size]
502                         @string = left.ljust(@width - right.size) + right
503                 else
504                         ## favour right side
505                         right = @right[0..@width]
506                         left  = @left [0...@width-right.size]
507                         @string = left + right.rjust(@width - left.size)
508                 end
509         end
510         ## }}}