-
Notifications
You must be signed in to change notification settings - Fork 51
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add key package to handle custom keys
- Loading branch information
Showing
3 changed files
with
350 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
package key | ||
|
||
import ( | ||
"fmt" | ||
"io/ioutil" | ||
"os" | ||
"os/user" | ||
"sort" | ||
"strings" | ||
"time" | ||
|
||
"github.com/mkchoi212/fac/color" | ||
yaml "gopkg.in/yaml.v2" | ||
) | ||
|
||
var currentUser = user.Current | ||
|
||
// Binding represents the user's key binding configuration | ||
type Binding map[string]string | ||
|
||
// Following constants represent all the actions available to the user | ||
// The string literals are used to retrieve values from `Binding` and | ||
// when writing/reading from .fac.yml | ||
const ( | ||
SelectLocal = "select_local" | ||
SelectIncoming = "select_incoming" | ||
ToggleViewOrientation = "toggle_view" | ||
ShowLinesUp = "show_up" | ||
ShowLinesDown = "show_down" | ||
ScrollUp = "scroll_up" | ||
ScrollDown = "scroll_down" | ||
EditCode = "edit" | ||
NextConflict = "next" | ||
PreviousConflict = "previous" | ||
QuitApplication = "quit" | ||
ShowHelp = "help" | ||
) | ||
|
||
// defaultBinding is used when the user has not specified any of the | ||
// available actions via `.fac.yml` | ||
var defaultBinding = Binding{ | ||
SelectLocal: "a", | ||
SelectIncoming: "d", | ||
ToggleViewOrientation: "v", | ||
ShowLinesUp: "w", | ||
ShowLinesDown: "s", | ||
ScrollUp: "j", | ||
ScrollDown: "k", | ||
EditCode: "e", | ||
NextConflict: "n", | ||
PreviousConflict: "p", | ||
QuitApplication: "q", | ||
ShowHelp: "h", | ||
} | ||
|
||
// LoadSettings looks for a user specified key-binding settings file - `$HOME/.fac.yml` | ||
// and returns a map representation of the file | ||
// It also looks for errors, and ambiguities within the file and notifies the user of them | ||
func LoadSettings() (b Binding, err error) { | ||
b, err = parseSettings() | ||
if err != nil { | ||
return | ||
} | ||
|
||
warnings, fatals := b.verify() | ||
if len(fatals) != 0 { | ||
fmt.Println(color.Red(color.Regular, "🚫 %d unrecoverable error(s) detected in .fac.yml\n", len(fatals))) | ||
for _, msg := range fatals { | ||
fmt.Println(color.Red(color.Regular, "%s", msg)) | ||
} | ||
os.Exit(1) | ||
} | ||
|
||
if len(warnings) != 0 { | ||
fmt.Println(color.Yellow(color.Regular, "⚠️ %d infraction(s) detected in .fac.yml\n", len(warnings))) | ||
for _, msg := range warnings { | ||
fmt.Println(color.Yellow(color.Regular, "%s", msg)) | ||
} | ||
fmt.Println() | ||
time.Sleep(time.Duration(2) * time.Second) | ||
} | ||
|
||
b.consolidate() | ||
return | ||
} | ||
|
||
// parseSettings looks for `$HOME/.fac.yml` and parses it into a `Binding` value | ||
// If the file does not exist, it returns the `defaultBinding` | ||
func parseSettings() (b Binding, err error) { | ||
usr, err := currentUser() | ||
if err != nil { | ||
fmt.Println(color.Yellow(color.Regular, "fac: %s. Default key-bindings will be used", err.Error())) | ||
return defaultBinding, nil | ||
} | ||
|
||
// Read config file | ||
f, err := ioutil.ReadFile(usr.HomeDir + "/.fac.yml") | ||
if err != nil { | ||
fmt.Println(color.Yellow(color.Regular, "fac: %s. Default key-bindings will be used", err.Error())) | ||
return defaultBinding, nil | ||
} | ||
|
||
// Parse config file | ||
if err = yaml.Unmarshal(f, &b); err != nil { | ||
fmt.Println(color.Yellow(color.Regular, "fac: %s. Default key-bindings will be used", err.Error())) | ||
return defaultBinding, nil | ||
} | ||
return | ||
} | ||
|
||
// consolidate takes the user's key-binding settings and fills the missings key-binds | ||
// with the default key-binding values | ||
func (b Binding) consolidate() { | ||
for key, defaultValue := range defaultBinding { | ||
userValue, ok := b[key] | ||
|
||
if !ok || userValue == "" { | ||
b[key] = defaultValue | ||
} else if len(userValue) > 1 { | ||
b[key] = string(userValue[0]) | ||
} | ||
} | ||
} | ||
|
||
// verify looks through the user's key-binding settings and looks for any infractions such as.. | ||
// 1. Invalid/ignored key-binding keys | ||
// 2. Multi-character key-mappings | ||
// 3. Duplicate key-mappings | ||
func (b Binding) verify() (warnings []string, fatals []string) { | ||
bindTable := map[string][]string{} | ||
|
||
for k, v := range b { | ||
bindTable[string(v[0])] = append(bindTable[string(v[0])], k) | ||
|
||
// Check for "1. Invalid/ignored key-binding keys" | ||
if _, ok := defaultBinding[k]; !ok { | ||
warnings = append(warnings, fmt.Sprintf("Invalid key: \"%s\" will be ignored", k)) | ||
delete(b, k) | ||
continue | ||
} | ||
|
||
// Check for "2. Multi-character key-mappings" | ||
if len(v) > 1 { | ||
warnings = append(warnings, fmt.Sprintf("Illegal multi-character mapping: \"%s\" will be interpreted as '%s'", v, string(v[0]))) | ||
continue | ||
} | ||
} | ||
|
||
// Check for "3. Duplicate key-mappings" | ||
for k, v := range bindTable { | ||
if len(v) > 1 { | ||
sort.Strings(v) | ||
duplicateValues := strings.Join(v, ", ") | ||
fatals = append(fatals, fmt.Sprintf("Duplicate key-mapping: \"%s\" are all represented by '%s'", duplicateValues, k)) | ||
} | ||
} | ||
|
||
return | ||
} | ||
|
||
// Summary returns a short summary of the provided `Binding` | ||
// and is used as the helpful string displayed by the user's input field | ||
// e.g. "[w,a,s,d,e,?] >>" | ||
func (b Binding) Summary() string { | ||
targetKeys := []string{ | ||
b[ShowLinesUp], | ||
b[SelectLocal], | ||
b[ShowLinesDown], | ||
b[SelectIncoming], | ||
b[EditCode], | ||
} | ||
return "[" + strings.Join(targetKeys, ",") + ",?] >>" | ||
} | ||
|
||
// Help returns a help string that is displayed on the right panel of the UI | ||
// It should provided an overall summary of all available key options | ||
func (b Binding) Help() string { | ||
format := ` | ||
%s - use local version | ||
%s - use incoming version | ||
%s - manually edit code | ||
%s - show more lines up | ||
%s - show more lines down | ||
%s - scroll up | ||
%s - scroll down | ||
%s - view orientation | ||
%s - next conflict | ||
%s - previous conflict | ||
%s | ? - help | ||
%s | Ctrl+C - quit | ||
` | ||
|
||
return fmt.Sprintf(format, b[SelectLocal], b[SelectIncoming], b[EditCode], | ||
b[ShowLinesUp], b[ShowLinesDown], | ||
b[ScrollUp], b[ScrollDown], b[ToggleViewOrientation], b[NextConflict], b[PreviousConflict], | ||
b[ShowHelp], b[QuitApplication]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
package key | ||
|
||
import ( | ||
"errors" | ||
"os" | ||
"os/user" | ||
"sort" | ||
"testing" | ||
|
||
"gopkg.in/yaml.v2" | ||
|
||
"github.com/mkchoi212/fac/testhelper" | ||
) | ||
|
||
var tests = []struct { | ||
settings Binding | ||
expected Binding | ||
warnings []string | ||
fatals []string | ||
}{ | ||
{ | ||
settings: Binding{ | ||
"foobar": "a", | ||
"hello": "b", | ||
}, | ||
expected: defaultBinding, | ||
warnings: []string{ | ||
"Invalid key: \"foobar\" will be ignored", | ||
"Invalid key: \"hello\" will be ignored", | ||
}, | ||
fatals: nil, | ||
}, | ||
{ | ||
settings: Binding{ | ||
SelectLocal: "i", | ||
SelectIncoming: "incoming", | ||
}, | ||
expected: Binding{ | ||
SelectLocal: "l", | ||
SelectIncoming: "i", | ||
}, | ||
warnings: []string{ | ||
"Illegal multi-character mapping: \"incoming\" will be interpreted as 'i'", | ||
}, | ||
fatals: []string{ | ||
"Duplicate key-mapping: \"select_incoming, select_local\" are all represented by 'i'", | ||
}, | ||
}, | ||
{ | ||
settings: Binding{ | ||
ShowLinesDown: "d", | ||
ShowLinesUp: "u", | ||
}, | ||
expected: Binding{ | ||
ShowLinesDown: "d", | ||
ShowLinesUp: "u", | ||
}, | ||
warnings: nil, | ||
fatals: nil, | ||
}, | ||
} | ||
|
||
func TestLoadSettings(t *testing.T) { | ||
currentUser = func() (*user.User, error) { | ||
return &user.User{HomeDir: "."}, nil | ||
} | ||
|
||
for _, test := range tests { | ||
if test.fatals != nil { | ||
continue | ||
} | ||
|
||
// Create dummy yml file with content | ||
f, err := os.Create(".fac.yml") | ||
testhelper.Ok(t, err) | ||
data, _ := yaml.Marshal(&test.settings) | ||
f.WriteString(string(data)) | ||
f.Close() | ||
|
||
output, err := LoadSettings() | ||
testhelper.Ok(t, err) | ||
|
||
test.expected.consolidate() | ||
testhelper.Equals(t, test.expected, output) | ||
} | ||
} | ||
|
||
func TestParseSettings(t *testing.T) { | ||
// Test with invalid currentUser | ||
currentUser = func() (*user.User, error) { | ||
return nil, errors.New("Could not find current user") | ||
} | ||
binding, _ := parseSettings() | ||
testhelper.Equals(t, defaultBinding, binding) | ||
|
||
// Test with invalid directory | ||
currentUser = func() (*user.User, error) { | ||
return &user.User{HomeDir: "foobar"}, nil | ||
} | ||
binding, _ = parseSettings() | ||
testhelper.Equals(t, defaultBinding, binding) | ||
|
||
// Test with valid directory with empty file | ||
currentUser = func() (*user.User, error) { | ||
return &user.User{HomeDir: "."}, nil | ||
} | ||
f, err := os.Create(".fac.yml") | ||
testhelper.Ok(t, err) | ||
defer f.Close() | ||
|
||
binding, _ = parseSettings() | ||
testhelper.Equals(t, 0, len(binding)) | ||
|
||
// Test valid directory with erroneous content | ||
f.WriteString("erroneous content") | ||
binding, _ = parseSettings() | ||
testhelper.Equals(t, defaultBinding, binding) | ||
} | ||
|
||
func TestVerify(t *testing.T) { | ||
for _, test := range tests { | ||
warnings, fatals := test.settings.verify() | ||
sort.Strings(warnings) | ||
sort.Strings(test.warnings) | ||
testhelper.Equals(t, test.warnings, warnings) | ||
testhelper.Equals(t, test.fatals, fatals) | ||
} | ||
} | ||
|
||
func TestSummary(t *testing.T) { | ||
summary := defaultBinding.Summary() | ||
testhelper.Equals(t, summary, "[w,a,s,d,e,?] >>") | ||
} | ||
|
||
func TestHelp(t *testing.T) { | ||
helpMsg := defaultBinding.Help() | ||
testhelper.Assert(t, len(helpMsg) != 0, "Help message should not be of length 0") | ||
} | ||
|
||
func TestConsolidate(t *testing.T) { | ||
for _, test := range tests { | ||
test.settings.consolidate() | ||
|
||
for k := range defaultBinding { | ||
_, ok := test.settings[k] | ||
testhelper.Equals(t, true, ok) | ||
} | ||
} | ||
} |