fs-ticket 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +19 -0
- data/bin/fs-ticket +2 -0
- data/lib/fs-ticket.rb +86 -0
- data/lib/render.rb +42 -0
- data/lib/ticket.rb +76 -0
- data/lib/view/add-ticket-view.rb +129 -0
- data/lib/view/report-view.rb +170 -0
- data/lib/view/ticket-view.rb +80 -0
- data/lib/widget/button-widget.rb +37 -0
- data/lib/widget/dropdown-widget.rb +91 -0
- data/lib/widget/text-field-widget.rb +42 -0
- data/lib/widget/text-line-widget.rb +36 -0
- metadata +89 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 146341c184c747b654b08d7e1c7145d410a2b2e4
|
4
|
+
data.tar.gz: 083d0e74aa12dd6b04774eb0a6253cdc4e398b3e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d0f40c88ca36c2b2f5740b4e2f4eed5823043d113eac98b3569f0e482266993abcd865bc16fcc0ac2eff600b84e4bec31a17fb1d430109dd064fbb97ef8fc1be
|
7
|
+
data.tar.gz: 7736fb517c07a77a01ef7401d427fad364c1247ffd5c3e20fea11dacca7015b057da5bc4c246d27e909e69fcbf1f167335b8582ee483a14d110dd6c7a5ee17e2
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2015 Mark McCurry
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/bin/fs-ticket
ADDED
data/lib/fs-ticket.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require "sqlite3"
|
2
|
+
require "curses"
|
3
|
+
require "pp"
|
4
|
+
|
5
|
+
require_relative "ticket.rb"
|
6
|
+
require_relative "render.rb"
|
7
|
+
FossilCheckoutName = ".fslckout"
|
8
|
+
|
9
|
+
$fossil_repo_db = nil
|
10
|
+
|
11
|
+
def find_file_prefix(fname, prefix="")
|
12
|
+
if(prefix.length > 10)
|
13
|
+
nil
|
14
|
+
else
|
15
|
+
begin
|
16
|
+
f = File.open(prefix+fname)
|
17
|
+
f.close
|
18
|
+
prefix
|
19
|
+
rescue Exception=>e
|
20
|
+
find_file_prefix(fname, prefix+"../")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
#1. Identify Fossil Checkout File
|
27
|
+
#2. Identify Main Repo File
|
28
|
+
#3. Obtain A Full Dump of Tickets
|
29
|
+
|
30
|
+
#Locate file
|
31
|
+
FilePrefix = find_file_prefix(FossilCheckoutName)
|
32
|
+
if(FilePrefix.nil?)
|
33
|
+
puts "This directory doesn't appear to be part of a fossil repo..."
|
34
|
+
puts "Please make sure that a #{FossilCheckoutName} file is visible to this tool"
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
|
38
|
+
#Verify File Exists
|
39
|
+
f = File.open(FilePrefix+FossilCheckoutName)
|
40
|
+
f.close
|
41
|
+
|
42
|
+
#Grab The Real Repo
|
43
|
+
SQLite3::Database.new(FilePrefix+FossilCheckoutName) do |db|
|
44
|
+
db.execute("select value from vvar where name is \"repository\"") do |val|
|
45
|
+
$fossil_repo_db = FilePrefix+val[0]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_tickets
|
50
|
+
#Verify The Real Repo Exists
|
51
|
+
f = File.open($fossil_repo_db)
|
52
|
+
f.close
|
53
|
+
|
54
|
+
#Grab The Tickets
|
55
|
+
tickets = []
|
56
|
+
SQLite3::Database.new($fossil_repo_db) do |db|
|
57
|
+
db.execute("select tkt_uuid from ticket order by tkt_ctime") do |val|
|
58
|
+
tickets << val[0]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
#Display The Tickets
|
63
|
+
tkt_objs = []
|
64
|
+
display_id = 1
|
65
|
+
$db ||= SQLite3::Database.new($fossil_repo_db)
|
66
|
+
tickets.each do |id|
|
67
|
+
$db.execute("select title, status from ticket where tkt_uuid = \"#{id}\"") do |val|
|
68
|
+
if(val[1] != "Fixed")
|
69
|
+
#puts "#{display_id}. [#{val[1]}] #{val[0]}"
|
70
|
+
display_id += 1
|
71
|
+
end
|
72
|
+
tkt_objs << Ticket.new($db, id)
|
73
|
+
#puts tkt_objs[-1].title
|
74
|
+
#puts tkt_objs[-1].status
|
75
|
+
end
|
76
|
+
end
|
77
|
+
tkt_objs
|
78
|
+
end
|
79
|
+
|
80
|
+
TableRender.new{get_tickets}
|
81
|
+
$db.close
|
82
|
+
|
83
|
+
#Grab an action
|
84
|
+
# A - add [id]
|
85
|
+
# F - fix [id]
|
86
|
+
# V - view [id]
|
data/lib/render.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
#Widgets
|
2
|
+
require_relative "widget/text-line-widget.rb"
|
3
|
+
require_relative "widget/text-field-widget.rb"
|
4
|
+
require_relative "widget/dropdown-widget.rb"
|
5
|
+
require_relative "widget/button-widget.rb"
|
6
|
+
|
7
|
+
#Application Views
|
8
|
+
require_relative "view/report-view.rb"
|
9
|
+
require_relative "view/ticket-view.rb"
|
10
|
+
require_relative "view/add-ticket-view.rb"
|
11
|
+
|
12
|
+
|
13
|
+
#Curses render
|
14
|
+
class TableRender
|
15
|
+
def initialize(&block)
|
16
|
+
@tickets = block.call
|
17
|
+
@screen = nil
|
18
|
+
init_curses
|
19
|
+
view = ReportView.new(@screen, @tickets, block)
|
20
|
+
view.interact
|
21
|
+
end
|
22
|
+
|
23
|
+
# Perform the curses setup
|
24
|
+
def init_curses
|
25
|
+
# signal(SIGINT, finish)
|
26
|
+
|
27
|
+
Curses.init_screen
|
28
|
+
Curses.raw
|
29
|
+
Curses.nonl
|
30
|
+
#Curses.cbreak
|
31
|
+
Curses.noecho
|
32
|
+
Curses.curs_set(0)
|
33
|
+
Curses.ESCDELAY = 10
|
34
|
+
Curses.start_color
|
35
|
+
Curses.init_pair(1, Curses::COLOR_WHITE, Curses::COLOR_BLUE);
|
36
|
+
|
37
|
+
@screen = Curses.stdscr
|
38
|
+
|
39
|
+
@screen.scrollok(true)
|
40
|
+
@screen.keypad(true)
|
41
|
+
end
|
42
|
+
end
|
data/lib/ticket.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
class Ticket
|
2
|
+
def initialize(database, id)
|
3
|
+
@db = database
|
4
|
+
@ticketid = id
|
5
|
+
@title = nil
|
6
|
+
@status = nil
|
7
|
+
@comments = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def select(field)
|
11
|
+
result = nil
|
12
|
+
@db.execute(
|
13
|
+
"select #{field} from ticket where tkt_uuid is ?",
|
14
|
+
@ticketid.to_s) do |val|
|
15
|
+
result = val[0]
|
16
|
+
end
|
17
|
+
result
|
18
|
+
end
|
19
|
+
|
20
|
+
def title
|
21
|
+
@title ||= select("title")
|
22
|
+
end
|
23
|
+
|
24
|
+
def status
|
25
|
+
@status ||= select("status")
|
26
|
+
end
|
27
|
+
|
28
|
+
def type
|
29
|
+
@type ||= select("type")
|
30
|
+
end
|
31
|
+
|
32
|
+
def subsystem
|
33
|
+
@subsystem ||= select("subsystem")
|
34
|
+
end
|
35
|
+
|
36
|
+
def comment_id
|
37
|
+
@comment_id ||= select("tkt_id")
|
38
|
+
end
|
39
|
+
|
40
|
+
def old_comments
|
41
|
+
res = select("comment")
|
42
|
+
res ||= ""
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def comments
|
47
|
+
if(!@comments)
|
48
|
+
old = old_comments
|
49
|
+
res = @db.execute("select icomment from ticketchng where tkt_id is ?", comment_id)
|
50
|
+
if(res)
|
51
|
+
@comments = old+res.join
|
52
|
+
else
|
53
|
+
@comments = old+""
|
54
|
+
end
|
55
|
+
@comments.gsub!(/\r\n/, "\n")
|
56
|
+
end
|
57
|
+
@comments
|
58
|
+
end
|
59
|
+
|
60
|
+
def priority
|
61
|
+
@priority ||= select("priority")
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def resolve
|
66
|
+
if(status == "Open")
|
67
|
+
`fossil ticket set #{@ticketid} status Fixed`
|
68
|
+
@status = nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def match(regex)
|
73
|
+
(comments && comments.match(regex)) || (title && title.match(regex))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
@@ -0,0 +1,129 @@
|
|
1
|
+
class AddTicketView
|
2
|
+
def initialize(screen)
|
3
|
+
@screen = screen
|
4
|
+
@props = Hash.new
|
5
|
+
@title = TextLineWidget.new(@screen, "", 11, 1, 30)
|
6
|
+
@priort = DropdownWidget.new(@screen, "Zero", 11, 2, 30)
|
7
|
+
@type = DropdownWidget.new(@screen, "Unclassified", 11, 3, 30)
|
8
|
+
@desc = TextFieldWidget.new(@screen, "", 0, 5, 78, 14)
|
9
|
+
@cancel = ButtonWidget.new(@screen, "Cancel", 20, 20)
|
10
|
+
@submit = ButtonWidget.new(@screen, "Submit", 0, 20)
|
11
|
+
@priort.options = %w{Zero Low Medium High Immediate}
|
12
|
+
@type.options = %w{Incident Code_Defect Build_Problem Documentation Feature_Request}
|
13
|
+
@widgets = [@title, @priort, @type, @desc, @submit, @cancel]
|
14
|
+
@title.active = true
|
15
|
+
@active_id = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def display
|
19
|
+
@screen.clear
|
20
|
+
@screen.setpos(0,0)
|
21
|
+
@screen.attron(Curses.color_pair(1));
|
22
|
+
@screen.addstr("Issue Creation")
|
23
|
+
@screen.attroff(Curses.color_pair(1));
|
24
|
+
|
25
|
+
|
26
|
+
@screen.setpos(1,0)
|
27
|
+
@screen.addstr("Title:")
|
28
|
+
@screen.setpos(2,0)
|
29
|
+
@screen.addstr("Priority:")
|
30
|
+
@screen.setpos(3,0)
|
31
|
+
@screen.addstr("Type:")
|
32
|
+
@screen.setpos(4,0)
|
33
|
+
@screen.addstr("Description:")
|
34
|
+
|
35
|
+
ln = 6
|
36
|
+
@widgets.each_with_index do |w, idx|
|
37
|
+
if(idx != @active_id)
|
38
|
+
w.draw
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
#Draw the active widget last as it might have an overlay
|
43
|
+
@widgets[@active_id].draw
|
44
|
+
end
|
45
|
+
|
46
|
+
def changeActive(a)
|
47
|
+
a = [a,0].max
|
48
|
+
a = [a,@widgets.length-1].min
|
49
|
+
|
50
|
+
if(@active_id != a)
|
51
|
+
@widgets[@active_id].active = false
|
52
|
+
@active_id = a
|
53
|
+
@widgets[@active_id].active = true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def interact
|
58
|
+
while true
|
59
|
+
result = true
|
60
|
+
c = Curses.getch
|
61
|
+
case c
|
62
|
+
when 27
|
63
|
+
break
|
64
|
+
when ?\t, 9
|
65
|
+
changeActive (@active_id + 1) % @widgets.length
|
66
|
+
display
|
67
|
+
when ?e
|
68
|
+
edit
|
69
|
+
display
|
70
|
+
else
|
71
|
+
if(!@widgets[@active_id].handle(c))
|
72
|
+
if(c == Curses::KEY_UP)
|
73
|
+
changeActive @active_id - 1
|
74
|
+
display
|
75
|
+
elsif(c == Curses::KEY_DOWN)
|
76
|
+
changeActive @active_id + 1
|
77
|
+
display
|
78
|
+
else
|
79
|
+
@screen.setpos(0,0)
|
80
|
+
@screen.addstr("[unknown key `#{Curses.keyname(c)}'=#{c}] ")
|
81
|
+
end
|
82
|
+
else
|
83
|
+
if((c == "\n" || c == 13) && @submit == @widgets[@active_id])
|
84
|
+
`fossil ticket add title '#{@title.value}' priority '#{@priort.value}' type '#{@type.value}' icomment '#{@desc.value}' status Open`
|
85
|
+
return :do_refresh
|
86
|
+
elsif((c == "\n" || c == 13) && @cancel == @widgets[@active_id])
|
87
|
+
break
|
88
|
+
else
|
89
|
+
display
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
@screen.setpos(0,0)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def edit
|
98
|
+
tmp = File.open("fs-ticket-edit.yaml", "w+")
|
99
|
+
tmp.puts({"title" => @title.value,
|
100
|
+
"priority" => @priort.value,
|
101
|
+
"type" => @type.value,
|
102
|
+
"description" => @desc.value}.to_yaml)
|
103
|
+
tmp.close
|
104
|
+
|
105
|
+
system("vim fs-ticket-edit.yaml")
|
106
|
+
|
107
|
+
#Restore curses options
|
108
|
+
Curses.raw
|
109
|
+
Curses.nonl
|
110
|
+
#Curses.cbreak
|
111
|
+
Curses.noecho
|
112
|
+
Curses.curs_set(0)
|
113
|
+
@screen.scrollok(true)
|
114
|
+
@screen.keypad(true)
|
115
|
+
|
116
|
+
begin
|
117
|
+
tmp = File.open("fs-ticket-edit.yaml", "r")
|
118
|
+
yy = YAML.load(tmp.read)
|
119
|
+
tmp.close
|
120
|
+
@title.value = yy["title"]
|
121
|
+
@priort.value = yy["priority"]
|
122
|
+
@type.value = yy["type"]
|
123
|
+
@desc.value = yy["description"]
|
124
|
+
|
125
|
+
#Later these editing errors should be handled correctly
|
126
|
+
#rescue Exception=>e
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
class ReportView
|
2
|
+
def initialize(screen, tickets, block)
|
3
|
+
@screen = screen
|
4
|
+
@mode = :all
|
5
|
+
@real_tickets = tickets
|
6
|
+
@tickets = tickets
|
7
|
+
@refresh = block
|
8
|
+
@highlight = 0
|
9
|
+
display_lines
|
10
|
+
end
|
11
|
+
|
12
|
+
def refresh
|
13
|
+
@real_tickets = @refresh.call
|
14
|
+
toggle_open_tickets
|
15
|
+
toggle_open_tickets
|
16
|
+
end
|
17
|
+
|
18
|
+
def toggle_open_tickets
|
19
|
+
@highlight = 0
|
20
|
+
if(@mode == :all)
|
21
|
+
@tickets = []
|
22
|
+
@real_tickets.each do |t|
|
23
|
+
if(t.status == "Open")
|
24
|
+
@tickets << t
|
25
|
+
end
|
26
|
+
end
|
27
|
+
@mode = :open
|
28
|
+
elsif(@mode == :open || @mode == :search)
|
29
|
+
@tickets = @real_tickets
|
30
|
+
@mode = :all
|
31
|
+
end
|
32
|
+
display_lines
|
33
|
+
end
|
34
|
+
|
35
|
+
def run_search(term)
|
36
|
+
@highlight = 0
|
37
|
+
@tickets = []
|
38
|
+
@real_tickets.each do |t|
|
39
|
+
if(t.match term)
|
40
|
+
@tickets << t
|
41
|
+
end
|
42
|
+
end
|
43
|
+
@mode = :search
|
44
|
+
display_lines
|
45
|
+
end
|
46
|
+
|
47
|
+
def search
|
48
|
+
label = TextLineWidget.new(@screen, "search: ", 0, @screen.maxy-1, 40)
|
49
|
+
line = TextLineWidget.new(@screen, "", 10, @screen.maxy-1, 40)
|
50
|
+
label.draw
|
51
|
+
line.draw
|
52
|
+
while true
|
53
|
+
c = Curses.getch
|
54
|
+
if(c == "\n" || !line.handle(c))
|
55
|
+
run_search(line.value)
|
56
|
+
break
|
57
|
+
else
|
58
|
+
label.draw
|
59
|
+
line.draw
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def display_lines
|
65
|
+
@screen.clear
|
66
|
+
|
67
|
+
@screen.setpos(0,0)
|
68
|
+
@screen.attron(Curses.color_pair(1));
|
69
|
+
@screen.addstr("Issue Overview - [a]dd new issue, [t]oggle open issues, [q]uit")
|
70
|
+
@screen.attroff(Curses.color_pair(1));
|
71
|
+
|
72
|
+
start = 0
|
73
|
+
ylimit = @screen.maxy-1
|
74
|
+
upper = ylimit-1
|
75
|
+
if(@highlight >= ylimit)
|
76
|
+
start = ylimit*(@highlight/ylimit).to_i
|
77
|
+
upper = start + ylimit - 1
|
78
|
+
end
|
79
|
+
|
80
|
+
if(@tickets.empty?)
|
81
|
+
return
|
82
|
+
end
|
83
|
+
|
84
|
+
@tickets[start..upper].each_with_index{|tick, idx|
|
85
|
+
line = tick.title
|
86
|
+
line_id = idx+start
|
87
|
+
@screen.setpos(idx+1, 0)
|
88
|
+
@screen.addstr(line_id.to_s+".")
|
89
|
+
@screen.setpos(idx+1, 4)
|
90
|
+
if(tick.status)
|
91
|
+
@screen.addstr("["+tick.status[0]+"]")
|
92
|
+
end
|
93
|
+
@screen.setpos(idx+1, 9)
|
94
|
+
if(@highlight == line_id)
|
95
|
+
@screen.attron Curses::A_BOLD
|
96
|
+
@screen.addstr(line)
|
97
|
+
@screen.attroff Curses::A_BOLD
|
98
|
+
else
|
99
|
+
@screen.addstr(line)
|
100
|
+
end
|
101
|
+
}
|
102
|
+
@screen.setpos(0,0)
|
103
|
+
@screen.refresh
|
104
|
+
end
|
105
|
+
|
106
|
+
def scroll_up(draw=true)
|
107
|
+
if(@highlight > 0)
|
108
|
+
@highlight -= 1
|
109
|
+
display_lines if draw
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def scroll_down(draw=true)
|
114
|
+
if(@highlight < @tickets.length-1)
|
115
|
+
@highlight += 1
|
116
|
+
display_lines if draw
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def interact
|
121
|
+
while true
|
122
|
+
result = true
|
123
|
+
c = Curses.getch
|
124
|
+
case c
|
125
|
+
when ?t
|
126
|
+
toggle_open_tickets
|
127
|
+
when ?a
|
128
|
+
at = AddTicketView.new(@screen)
|
129
|
+
@screen.clear
|
130
|
+
at.display
|
131
|
+
ret = at.interact
|
132
|
+
if(ret == :do_refresh)
|
133
|
+
refresh
|
134
|
+
end
|
135
|
+
@screen.clear
|
136
|
+
display_lines
|
137
|
+
when Curses::KEY_DOWN, Curses::KEY_CTRL_N, ?j
|
138
|
+
result = scroll_down
|
139
|
+
when Curses::KEY_UP, Curses::KEY_CTRL_P, ?k
|
140
|
+
result = scroll_up
|
141
|
+
when Curses::KEY_NPAGE, ?\s # white space
|
142
|
+
for i in 0..(@screen.maxy - 2)
|
143
|
+
scroll_down(false)
|
144
|
+
end
|
145
|
+
display_lines
|
146
|
+
when Curses::KEY_PPAGE
|
147
|
+
for i in 0..(@screen.maxy - 2)
|
148
|
+
scroll_up(false)
|
149
|
+
end
|
150
|
+
display_lines
|
151
|
+
when Curses::KEY_RIGHT, Curses::KEY_ENTER, ?\n, 13
|
152
|
+
tv = TicketView.new(@screen, @tickets[@highlight])
|
153
|
+
@screen.clear
|
154
|
+
tv.display
|
155
|
+
ret = tv.interact
|
156
|
+
@screen.clear
|
157
|
+
display_lines
|
158
|
+
when "/"
|
159
|
+
search
|
160
|
+
when ?q
|
161
|
+
break
|
162
|
+
else
|
163
|
+
@screen.setpos(0,0)
|
164
|
+
@screen.addstr("[unknown key `#{Curses.keyname(c)}'=#{c}] ")
|
165
|
+
end
|
166
|
+
@screen.setpos(0,0)
|
167
|
+
end
|
168
|
+
Curses.close_screen
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class TicketView
|
4
|
+
def initialize(screen, ticket)
|
5
|
+
@screen = screen
|
6
|
+
@ticket = ticket
|
7
|
+
end
|
8
|
+
|
9
|
+
def display
|
10
|
+
@screen.clear
|
11
|
+
@screen.setpos(0,0)
|
12
|
+
@screen.attron(Curses.color_pair(1));
|
13
|
+
@screen.addstr("Issue Details - [f]ix [q]uit")
|
14
|
+
@screen.attroff(Curses.color_pair(1));
|
15
|
+
|
16
|
+
|
17
|
+
@screen.setpos(1,0)
|
18
|
+
@screen.addstr("Title: #{@ticket.title}")
|
19
|
+
@screen.setpos(2,0)
|
20
|
+
@screen.addstr("Status: #{@ticket.status}")
|
21
|
+
@screen.setpos(3,0)
|
22
|
+
@screen.addstr("Priority: #{@ticket.priority}")
|
23
|
+
@screen.setpos(4,0)
|
24
|
+
@screen.addstr("Type: #{@ticket.type}")
|
25
|
+
@screen.setpos(5,0)
|
26
|
+
@screen.addstr("Subsystem: #{@ticket.subsystem}")
|
27
|
+
@screen.setpos(6,0)
|
28
|
+
@screen.addstr("Comments:")
|
29
|
+
ln = 7
|
30
|
+
@ticket.comments.split("\n").each do |l|
|
31
|
+
@screen.setpos(ln,0)
|
32
|
+
@screen.addstr(l)
|
33
|
+
ln += 1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def interact
|
38
|
+
while true
|
39
|
+
result = true
|
40
|
+
c = Curses.getch
|
41
|
+
case c
|
42
|
+
when Curses::KEY_LEFT
|
43
|
+
break
|
44
|
+
when ?q
|
45
|
+
break
|
46
|
+
when ?f
|
47
|
+
@ticket.resolve
|
48
|
+
display
|
49
|
+
when ?e
|
50
|
+
edit
|
51
|
+
display
|
52
|
+
else
|
53
|
+
@screen.setpos(0,0)
|
54
|
+
@screen.addstr("[unknown key `#{Curses.keyname(c)}'=#{c}] ")
|
55
|
+
end
|
56
|
+
@screen.setpos(0,0)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def edit
|
61
|
+
tmp = File.open("fs-ticket-edit.txt", "w+")
|
62
|
+
tmp.puts({"title" => @ticket.title,
|
63
|
+
"status" => @ticket.status,
|
64
|
+
"priority" => @ticket.priority,
|
65
|
+
"type" => @ticket.type,
|
66
|
+
"comment" => @ticket.comments}.to_yaml)
|
67
|
+
tmp.close
|
68
|
+
|
69
|
+
system("vim fs-ticket-edit.txt")
|
70
|
+
|
71
|
+
#Restore curses options
|
72
|
+
Curses.raw
|
73
|
+
Curses.nonl
|
74
|
+
#Curses.cbreak
|
75
|
+
Curses.noecho
|
76
|
+
Curses.curs_set(0)
|
77
|
+
@screen.scrollok(true)
|
78
|
+
@screen.keypad(true)
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
class ButtonWidget
|
3
|
+
attr_accessor :active
|
4
|
+
def initialize(screen, value, x, y)
|
5
|
+
@active = false
|
6
|
+
@screen = screen
|
7
|
+
@value = value
|
8
|
+
@x = x
|
9
|
+
@y = y
|
10
|
+
end
|
11
|
+
|
12
|
+
def handle(chr)
|
13
|
+
if(chr == "\n" || chr == 13)
|
14
|
+
true
|
15
|
+
else
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def draw
|
21
|
+
value = @value
|
22
|
+
if(value.empty?)
|
23
|
+
value = "XXXXX"
|
24
|
+
end
|
25
|
+
|
26
|
+
@screen.attron Curses::A_BOLD if @active
|
27
|
+
|
28
|
+
@screen.setpos(@y,@x)
|
29
|
+
@screen.addstr "-"*(2+value.length)
|
30
|
+
@screen.setpos(@y+1,@x)
|
31
|
+
@screen.addstr("|"+value+"|")
|
32
|
+
@screen.setpos(@y+2,@x)
|
33
|
+
@screen.addstr "-"*(2+value.length)
|
34
|
+
|
35
|
+
@screen.attroff Curses::A_BOLD if @active
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
class DropdownWidget
|
2
|
+
attr_reader :active
|
3
|
+
attr_reader :options
|
4
|
+
attr_reader :value
|
5
|
+
|
6
|
+
def active=(a)
|
7
|
+
if(!a)
|
8
|
+
@mode = :normal
|
9
|
+
end
|
10
|
+
@active = a
|
11
|
+
end
|
12
|
+
|
13
|
+
def options=(o)
|
14
|
+
@options = o
|
15
|
+
fix_sel
|
16
|
+
end
|
17
|
+
|
18
|
+
def value=(v)
|
19
|
+
@value = v
|
20
|
+
fix_sel
|
21
|
+
end
|
22
|
+
|
23
|
+
#Fix the selection when either the value or options change
|
24
|
+
def fix_sel
|
25
|
+
@options.each_with_index do |o, idx|
|
26
|
+
if(@value == o)
|
27
|
+
@sel = idx
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(screen, value, x, y, w)
|
33
|
+
@active = false
|
34
|
+
@screen = screen
|
35
|
+
@value = value
|
36
|
+
@x = x
|
37
|
+
@y = y
|
38
|
+
@w = w
|
39
|
+
@mode = :normal
|
40
|
+
@options = [value]
|
41
|
+
@sel = 0
|
42
|
+
end
|
43
|
+
|
44
|
+
def draw
|
45
|
+
if(@mode == :normal)
|
46
|
+
@screen.setpos(@y,@x)
|
47
|
+
@screen.attron Curses::A_BOLD if @active
|
48
|
+
@screen.addstr(@value)
|
49
|
+
@screen.attroff Curses::A_BOLD if @active
|
50
|
+
elsif(@mode == :selecting)
|
51
|
+
sel_width = @w-2
|
52
|
+
@screen.setpos(@y,@x)
|
53
|
+
@screen.addstr("-"*@w)
|
54
|
+
|
55
|
+
@options.each_with_index do |opt, idx|
|
56
|
+
@screen.setpos(@y+idx+1,@x)
|
57
|
+
padded_str = " "*sel_width
|
58
|
+
opt.chars.each_with_index do |chr, idx|
|
59
|
+
padded_str[idx] = chr
|
60
|
+
end
|
61
|
+
@screen.attron Curses::A_BOLD if idx == @sel
|
62
|
+
@screen.addstr "|"+padded_str+"|"
|
63
|
+
@screen.attroff Curses::A_BOLD if idx == @sel
|
64
|
+
end
|
65
|
+
|
66
|
+
@screen.setpos(@y+@options.length+1,@x)
|
67
|
+
@screen.addstr("-"*@w)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def handle(chr)
|
72
|
+
if(chr == Curses::KEY_RIGHT)
|
73
|
+
@mode = :selecting
|
74
|
+
@value = @options[@sel]
|
75
|
+
true
|
76
|
+
elsif(chr == Curses::KEY_LEFT)
|
77
|
+
@mode = :normal
|
78
|
+
true
|
79
|
+
elsif(chr == Curses::KEY_DOWN && @mode == :selecting)
|
80
|
+
@sel = [@sel+1, @options.length-1].min
|
81
|
+
@value = @options[@sel]
|
82
|
+
true
|
83
|
+
elsif(chr == Curses::KEY_UP && @mode == :selecting)
|
84
|
+
@sel = [@sel-1, 0].max
|
85
|
+
@value = @options[@sel]
|
86
|
+
true
|
87
|
+
else
|
88
|
+
false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class TextFieldWidget
|
2
|
+
attr_accessor :active
|
3
|
+
attr_accessor :value
|
4
|
+
def initialize(screen, value, x, y, w, h)
|
5
|
+
@active = false
|
6
|
+
@screen = screen
|
7
|
+
@value = value
|
8
|
+
@x = x
|
9
|
+
@y = y
|
10
|
+
@w = w
|
11
|
+
@h = h
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle(chr)
|
15
|
+
if(chr.class == String && chr.match(/[[:print:]\n]/))
|
16
|
+
@value += chr
|
17
|
+
true
|
18
|
+
elsif(chr == Curses::KEY_ENTER || chr == 13)
|
19
|
+
@value += "\n"
|
20
|
+
true
|
21
|
+
elsif(chr == Curses::KEY_BACKSPACE || chr == 127)
|
22
|
+
@value = @value[0..-2]
|
23
|
+
true
|
24
|
+
else
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def draw
|
30
|
+
value = @value
|
31
|
+
if(value.empty?)
|
32
|
+
(0..@h).each do |x|
|
33
|
+
value += "_"*@w+"\n"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
@screen.setpos(@y,@x)
|
38
|
+
@screen.attron Curses::A_BOLD if @active
|
39
|
+
@screen.addstr(value)
|
40
|
+
@screen.attroff Curses::A_BOLD if @active
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class TextLineWidget
|
2
|
+
attr_accessor :active
|
3
|
+
attr_accessor :value
|
4
|
+
def initialize(screen, value, x, y, w)
|
5
|
+
@active = false
|
6
|
+
@screen = screen
|
7
|
+
@value = value
|
8
|
+
@x = x
|
9
|
+
@y = y
|
10
|
+
@w = w
|
11
|
+
end
|
12
|
+
|
13
|
+
def handle(chr)
|
14
|
+
if(chr.class == String && chr.match(/[a-zA-Z\- .]/))
|
15
|
+
@value += chr
|
16
|
+
true
|
17
|
+
elsif(chr == Curses::KEY_BACKSPACE || chr == 127)
|
18
|
+
@value = @value[0..-2]
|
19
|
+
true
|
20
|
+
else
|
21
|
+
false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def draw
|
26
|
+
value = @value
|
27
|
+
if(value.empty?)
|
28
|
+
value = "_________________"
|
29
|
+
end
|
30
|
+
|
31
|
+
@screen.setpos(@y,@x)
|
32
|
+
@screen.attron Curses::A_BOLD if @active
|
33
|
+
@screen.addstr(value)
|
34
|
+
@screen.attroff Curses::A_BOLD if @active
|
35
|
+
end
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fs-ticket
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mark McCurry
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-12-02 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.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sqlite3
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
description: |
|
42
|
+
fs-ticket is a command line interface to issues/tickets stored in a fossil
|
43
|
+
repository with a design influienced by mutt.
|
44
|
+
This tool supports basic operations such as adding, editing, searching, and
|
45
|
+
viewing tickets in the terminal.
|
46
|
+
email: mark.d.mccurry@gmail.com
|
47
|
+
executables:
|
48
|
+
- fs-ticket
|
49
|
+
extensions: []
|
50
|
+
extra_rdoc_files: []
|
51
|
+
files:
|
52
|
+
- LICENSE.txt
|
53
|
+
- bin/fs-ticket
|
54
|
+
- lib/fs-ticket.rb
|
55
|
+
- lib/render.rb
|
56
|
+
- lib/ticket.rb
|
57
|
+
- lib/view/add-ticket-view.rb
|
58
|
+
- lib/view/report-view.rb
|
59
|
+
- lib/view/ticket-view.rb
|
60
|
+
- lib/widget/button-widget.rb
|
61
|
+
- lib/widget/dropdown-widget.rb
|
62
|
+
- lib/widget/text-field-widget.rb
|
63
|
+
- lib/widget/text-line-widget.rb
|
64
|
+
homepage: http://rubygems.org/gems/fs-ticket
|
65
|
+
licenses:
|
66
|
+
- MIT
|
67
|
+
metadata: {}
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 2.4.5.1
|
85
|
+
signing_key:
|
86
|
+
specification_version: 4
|
87
|
+
summary: Ncurses interface to tickets stored in a fossil-scm repo
|
88
|
+
test_files: []
|
89
|
+
has_rdoc:
|