From bf5786ebd6d585459623cb9b38df79d05488c94a Mon Sep 17 00:00:00 2001 From: Ingo Weinhold Date: Wed, 27 Nov 2013 15:38:07 +0100 Subject: [PATCH] pkgman search: Add -D/--details * Add a detailed listing mode (-D/--details), which prints a table with repository/installation location name, package name, package version, package architecture. * Make the normal listing more compact. Now there's only one row per package. The "Installed" column has been replaced by "Status" which displays if/where the package is installed and whether it matches the repository version. --- src/bin/pkgman/Jamfile | 1 + src/bin/pkgman/TextTable.cpp | 274 ++++++++++++++++++++++++++++++ src/bin/pkgman/TextTable.h | 42 +++++ src/bin/pkgman/command_search.cpp | 204 +++++++++++++++------- 4 files changed, 464 insertions(+), 57 deletions(-) create mode 100644 src/bin/pkgman/TextTable.cpp create mode 100644 src/bin/pkgman/TextTable.h diff --git a/src/bin/pkgman/Jamfile b/src/bin/pkgman/Jamfile index e37556419c..e225036624 100644 --- a/src/bin/pkgman/Jamfile +++ b/src/bin/pkgman/Jamfile @@ -17,6 +17,7 @@ BinCommand pkgman : JobStateListener.cpp PackageManager.cpp pkgman.cpp + TextTable.cpp : package be $(TARGET_LIBSUPC++) $(TARGET_LIBSTDC++) diff --git a/src/bin/pkgman/TextTable.cpp b/src/bin/pkgman/TextTable.cpp new file mode 100644 index 0000000000..f173665532 --- /dev/null +++ b/src/bin/pkgman/TextTable.cpp @@ -0,0 +1,274 @@ +/* + * Copyright 2013, Ingo Weinhold, ingo_weinhold@gmx.de. + * Distributed under the terms of the MIT License. + */ + + +#include "TextTable.h" + +#include + +#include + + +// #pragma mark - Column + + +struct TextTable::Column { + Column(const BString& title, enum alignment align, bool canTruncate) + : + fTitle(title), + fAlignment(align), + fCanBeTruncated(canTruncate), + fNeededWidth(0), + fWidth(0) + { + UpdateNeededWidth(fTitle); + fMinWidth = fNeededWidth; + } + + const BString& Title() const + { + return fTitle; + } + + enum alignment Alignment() const + { + return fAlignment; + } + + bool CanBeTruncated() const + { + return fCanBeTruncated; + } + + int32 NeededWidth() const + { + return fNeededWidth; + } + + int32 MinWidth() const + { + return fMinWidth; + } + + int32 Width() const + { + return fWidth; + } + + void SetWidth(int32 width) + { + fWidth = width; + } + + void UpdateNeededWidth(const BString& text) + { + // TODO: Full-width character support. + int32 textWidth = text.CountChars(); + if (textWidth > fNeededWidth) + fNeededWidth = textWidth; + } + + BString Format(const BString& text) + { + // TODO: Full-width character support. + int32 textWidth = text.CountChars(); + if (textWidth == fWidth) + return text; + + // truncate, if too long + if (textWidth > fWidth) { + BString result(text); + result.TruncateChars(fWidth); + return result; + } + + // align, if too short + int32 missing = fWidth - textWidth; + switch (fAlignment) { + case B_ALIGN_LEFT: + default: + { + BString result(text); + result.Append(' ', missing); + return result; + } + + case B_ALIGN_RIGHT: + { + BString result; + result.Append(' ', missing); + result.Append(text); + return result; + } + + case B_ALIGN_CENTER: + { + BString result; + result.Append(' ', missing / 2); + result.Append(text); + result.Append(' ', missing - missing / 2); + return result; + } + } + } + +private: + BString fTitle; + enum alignment fAlignment; + bool fCanBeTruncated; + int32 fNeededWidth; + int32 fMinWidth; + int32 fWidth; +}; + + +// #pragma mark - TextTable + + +TextTable::TextTable() + : + fColumns(10, true), + fRows(100, true) +{ +} + + +TextTable::~TextTable() +{ +} + + +int32 +TextTable::CountColumns() const +{ + return fColumns.CountItems(); +} + + +void +TextTable::AddColumn(const BString& title, enum alignment align, + bool canTruncate) +{ + Column* column = new Column(title, align, canTruncate); + if (!fColumns.AddItem(column)) { + delete column; + throw std::bad_alloc(); + } +} + + +int32 +TextTable::CountRows() const +{ + return fRows.CountItems(); +} + + +BString +TextTable::TextAt(int32 rowIndex, int32 columnIndex) const +{ + BStringList* row = fRows.ItemAt(rowIndex); + if (row == NULL) + return BString(); + return row->StringAt(columnIndex); +} + + +void +TextTable::SetTextAt(int32 rowIndex, int32 columnIndex, const BString& text) +{ + // If necessary append empty rows up to the specified row index. + while (rowIndex >= fRows.CountItems()) { + BStringList* row = new BStringList(); + if (!fRows.AddItem(row)) { + delete row; + throw std::bad_alloc(); + } + } + + // If necessary append empty strings up to the specified column index. + BStringList* row = fRows.ItemAt(rowIndex); + while (columnIndex >= row->CountStrings()) { + if (!row->Add(BString())) + throw std::bad_alloc(); + } + + // set the text + if (!row->Replace(columnIndex, text)) + throw std::bad_alloc(); +} + + +void +TextTable::Print(int32 maxWidth) +{ + int32 columnCount = fColumns.CountItems(); + if (columnCount == 0) + return; + + // determine the column widths + int32 rowCount = fRows.CountItems(); + for (int32 rowIndex = 0; rowIndex < rowCount; rowIndex++) { + BStringList* row = fRows.ItemAt(rowIndex); + int32 rowColumnCount = std::min(row->CountStrings(), columnCount); + for (int32 columnIndex = 0; columnIndex < rowColumnCount; + columnIndex++) { + fColumns.ItemAt(columnIndex)->UpdateNeededWidth( + row->StringAt(columnIndex)); + } + } + + int32 neededWidth = (columnCount - 1) * 2; + // spacing + for (int32 i = 0; i < columnCount; i++) + neededWidth += fColumns.ItemAt(i)->NeededWidth(); + + int32 width = neededWidth; + int32 missingWidth = neededWidth - std::min(maxWidth, neededWidth); + + for (int32 i = 0; i < columnCount; i++) { + Column* column = fColumns.ItemAt(i); + if (missingWidth > 0 && column->CanBeTruncated()) { + int32 truncateBy = std::min(missingWidth, + column->NeededWidth() - column->MinWidth()); + column->SetWidth(column->NeededWidth() - truncateBy); + missingWidth -= truncateBy; + width -= truncateBy; + } else + column->SetWidth(column->NeededWidth()); + } + + // print the header + BString line; + for (int32 i = 0; i < columnCount; i++) { + if (i > 0) + line << " "; + + Column* column = fColumns.ItemAt(i); + line << column->Format(column->Title()); + } + line << '\n'; + fputs(line.String(), stdout); + + line.SetTo('-', width); + line << '\n'; + fputs(line.String(), stdout); + + // print the rows + for (int32 rowIndex = 0; rowIndex < rowCount; rowIndex++) { + line.Truncate(0); + BStringList* row = fRows.ItemAt(rowIndex); + for (int32 columnIndex = 0; columnIndex < columnCount; columnIndex++) { + if (columnIndex > 0) + line << " "; + + line << fColumns.ItemAt(columnIndex)->Format( + row->StringAt(columnIndex)); + } + + line << '\n'; + fputs(line.String(), stdout); + } +} diff --git a/src/bin/pkgman/TextTable.h b/src/bin/pkgman/TextTable.h new file mode 100644 index 0000000000..dd94bfe9b7 --- /dev/null +++ b/src/bin/pkgman/TextTable.h @@ -0,0 +1,42 @@ +/* + * Copyright 2013, Ingo Weinhold, ingo_weinhold@gmx.de. + * Distributed under the terms of the MIT License. + */ +#ifndef TEXT_TABLE_H +#define TEXT_TABLE_H + + +#include +#include +#include + + +class TextTable { +public: + TextTable(); + ~TextTable(); + + int32 CountColumns() const; + void AddColumn(const BString& title, + enum alignment align = B_ALIGN_LEFT, + bool canTruncate = false); + + int32 CountRows() const; + BString TextAt(int32 rowIndex, int32 columnIndex) const; + void SetTextAt(int32 rowIndex, int32 columnIndex, + const BString& text); + + void Print(int32 maxWidth); + +private: + struct Column; + typedef BObjectList ColumnList; + typedef BObjectList RowList; + +private: + ColumnList fColumns; + RowList fRows; +}; + + +#endif // TEXT_TABLE_H diff --git a/src/bin/pkgman/command_search.cpp b/src/bin/pkgman/command_search.cpp index 36b7945e31..d84cff1a96 100644 --- a/src/bin/pkgman/command_search.cpp +++ b/src/bin/pkgman/command_search.cpp @@ -15,17 +15,18 @@ #include #include +#include #include #include "Command.h" #include "PackageManager.h" #include "pkgman.h" +#include "TextTable.h" // TODO: internationalization! -// The printing code will need serious attention wrt. dealing with UTF-8 and, -// even worse, full-width characters. +// The table code doesn't support full-width characters yet. using namespace BPackageKit; @@ -42,10 +43,20 @@ static const char* const kLongUsage = "Options:\n" " -a, --all\n" " List all packages. Specified instead of .\n" + " -D, --details\n" + " Print more details. Matches in each installation location and each\n" + " repository will be listed individually with their version.\n" " -i, --installed-only\n" " Only find installed packages.\n" " -u, --uninstalled-only\n" " Only find not installed packages.\n" + "\n" + "Status flags in non-detailed listings:\n" + " S - installed in system with a matching version in a repository\n" + " s - installed in system without a matching version in a repository\n" + " H - installed in home with a matching version in a repository\n" + " h - installed in home without a matching version in a repository\n" + " v - multiple different versions available in repositories\n" "\n"; @@ -65,16 +76,64 @@ get_terminal_width() } +struct PackageComparator { + PackageComparator(const BSolverRepository* systemRepository, + const BSolverRepository* homeRepository) + : + fSystemRepository(systemRepository), + fHomeRepository(homeRepository) + { + } + + int operator()(const BSolverPackage* a, const BSolverPackage* b) const + { + int cmp = a->Name().Compare(b->Name()); + if (cmp != 0) + return cmp; + + // Names are equal. Sort by installation location and then by repository + // name. + if (a->Repository() == b->Repository()) + return 0; + + if (a->Repository() == fSystemRepository) + return -1; + if (b->Repository() == fSystemRepository) + return 1; + if (a->Repository() == fHomeRepository) + return -1; + if (b->Repository() == fHomeRepository) + return 1; + + return a->Repository()->Name().Compare(b->Repository()->Name()); + } + +private: + const BSolverRepository* fSystemRepository; + const BSolverRepository* fHomeRepository; +}; + + +static int +compare_packages(const BSolverPackage* a, const BSolverPackage* b, + void* comparator) +{ + return (*(PackageComparator*)comparator)(a, b); +} + + int SearchCommand::Execute(int argc, const char* const* argv) { bool installedOnly = false; bool uninstalledOnly = false; bool listAll = false; + bool details = false; while (true) { static struct option sLongOptions[] = { { "all", no_argument, 0, 'a' }, + { "details", no_argument, 0, 'D' }, { "help", no_argument, 0, 'h' }, { "installed-only", no_argument, 0, 'i' }, { "uninstalled-only", no_argument, 0, 'u' }, @@ -82,7 +141,7 @@ SearchCommand::Execute(int argc, const char* const* argv) }; opterr = 0; // don't print errors - int c = getopt_long(argc, (char**)argv, "ahiu", sLongOptions, NULL); + int c = getopt_long(argc, (char**)argv, "aDhiu", sLongOptions, NULL); if (c == -1) break; @@ -91,6 +150,10 @@ SearchCommand::Execute(int argc, const char* const* argv) listAll = true; break; + case 'D': + details = true; + break; + case 'h': PrintUsageAndExit(false); break; @@ -139,66 +202,93 @@ SearchCommand::Execute(int argc, const char* const* argv) return 0; } + // sort packages by name and installation location/repository + const BSolverRepository* systemRepository + = static_cast( + packageManager.SystemRepository()); + const BSolverRepository* homeRepository + = static_cast( + packageManager.HomeRepository()); + PackageComparator comparator(systemRepository, homeRepository); + packages.SortItems(&compare_packages, &comparator); + // print table + TextTable table; - // determine column widths - BString installedColumnTitle("Installed"); - BString nameColumnTitle("Name"); - BString descriptionColumnTitle("Description"); + if (details) { + table.AddColumn("Repository"); + table.AddColumn("Name"); + table.AddColumn("Version"); + table.AddColumn("Arch"); - int installedColumnWidth = installedColumnTitle.Length(); - int nameColumnWidth = nameColumnTitle.Length(); - int descriptionColumnWidth = descriptionColumnTitle.Length(); + int32 packageCount = packages.CountItems(); + for (int32 i = 0; i < packageCount; i++) { + BSolverPackage* package = packages.ItemAt(i); - int32 packageCount = packages.CountItems(); - for (int32 i = 0; i < packageCount; i++) { - BSolverPackage* package = packages.ItemAt(i); - nameColumnWidth = std::max(nameColumnWidth, - (int)package->Name().Length()); - descriptionColumnWidth = std::max(descriptionColumnWidth, - (int)package->Info().Summary().Length()); + BString repository = ""; + if (package->Repository() == systemRepository) + repository = ""; + else if (package->Repository() == homeRepository) + repository = ""; + else + repository = package->Repository()->Name(); + + table.SetTextAt(i, 0, repository); + table.SetTextAt(i, 1, package->Name()); + table.SetTextAt(i, 2, package->Version().ToString()); + table.SetTextAt(i, 3, package->Info().ArchitectureName()); + } + } else { + table.AddColumn("Status"); + table.AddColumn("Name"); + table.AddColumn("Description", B_ALIGN_LEFT, true); + + int32 packageCount = packages.CountItems(); + for (int32 i = 0; i < packageCount;) { + // find the next group of equally named packages + int32 groupStart = i; + std::set versions; + BSolverPackage* systemPackage = NULL; + BSolverPackage* homePackage = NULL; + while (i < packageCount) { + BSolverPackage* package = packages.ItemAt(i); + if (i > groupStart + && package->Name() != packages.ItemAt(groupStart)->Name()) { + break; + } + + if (package->Repository() == systemRepository) + systemPackage = package; + else if (package->Repository() == homeRepository) + homePackage = package; + else + versions.insert(package->Version()); + + i++; + } + + // add a table row for the group + BString status; + if (systemPackage != NULL) { + status << (versions.find(systemPackage->Version()) + != versions.end() ? 'S' : 's'); + } + if (homePackage != NULL) { + status << (versions.find(homePackage->Version()) + != versions.end() ? 'H' : 'h'); + } + if (versions.size() > 1) + status << 'v'; + + int32 rowIndex = table.CountRows(); + BSolverPackage* package = packages.ItemAt(groupStart); + table.SetTextAt(rowIndex, 0, status); + table.SetTextAt(rowIndex, 1, package->Name()); + table.SetTextAt(rowIndex, 2, package->Info().Summary()); + } } - // print header - BString header; - header.SetToFormat("%-*s %-*s %s", - installedColumnWidth, installedColumnTitle.String(), - nameColumnWidth, nameColumnTitle.String(), - descriptionColumnTitle.String()); - printf("%s\n", header.String()); - - int minLineWidth = header.Length(); - int lineWidth = minLineWidth + descriptionColumnWidth - - descriptionColumnTitle.Length(); - int terminalWidth = get_terminal_width(); - if (lineWidth > terminalWidth) { - // truncate description - int actualLineWidth = std::max(minLineWidth, terminalWidth); - descriptionColumnWidth -= lineWidth - actualLineWidth; - lineWidth = actualLineWidth; - } - - header.SetTo('-', lineWidth); - printf("%s\n", header.String()); - - // print packages - for (int32 i = 0; i < packageCount; i++) { - BSolverPackage* package = packages.ItemAt(i); - - const char* installed = ""; - if (package->Repository() == static_cast( - packageManager.SystemRepository())) - installed = "system"; - else if (package->Repository() == static_cast( - packageManager.HomeRepository())) - installed = "home"; - - printf("%-*s %-*s %-*.*s\n", - installedColumnWidth, installed, - nameColumnWidth, package->Name().String(), - descriptionColumnWidth, descriptionColumnWidth, - package->Info().Summary().String()); - } + table.Print(get_terminal_width()); return 0; }