From 846390d015aaf3fde0f3717b109741dc787c5669 Mon Sep 17 00:00:00 2001 From: sloum Date: Thu, 25 Mar 2021 15:53:22 -0700 Subject: [PATCH] Adds license, fixes bugs, adds features --- LICENSE | 259 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 5 ++ board.go | 75 +++++++++++++--- lane.go | 17 +++- main.go | 67 ++++++++++---- story.go | 21 ++++- 6 files changed, 414 insertions(+), 30 deletions(-) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1f4664b --- /dev/null +++ b/LICENSE @@ -0,0 +1,259 @@ +swim is (c) 2021 sloum , All Rights Reserved +swim is made available under the following terms: + + Floodgap Free Software License + + The author of your software has chosen to distribute it under the + Floodgap Free Software License. Although this software is without cost, + it is not released under Copyleft or GPL, and there are differences + which you should read. Your use of this software package constitutes + your binding acceptance without restriction. + + This software is without cost + + The Floodgap Free Software License (FFSL) has one overriding mandate: + that software using it, or derivative works based on software that uses + it, must be free. By free we mean simply "free as in beer" -- you may + put your work into open or closed source packages as you see fit, + whether or not you choose to release your changes or updates publicly, + but you must not ask any fee for it. (There are certain exceptions for + for-profit use which we will discuss below.) + + Definitions and terms + + Author + The declared copyright owner of this software package. + + Binary + A pre-compiled or pre-interpreted bytecode or machine language + representation of a software package not designed for further + modification and tied to a particular platform or architecture. + + Derivative work + Any distribution (q.v.) that contains any modification to or + deviation from the official reference distribution (q.v.); or + any software package significantly based on or integrally + including the source code for its features, including but not + limited to supersets; subsets of a significant proportion; + in-place patched changes to source or binary files; linking in + as a library; binary-only distributions if the original package + included source (even if the source was not modified prior to + compilation); or translations to another programming language, + architecture or operating system environment. Derivative works + of packages released under this license are also considered + subject to this license. + + However, a software package that requires this package but does + not include it or is not based upon it, even if it will not + operate without it, is not considered a derivative work. For + example, interpreted programs requiring an interpreter issued + under this license, assuming they are not distributed with any + portion of the interpreter, are not derivative works. + + Distribution + A packaged release of this software, either the author's + original work (the "reference distribution") or a derivative + work based upon it. + + Reference distribution + A packaged release of this software explicitly designated as the + official release, written by or on behalf of the Author with his + or her explicit designation as official. Only exact copies of + the reference distribution may be called reference + distributions; all other forms are derivative works. + + Source code + The human-readable programming instructions of the package which + might be easily read as text and subsequently edited, but + requiring compilation or interpretation into binary before being + directly useable. + + What you are permitted to do under this license + + Pursuant to the remainder of the terms below, + * You may freely use, copy, and disseminate this software package for + any non-commercial purpose as well as the commercial purposes + permitted below. + * You may freely modify this package, including source code if + available. Your modifications need not be released, although you + are encouraged to do so. + * You may release your derivative works based upon this software in + purely binary (non-source) form if you choose. You are not + obligated to release any portion of your source code openly, + although you are encouraged to do so. + * If this package is a tool used for generation, compilation or + maintenance of works, including but not limited to readable + documents, software packages or images (for example, compilers, + interpreters, translators, linkers, editors, assemblers or + typesetters), you may freely use it for that purpose, commercial or + otherwise, as the works made by this package are not considered + subject to this license unless specified otherwise within and may + be distributed under any desired license and/or offered for sale or + rental. Any run-time library or run-time code section linked into + the output by a compiler or similar code-generating tool governed + by this license is considered to be an integral part of the output, + and its presence does not subject the generated work to this + license either. (This is, of course, assuming you are not using + said tools to generate a derivative work based on this package in + violation of the other license terms.) + However, if you are linking or including a separately distributed + library that is under this license, no matter what tool you are + using to do the linking or inclusion, you are then considered to be + making a derivative work based on that library and your work does + fall under this license. To avoid this, do not include the library + with your work (even though it needs the library to function) and + instead offer the library separately without cost. + * In addition to non-commercial use and the uses permitted above, you + may use this software package in any for-profit endeavour as long + as it does not involve the specific sale or rental of this package. + Some specific but by no means exhaustive examples are listed below. + Note that some of these situations may require additional action be + taken to ensure compliance. + + If this package or a derivative work allows you to serve data + or make data available to others (for example, web servers, + mail servers, gopher servers, etc.), you may use it to serve + any commercial content or in any commercial setting whether + you choose to charge a fee or not, as you are considered to be + earning income from the content you serve and/or the services + facilitated by your business and not from the sale of this + package itself. (This is, of course, assuming that you are not + charging a fee for sale or rental of this package or a + derivative work based on this package in violation of the + other license terms.) Similarly, any data you may acquire from + the use of this package is yours, and not governed by this + license in any way even if for-profit. + + If you are selling a product that includes this package or a + derivative work either as part of your product's requirements + for function or as a bundled extra, such as an operating + system distribution, you may charge a fee for your product as + long as you also make this package or said derivative work + available for free separately (such as by download or link + back to this package's site), as you are considered to be + requesting a fee for your own product and the package is + merely included as a convenience to your users. + + If you offer installation of this package or a derivative work + as a service, you may charge a fee for the act of installation + as long as you also make this package or said derivative work + available for free (such as by download or link back to this + package's site), as you are considered to be requesting a fee + for the act of installation and not for the software you are + installing. + + The Author may also grant, in writing, other specified + exemptions for your particular commercial purpose that do not + contravene the spirit of this license or any license terms + this package additionally carries. + * In your derivative works based on this package, you may choose to + offer warranty support or guarantees of performance. This does not + in any way make the original Author legally, financially or in any + other respect liable for claims issued under your warranty or + guarantee, and you are solely responsible for the fulfillment of + your terms even if the Author of the work you have based your work + upon offers his or her own. + * In your derivative works based on this package, you may further + restrict the acceptable uses of your package or situations in which + it may be employed as long as you clearly state that your terms + apply only to your derivative work and not to the original + reference distribution. However, you may not countermand or ignore, + directly or otherwise, any restriction already made in the + reference distribution's license, including in this document + itself, in similar fashion to other licenses allowing compatible + licenses to co-govern a particular package's use. + + What you must not do under this license + + Remember that these limits apply only to redistribution of a reference + distribution, or to a true derivative work. If your project does not + include this package or code based upon it, even if it requires this + package to function, it is not considered subject to this license or + these restrictions. + * You must not charge a fee for purchase or rental of this package or + any derivative work based on this package. It is still possible to + use this package in a commercial environment, however -- see What + you are permitted to do under this license. + * You must not countermand or ignore, directly or otherwise, the + restrictions already extant in this package's license in your + derivative work based on it. As a corollary, you must not place + your derivative work under a secondary license or description of + terms that conflicts with it (for example, this license is not + compatible with the GNU Public License). + * You must not label any modified distribution of this package as a + reference or otherwise official distribution without the permission + of the original Author or Authors. You must clearly specify that + your modified work is a derivative work, including binary-only + releases if the original included source code and you do not even + if you did not modify the source prior to compilation. + + What you must do under this license + + * You must agree to all terms specified (agreement to which is + unconditionally signified by your usage, modification or + repurposing of this package), or to remove the package from your + computer and not use it further. + * In the absence of any specific offer for redress or assistance + under warranty or guarantee of performance that the Author of this + package might make, you must agree to accept any and all liability + that may come from the use of this package, proper or improper, + real or imagined, and certify without condition that you use this + product at your own risk with no guarantee of function, + merchantability or fitness for a particular purpose. If such offer + of redress or assistance is extended, it is fulfillable only by the + Author who extended the offer, which might not necessarily be this + Author, nor might it be the Authors of any packages it might be + based upon. + * If you choose to publicly redistribute this package or create a + derivative work based on this package, you must make it available + without any purchase or rental fee of any kind. + * If you choose to create a derivative work based on this package, + your derivative work must be copyrighted, and must be governed + under (at a minimum) the original package's license, which will + necessarily include all terms noted here. As such, if you choose to + distribute your derivative work, you must include a human-readable + license in your distribution containing all restrictions of use, + necessarily including this license, and any additional restrictions + the Author has mandated that do not contravene this license which + you and users of your derivative work must also honour. + * If you choose to create and distribute a derivative work based on + this package, your derivative work must clearly make reference to + this package, any other packages your work or the original work + might be based on, and all applicable copyrights, either in your + documentation, your work's standard human-readable output, or both. + A suggested method might be + + Contains or is based on the Foo software package. + Copyright (C) 2112 D. Original Author. All rights reserved. + http://their.web.site.invalid/ + + Additional notes + + Enforcement is the responsibility of the Author. However, violation of + this license may subject you to criminal and civil penalties depending + on your country. + + This package is bound by the version of license that accompanies it. + Future official versions of a particular package may use a more updated + license, and you should always review the license before use. This + license's most current version is always available from the following + locations: + + [1]http://www.floodgap.com/software/ffsl/ + [2]gopher://gopher.floodgap.com/1/ffsl/ + + This license is version 1, dated 19 November 2006. + + This license is copyright � 2006 Cameron Kaiser. All rights reserved. + The text of this license is available for re-use and re-distribution + under the Creative Commons. The use of the term "Floodgap Free Software + License" does not imply endorsement of packages using this license by + Floodgap Systems or by Cameron Kaiser. Modified licenses using portions + of these terms may refer to themselves as modified FFSL, with the + proviso that their modifications be clearly marked in accordance with + the Creative Commons Attribution-ShareAlike 2.5 License. + + Only the text of this license, and not programs covered by this + license, is so offered under Creative Commons. + +References + + 1. http://www.floodgap.com/software/ffsl/ + 2. gopher://gopher.floodgap.com/1/ffsl/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..04efa81 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# swim - project board software + +swim is a project/task management helper that organizes projects into "swim lanes". The lanes can contain "stories" that represent a subproject. These subprojects can have titles, descriptions, assigned users, a point representation of their difficulty, a task list, and comments. Stories can be moved around within and between lanes to represent their current state in the project. A common layout for simple projects is to have three lanes: backlog, active, complete; but many other working models are possible. + + diff --git a/board.go b/board.go index d947f7a..0b00bd8 100644 --- a/board.go +++ b/board.go @@ -7,6 +7,7 @@ import ( "reflect" "strings" "tildegit.org/sloum/swim/termios" + "time" ) const ( @@ -18,22 +19,41 @@ const ( ) type Board struct { - Title string `json:"BoardTitle"` - Lanes []Lane `json:"Lanes"` - Current int `json:"CurrentLane"` // Index of current lane - laneOff int - message string - msgErr bool - width int - height int - Zoom int `json:"Zoom"` + Title string `json:"BoardTitle"` + Lanes []Lane `json:"Lanes"` + Current int `json:"CurrentLane"` // Index of current lane + laneOff int + message string + msgErr bool + width int + height int + Zoom int `json:"Zoom"` + StoryOpen bool } +func (b *Board) PollForTermSize() { + for { + var w, h = termios.GetWindowSize() + if h != b.height || w != b.width { + b.height = h + b.width = w + if b.StoryOpen { + b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].offset = 0 + b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].Draw(b) + } else { + b.Draw() + } + } + + time.Sleep(500 * time.Millisecond) + } +} func (b *Board) Run() { defer termios.Restore() termios.SetCharMode() fmt.Print("\033[?25l") + go b.PollForTermSize() var ch rune for { @@ -48,7 +68,9 @@ func (b *Board) Run() { case 'n': b.CreateStory() case '\n': + b.StoryOpen = true b.ViewStory() + b.StoryOpen = false case 'h', 'j', 'k', 'l', 'H', 'L', 'K', 'J': b.ClearMessage() b.Move(ch) @@ -183,6 +205,8 @@ func (b *Board) EnterCommand() { if len(f) > 2 { target := strings.ToLower(f[1]) switch target { + case "board", "b", "bo", "boa", "boar": + b.Update(f[2:]) case "lane", "l", "la", "lan": b.Lanes[b.Current].Update(f[2:], b) case "story", "s", "st", "sto", "stor": @@ -200,6 +224,21 @@ func (b *Board) EnterCommand() { } } +func (b *Board) Update(args []string) { + location := strings.ToLower(args[0]) + switch location { + case "title", "t", "name", "n": + title, err := GetAndConfirmCommandLine("New board title: ") + if err != nil { + b.SetMessage(err.Error(), true) + break + } + b.Title = title + b.SetMessage("Board title updated", false) + } + +} + func (b Board) PrintMessage() string { var out strings.Builder if b.msgErr { @@ -218,12 +257,21 @@ func (b Board) PrintMessage() string { func (b *Board) ZoomIn() { if b.Zoom > 1 { b.Zoom -= 1 + if b.Current + 1 > b.laneOff + b.Zoom { + b.laneOff += 1 + } } } func (b *Board) ZoomOut() { if b.width / (b.Zoom+1) > 15 { b.Zoom += 1 + if len(b.Lanes) < b.laneOff + b.Zoom { + b.laneOff -= 1 + if b.laneOff < 0 { + b.laneOff = 0 + } + } } } @@ -344,7 +392,6 @@ func (b *Board) ViewStory() { } else { b.SetMessage("There is no story to view", true) } - } func (b Board) Draw() { @@ -372,7 +419,13 @@ func (b *Board) Write(com []string) { } else { path = ExpandedAbsFilepath(strings.Join(com[1:], " ")) } - f, err := os.Create(path) + + var perms os.FileMode = 0664 + fstats, err := os.Stat(path) + if err == nil { + perms = fstats.Mode() + } + f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perms) if err != nil { b.SetMessage(err.Error(), true) return diff --git a/lane.go b/lane.go index 0860adb..4310310 100644 --- a/lane.go +++ b/lane.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strconv" "strings" ) @@ -24,6 +25,16 @@ func (l *Lane) CreateStory(b *Board) { } } +// Zeroes out the offset and current values +func (l *Lane) ResetLoadPositions() { + if len(l.Stories) > 0 { + l.Current = 0 + } else { + l.Current = -1 + } + l.storyOff = 0 +} + func (l Lane) Header(width int, selected bool) string { marker := " " color := style.Lane @@ -50,10 +61,14 @@ func (l Lane) StringSlice(width int, selected bool) []string { } out = append(out, fmt.Sprintf("%s%*.*s%s", style.Lane, width, width, " ", styleOff)) title := l.Stories[i].Title + pts := strconv.Itoa(l.Stories[i].Points) + if pts == "0" || pts[0] == '-' { + pts = "" + } if len(title) > width-4 { title = title[:width-5] + "…" } - out = append(out, fmt.Sprintf("%s %s%s%-*.*s%s %s", style.Lane, style.Input, leadIn, width-4, width-4, title, style.Lane, styleOff)) + out = append(out, fmt.Sprintf("%s %s%s%-*.*s\033[7m%*s\033[27m%s %s", style.Lane, style.Input, leadIn, width-7, width-7, title, 3, pts, style.Lane, styleOff)) } if len(out) > 0 { out = append(out, fmt.Sprintf("%s%*.*s%s", style.Lane, width, width, " ", styleOff)) diff --git a/main.go b/main.go index 57043fe..895b413 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,23 @@ package main +// swim is a project management board for the terminal of unix or +// unix-like systems. +// +// Copyright (C) 2021 Brian Evans, All Rights Reserved +// +// This program is free, as in beer, software: you can redistribute it +// and/or modify it under the terms of the Floodgap Free Software license. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// Floodgap Free Software License for more details. +// +// You should have received a copy of the Floodgap Free Software License +// along with this program. If not, see: +// 1. http://www.floodgap.com/software/ffsl/ +// 2. gopher://gopher.floodgap.com/1/ffsl/ + import ( "bufio" "encoding/json" @@ -127,13 +145,13 @@ func Quit() { } func LoadFile(path string, cols, rows int) { - p := ExpandedAbsFilepath(path) - bytes, err := ioutil.ReadFile(p) + fp = ExpandedAbsFilepath(path) + bytes, err := ioutil.ReadFile(fp) if err != nil { - fmt.Fprintf(os.Stderr, "Could not open file %q\n", path) - os.Exit(1) + board = DefaultBoard(cols, rows) + board.SetMessage("Could not load file at path, new file created", false) + return } - fp = p err = json.Unmarshal(bytes, &board) if err != nil { fmt.Fprintf(os.Stderr, "Could not understand input file:\n%s\n", err.Error()) @@ -141,7 +159,19 @@ func LoadFile(path string, cols, rows int) { } board.width = cols board.height = rows - board.message = fmt.Sprintf("Loaded file: %s", path) + board.laneOff = 0 + board.Current = -1 + board.StoryOpen = false + if len(board.Lanes) > 0 { + board.Current = 0 + } + for i := range board.Lanes { + board.Lanes[i].ResetLoadPositions() + for s := range board.Lanes[i].Stories { + board.Lanes[i].Stories[s].offset = 0 + } + } + board.SetMessage(fmt.Sprintf("Loaded file: %s", path), false) } func handleSignals(c <-chan os.Signal) { @@ -163,6 +193,20 @@ func handleSignals(c <-chan os.Signal) { } } +func DefaultBoard(cols, rows int) Board { + return Board{ + Title: "", + Lanes: make([]Lane, 0, 1), + Current: -1, + laneOff: 0, + message: "Welcome to SWIM", + msgErr: false, + width: cols, + height: rows, + Zoom: 3} + +} + func main() { flag.Parse() args := flag.Args() @@ -172,16 +216,7 @@ func main() { LoadFile(args[0], cols, rows) } else { fp = "" - board = Board{ - Title: "My Test Board", - Lanes: make([]Lane, 0, 1), - Current: -1, - laneOff: 0, - message: "Welcome to SWIM", - msgErr: false, - width: cols, - height: rows, - Zoom: 3} + board = DefaultBoard(cols, rows) } // watch for signals, send them to be handled diff --git a/story.go b/story.go index 7a7656a..546e58c 100644 --- a/story.go +++ b/story.go @@ -26,12 +26,13 @@ type Story struct { func (s *Story) View(b *Board) { s.BuildStorySlice(b.width) var ch rune + for { s.Draw(b) ch = Getch() switch ch { - case 'j', 'k': + case 'j', 'k', 'g', 'G': b.ClearMessage() s.Scroll(ch, b) case ':': @@ -159,6 +160,18 @@ func (s *Story) Scroll(dir rune, b *Board) { } else { b.SetMessage("Cannot move further up", true) } + case 'g': + if s.offset > 0 { + s.offset = 0 + } else { + b.SetMessage("Cannot move further up", true) + } + case 'G': + if s.offset + b.height - 3 < len(s.stSlice) { + s.offset = len(s.stSlice) - b.height + 3 + } else { + b.SetMessage("Cannot move further down", true) + } } } @@ -187,7 +200,7 @@ func (s *Story) Update(args []string, b *Board) { s.BuildStorySlice(b.width) case "points", "sp", "pts", "p": if len(args) != 2 { - ps, err := GetAndConfirmCommandLine("Set story points: ") + ps, err := GetCommandLine("Set story points: ") if err != nil { b.SetMessage(err.Error(), true) return @@ -232,6 +245,7 @@ func (s *Story) Update(args []string, b *Board) { return } s.Tasks[num].Complete = !s.Tasks[num].Complete + s.Updated = time.Now() b.SetMessage("Task state updated", false) default: b.SetMessage(fmt.Sprintf("Unknown story location %q", args[0]), true) @@ -256,6 +270,7 @@ func (s *Story) AddRemoveUser(users []string, b *Board) { } } sort.Strings(s.Users) + s.Updated = time.Now() b.SetMessage("Updated user list", false) } @@ -266,6 +281,7 @@ func (s *Story) AddTask(b *Board) { return } s.Tasks = append(s.Tasks, Task{body, false}) + s.Updated = time.Now() s.BuildStorySlice(b.width) b.SetMessage("Task added", false) } @@ -283,6 +299,7 @@ func (s Story) Duplicate() Story { copy(out.Comments, s.Comments) out.Created = s.Created out.Updated = s.Updated + out.Points = s.Points return out }