From e8923a65aa88aed55884af576e6adabc3cff7d79 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Sun, 11 Apr 2021 16:55:47 +0100 Subject: [PATCH 1/5] update README --- documentation/ideas.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/documentation/ideas.md b/documentation/ideas.md index 4a234ecc..625c6669 100644 --- a/documentation/ideas.md +++ b/documentation/ideas.md @@ -3,8 +3,4 @@ It may be helpful to be able to answer questions like "How many Bitcoin Cash do I have, and where?" for forks like Bitcoin Cash, Bitcoin Gold, ... that share parts of their blockchain with Bitcoin. This can be done by making use of the block height information (before/after a fork?), and making the -code fork-aware. - -## Interactive CLI -Running `get-neighbour-transactions` and the other commands repeatedly is quite frustrating. -BitBook should offer an interactive mode that guides the user to completion. \ No newline at end of file +code fork-aware. \ No newline at end of file From c7fa950d34cdac5dbf4e00ad3162fdc06a6b1d61 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Sun, 11 Apr 2021 18:36:12 +0100 Subject: [PATCH 2/5] allow setting the prompt --- .../cotto/bitbook/cli/PromptChangeListener.java | 7 +++++++ .../cotto/bitbook/cli/CustomPromptProvider.java | 16 ++++++++++++++-- .../bitbook/cli/CustomPromptProviderTest.java | 15 ++++++++++++++- 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 cli/base/src/main/java/de/cotto/bitbook/cli/PromptChangeListener.java diff --git a/cli/base/src/main/java/de/cotto/bitbook/cli/PromptChangeListener.java b/cli/base/src/main/java/de/cotto/bitbook/cli/PromptChangeListener.java new file mode 100644 index 00000000..2df75cc5 --- /dev/null +++ b/cli/base/src/main/java/de/cotto/bitbook/cli/PromptChangeListener.java @@ -0,0 +1,7 @@ +package de.cotto.bitbook.cli; + +public interface PromptChangeListener { + void changePrompt(String newState); + + void changePromptToDefault(); +} diff --git a/cli/src/main/java/de/cotto/bitbook/cli/CustomPromptProvider.java b/cli/src/main/java/de/cotto/bitbook/cli/CustomPromptProvider.java index d8f38ecd..d7683f08 100644 --- a/cli/src/main/java/de/cotto/bitbook/cli/CustomPromptProvider.java +++ b/cli/src/main/java/de/cotto/bitbook/cli/CustomPromptProvider.java @@ -6,7 +6,10 @@ import org.springframework.stereotype.Component; @Component -public class CustomPromptProvider implements PromptProvider { +public class CustomPromptProvider implements PromptProvider, PromptChangeListener { + private static final String DEFAULT_PROMPT = "BitBook$ "; + + private String prompt = DEFAULT_PROMPT; public CustomPromptProvider() { // default constructor @@ -14,7 +17,16 @@ public CustomPromptProvider() { @Override public AttributedString getPrompt() { - return new AttributedString("BitBook$ ", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)); + return new AttributedString(prompt, AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)); + } + + @Override + public void changePrompt(String newPrompt) { + prompt = newPrompt; } + @Override + public void changePromptToDefault() { + changePrompt(DEFAULT_PROMPT); + } } \ No newline at end of file diff --git a/cli/src/test/java/de/cotto/bitbook/cli/CustomPromptProviderTest.java b/cli/src/test/java/de/cotto/bitbook/cli/CustomPromptProviderTest.java index 696e8062..6de51698 100644 --- a/cli/src/test/java/de/cotto/bitbook/cli/CustomPromptProviderTest.java +++ b/cli/src/test/java/de/cotto/bitbook/cli/CustomPromptProviderTest.java @@ -9,7 +9,20 @@ class CustomPromptProviderTest { private final CustomPromptProvider customPromptProvider = new CustomPromptProvider(); @Test - void customPrompt_text() { + void default_prompt() { + assertThat(customPromptProvider.getPrompt()).hasToString("BitBook$ "); + } + + @Test + void changePrompt() { + customPromptProvider.changePrompt("foo"); + assertThat(customPromptProvider.getPrompt()).hasToString("foo"); + } + + @Test + void changePromptToDefault() { + customPromptProvider.changePrompt("foo"); + customPromptProvider.changePromptToDefault(); assertThat(customPromptProvider.getPrompt()).hasToString("BitBook$ "); } } \ No newline at end of file From f4cabc540abb81769acfe4b929630914e071177a Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Sun, 11 Apr 2021 16:55:52 +0100 Subject: [PATCH 3/5] add gradle subproject --- cli/build.gradle | 1 + cli/wizard/build.gradle | 13 ++++ .../bitbook/wizard/cli/WizardCommands.java | 37 ++++++++++ .../wizard/cli/WizardCommandsTest.java | 68 +++++++++++++++++++ settings.gradle | 2 + wizard/build.gradle | 8 +++ .../cotto/bitbook/wizard/WizardService.java | 24 +++++++ .../bitbook/wizard/WizardServiceTest.java | 31 +++++++++ 8 files changed, 184 insertions(+) create mode 100644 cli/wizard/build.gradle create mode 100644 cli/wizard/src/main/java/de/cotto/bitbook/wizard/cli/WizardCommands.java create mode 100644 cli/wizard/src/test/java/de/cotto/bitbook/wizard/cli/WizardCommandsTest.java create mode 100644 wizard/build.gradle create mode 100644 wizard/src/main/java/de/cotto/bitbook/wizard/WizardService.java create mode 100644 wizard/src/test/java/de/cotto/bitbook/wizard/WizardServiceTest.java diff --git a/cli/build.gradle b/cli/build.gradle index 89d2fa0c..4dc2adc8 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -10,6 +10,7 @@ dependencies { implementation project(':cli:ownership') implementation project(':cli:lnd') implementation project(':cli:base') + implementation project(':cli:wizard') implementation 'org.flywaydb:flyway-core' testImplementation testFixtures(project(':backend:transaction')) testImplementation testFixtures(project(':backend:transaction:models')) diff --git a/cli/wizard/build.gradle b/cli/wizard/build.gradle new file mode 100644 index 00000000..20b3003e --- /dev/null +++ b/cli/wizard/build.gradle @@ -0,0 +1,13 @@ +plugins { + id 'bitbook.java-library-conventions' +} + +dependencies { + implementation project(':cli:base') + implementation project(':wizard') + testImplementation testFixtures(project(':backend:transaction:models')) +} + +jar { + archivesBaseName = 'wizard-cli' +} \ No newline at end of file diff --git a/cli/wizard/src/main/java/de/cotto/bitbook/wizard/cli/WizardCommands.java b/cli/wizard/src/main/java/de/cotto/bitbook/wizard/cli/WizardCommands.java new file mode 100644 index 00000000..1980391a --- /dev/null +++ b/cli/wizard/src/main/java/de/cotto/bitbook/wizard/cli/WizardCommands.java @@ -0,0 +1,37 @@ +package de.cotto.bitbook.wizard.cli; + +import de.cotto.bitbook.cli.PromptChangeListener; +import de.cotto.bitbook.wizard.WizardService; +import org.springframework.shell.Availability; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellMethod; + +@ShellComponent +public class WizardCommands { + private final WizardService wizardService; + private final PromptChangeListener promptChangeListener; + + public WizardCommands(WizardService wizardService, PromptChangeListener promptChangeListener) { + this.wizardService = wizardService; + this.promptChangeListener = promptChangeListener; + } + + @ShellMethod("Start the wizard which helps you complete the ownership information") + public void wizard() { + wizardService.enableWizard(); + promptChangeListener.changePrompt("wizard$ "); + } + + @ShellMethod("Exit the wizard") + public void exitWizard() { + wizardService.disableWizard(); + promptChangeListener.changePromptToDefault(); + } + + public Availability exitWizardAvailability() { + if (wizardService.isEnabled()) { + return Availability.available(); + } + return Availability.unavailable("wizard is not active"); + } +} diff --git a/cli/wizard/src/test/java/de/cotto/bitbook/wizard/cli/WizardCommandsTest.java b/cli/wizard/src/test/java/de/cotto/bitbook/wizard/cli/WizardCommandsTest.java new file mode 100644 index 00000000..71f16f7c --- /dev/null +++ b/cli/wizard/src/test/java/de/cotto/bitbook/wizard/cli/WizardCommandsTest.java @@ -0,0 +1,68 @@ +package de.cotto.bitbook.wizard.cli; + +import de.cotto.bitbook.cli.PromptChangeListener; +import de.cotto.bitbook.wizard.WizardService; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class WizardCommandsTest { + @InjectMocks + private WizardCommands wizardCommands; + + @Mock + private WizardService wizardService; + + @Mock + private PromptChangeListener promptChangeListener; + + @Nested + class WizardDisabled { + @Test + void enables_wizard() { + wizardCommands.wizard(); + verify(wizardService).enableWizard(); + } + + @Test + void exit_wizard_command_initially_not_available() { + assertThat(wizardCommands.exitWizardAvailability().isAvailable()).isEqualTo(false); + assertThat(wizardCommands.exitWizardAvailability().getReason()).isEqualTo("wizard is not active"); + } + } + + @Nested + class WizardEnabled { + @Test + void changes_prompt() { + wizardCommands.wizard(); + verify(promptChangeListener).changePrompt("wizard$ "); + } + + @Test + void exit_wizard_command_available() { + when(wizardService.isEnabled()).thenReturn(true); + assertThat(wizardCommands.exitWizardAvailability().isAvailable()).isEqualTo(true); + } + + @Test + void exit_wizard_changes_prompt_to_default() { + wizardCommands.exitWizard(); + verify(promptChangeListener).changePromptToDefault(); + } + + @Test + void exit_wizard_notifies_service() { + wizardCommands.exitWizard(); + verify(wizardService).disableWizard(); + } + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 6b13ec51..4f89fd9e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,7 @@ include 'cli' include 'cli:base' include 'cli:lnd' include 'cli:ownership' +include 'cli:wizard' include 'backend' include 'backend:price' include 'backend:blockheight' @@ -24,3 +25,4 @@ include 'backend:request' include 'backend:request:models' include 'lnd' include 'ownership' +include 'wizard' diff --git a/wizard/build.gradle b/wizard/build.gradle new file mode 100644 index 00000000..eba51146 --- /dev/null +++ b/wizard/build.gradle @@ -0,0 +1,8 @@ +plugins { + id 'bitbook.java-library-conventions' +} + +dependencies { + implementation project(':ownership') + testImplementation testFixtures(project(':backend:transaction:models')) +} \ No newline at end of file diff --git a/wizard/src/main/java/de/cotto/bitbook/wizard/WizardService.java b/wizard/src/main/java/de/cotto/bitbook/wizard/WizardService.java new file mode 100644 index 00000000..e3e3d81e --- /dev/null +++ b/wizard/src/main/java/de/cotto/bitbook/wizard/WizardService.java @@ -0,0 +1,24 @@ +package de.cotto.bitbook.wizard; + +import org.springframework.stereotype.Component; + +@Component +public class WizardService { + private boolean enabled; + + public WizardService() { + // default constructor + } + + public void enableWizard() { + enabled = true; + } + + public boolean isEnabled() { + return enabled; + } + + public void disableWizard() { + enabled = false; + } +} diff --git a/wizard/src/test/java/de/cotto/bitbook/wizard/WizardServiceTest.java b/wizard/src/test/java/de/cotto/bitbook/wizard/WizardServiceTest.java new file mode 100644 index 00000000..134c22a7 --- /dev/null +++ b/wizard/src/test/java/de/cotto/bitbook/wizard/WizardServiceTest.java @@ -0,0 +1,31 @@ +package de.cotto.bitbook.wizard; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class WizardServiceTest { + @InjectMocks + private WizardService wizardService; + + @Test + void isEnabled() { + assertThat(wizardService.isEnabled()).isFalse(); + } + + @Test + void enableWizard() { + wizardService.enableWizard(); + assertThat(wizardService.isEnabled()).isTrue(); + } + + @Test + void disableWizard() { + wizardService.disableWizard(); + assertThat(wizardService.isEnabled()).isFalse(); + } +} \ No newline at end of file From 16ec10ad0cde974e6bdf861998a8acda848e9fc6 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Sat, 24 Apr 2021 21:37:26 +0100 Subject: [PATCH 4/5] add availability for "exit-wizard" --- .../de/cotto/bitbook/wizard/cli/WizardCommands.java | 7 +++++++ .../cotto/bitbook/wizard/cli/WizardCommandsTest.java | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/cli/wizard/src/main/java/de/cotto/bitbook/wizard/cli/WizardCommands.java b/cli/wizard/src/main/java/de/cotto/bitbook/wizard/cli/WizardCommands.java index 1980391a..8896bb72 100644 --- a/cli/wizard/src/main/java/de/cotto/bitbook/wizard/cli/WizardCommands.java +++ b/cli/wizard/src/main/java/de/cotto/bitbook/wizard/cli/WizardCommands.java @@ -28,6 +28,13 @@ public void exitWizard() { promptChangeListener.changePromptToDefault(); } + public Availability wizardAvailability() { + if (wizardService.isEnabled()) { + return Availability.unavailable("wizard is active"); + } + return Availability.available(); + } + public Availability exitWizardAvailability() { if (wizardService.isEnabled()) { return Availability.available(); diff --git a/cli/wizard/src/test/java/de/cotto/bitbook/wizard/cli/WizardCommandsTest.java b/cli/wizard/src/test/java/de/cotto/bitbook/wizard/cli/WizardCommandsTest.java index 71f16f7c..ef4e2393 100644 --- a/cli/wizard/src/test/java/de/cotto/bitbook/wizard/cli/WizardCommandsTest.java +++ b/cli/wizard/src/test/java/de/cotto/bitbook/wizard/cli/WizardCommandsTest.java @@ -37,6 +37,11 @@ void exit_wizard_command_initially_not_available() { assertThat(wizardCommands.exitWizardAvailability().isAvailable()).isEqualTo(false); assertThat(wizardCommands.exitWizardAvailability().getReason()).isEqualTo("wizard is not active"); } + + @Test + void wizard_command_initially_available() { + assertThat(wizardCommands.wizardAvailability().isAvailable()).isEqualTo(true); + } } @Nested @@ -64,5 +69,12 @@ void exit_wizard_notifies_service() { wizardCommands.exitWizard(); verify(wizardService).disableWizard(); } + + @Test + void start_wizard_command_not_available() { + when(wizardService.isEnabled()).thenReturn(true); + assertThat(wizardCommands.wizardAvailability().isAvailable()).isEqualTo(false); + assertThat(wizardCommands.wizardAvailability().getReason()).isEqualTo("wizard is active"); + } } } \ No newline at end of file From e96c0a833f89bcfe3e525f62f7ffabf3825b3e99 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Sun, 25 Apr 2021 10:23:09 +0100 Subject: [PATCH 5/5] add terminal support class --- .../de/cotto/bitbook/cli/TerminalSupport.java | 44 ++++++++++ .../bitbook/cli/TerminalSupportTest.java | 85 +++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 cli/base/src/main/java/de/cotto/bitbook/cli/TerminalSupport.java create mode 100644 cli/base/src/test/java/de/cotto/bitbook/cli/TerminalSupportTest.java diff --git a/cli/base/src/main/java/de/cotto/bitbook/cli/TerminalSupport.java b/cli/base/src/main/java/de/cotto/bitbook/cli/TerminalSupport.java new file mode 100644 index 00000000..1cc052b9 --- /dev/null +++ b/cli/base/src/main/java/de/cotto/bitbook/cli/TerminalSupport.java @@ -0,0 +1,44 @@ +package de.cotto.bitbook.cli; + +import org.apache.commons.lang3.StringUtils; +import org.jline.reader.LineReader; +import org.jline.terminal.Terminal; +import org.springframework.stereotype.Component; + +import java.io.PrintWriter; +import java.util.Optional; + +@Component +public class TerminalSupport { + private final LineReader lineReader; + private final PromptChangeListener promptChangeListener; + private final PrintWriter writer; + + public TerminalSupport(Terminal terminal, LineReader lineReader, PromptChangeListener promptChangeListener) { + this.lineReader = lineReader; + this.promptChangeListener = promptChangeListener; + writer = terminal.writer(); + } + + public void write(String text) { + writer.println(text); + writer.flush(); + } + + public Optional request(String prompt) { + String answer = lineReader.readLine(prompt + " "); + if (StringUtils.isBlank(answer)) { + return Optional.empty(); + } else { + return Optional.of(answer); + } + } + + public void changePrompt(String newPrompt) { + promptChangeListener.changePrompt(newPrompt); + } + + public void changePromptToDefault() { + promptChangeListener.changePromptToDefault(); + } +} diff --git a/cli/base/src/test/java/de/cotto/bitbook/cli/TerminalSupportTest.java b/cli/base/src/test/java/de/cotto/bitbook/cli/TerminalSupportTest.java new file mode 100644 index 00000000..e552b4cd --- /dev/null +++ b/cli/base/src/test/java/de/cotto/bitbook/cli/TerminalSupportTest.java @@ -0,0 +1,85 @@ +package de.cotto.bitbook.cli; + +import org.jline.reader.LineReader; +import org.jline.terminal.Terminal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.PrintWriter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TerminalSupportTest { + private TerminalSupport terminalSupport; + + @Mock + private Terminal terminal; + + @Mock + private LineReader lineReader; + + @Mock + private PromptChangeListener promptChangeListener; + + @Mock + private PrintWriter writer; + + @BeforeEach + void setUp() { + when(terminal.writer()).thenReturn(writer); + terminalSupport = new TerminalSupport(terminal, lineReader, promptChangeListener); + } + + @Test + void write() { + terminalSupport.write("x"); + InOrder inOrder = Mockito.inOrder(writer); + inOrder.verify(writer).println("x"); + inOrder.verify(writer).flush(); + } + + @Test + void request_prompt() { + terminalSupport.request("?"); + verify(lineReader).readLine("? "); + } + + @Test + void request_empty() { + when(lineReader.readLine(anyString())).thenReturn(""); + assertThat(terminalSupport.request("?")).isEmpty(); + } + + @Test + void request_blank() { + when(lineReader.readLine(anyString())).thenReturn("\t \r\n"); + assertThat(terminalSupport.request("?")).isEmpty(); + } + + @Test + void request() { + when(lineReader.readLine(anyString())).thenReturn("hello"); + assertThat(terminalSupport.request("?")).contains("hello"); + } + + @Test + void changePrompt() { + terminalSupport.changePrompt("x"); + verify(promptChangeListener).changePrompt("x"); + } + + @Test + void changePromptToDefault() { + terminalSupport.changePromptToDefault(); + verify(promptChangeListener).changePromptToDefault(); + } +} \ No newline at end of file